From ec6ad0b615cc8737edfd4bc868756d05c39c0a79 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Mon, 5 Feb 2024 14:28:54 +0100 Subject: [PATCH 01/28] Fix sign-in and confirm sign-in UI and UX --- .../src/app/auth/login/page.tsx | 99 +++++++---- .../molecules/ResendEmail/ResendEmail.tsx | 29 +++- .../organisms/Auth/ConfirmLogin.tsx | 73 +++++--- .../components/organisms/Auth/LoginForm.tsx | 158 +++++++++++------- .../src/helpers/auth.helpers.ts | 15 ++ .../src/lib/types/loginSteps.ts | 1 + apps/nextjs-website/src/messages/it.json | 18 +- 7 files changed, 262 insertions(+), 131 deletions(-) diff --git a/apps/nextjs-website/src/app/auth/login/page.tsx b/apps/nextjs-website/src/app/auth/login/page.tsx index b6aba87673..f4f790f9bc 100644 --- a/apps/nextjs-website/src/app/auth/login/page.tsx +++ b/apps/nextjs-website/src/app/auth/login/page.tsx @@ -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'; const Login = () => { const router = useRouter(); const [logInStep, setLogInStep] = useState(LoginSteps.LOG_IN); const [user, setUser] = useState(null); const [userName, setUserName] = useState(null); + const [noAccount, setNoAccount] = useState(false); + + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); const onLogin: LoginFunction = useCallback(async ({ username, password }) => { + setNoAccount(false); + setUsername(username); + setPassword(password); + const user = await Auth.signIn({ username, password, + }).catch((error) => { + if (error.code === 'UserNotConfirmedException') { + setUserName(username); + setLogInStep(LoginSteps.CONFIRM_ACCOUNT); + } else { + setNoAccount(true); + } + return false; }); setUserName(username); - setUser(user); - setLogInStep(LoginSteps.MFA_CHALLENGE); + + 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( @@ -33,36 +63,45 @@ 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 ( - - - {logInStep === LoginSteps.LOG_IN && } - {logInStep === LoginSteps.MFA_CHALLENGE && ( - - )} - - + <> + + + {logInStep === LoginSteps.LOG_IN && ( + + )} + {logInStep === LoginSteps.MFA_CHALLENGE && ( + + )} + {logInStep === LoginSteps.CONFIRM_ACCOUNT && ( + + )} + + + ); }; diff --git a/apps/nextjs-website/src/components/molecules/ResendEmail/ResendEmail.tsx b/apps/nextjs-website/src/components/molecules/ResendEmail/ResendEmail.tsx index 8edf929b4b..01c74dcfbe 100644 --- a/apps/nextjs-website/src/components/molecules/ResendEmail/ResendEmail.tsx +++ b/apps/nextjs-website/src/components/molecules/ResendEmail/ResendEmail.tsx @@ -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>; + resendCode?: () => Promise; }; -const ResendEmail = ({ text, email }: ResendEmailProps) => { +const ResendEmail = ({ + text, + email, + setSubmitting, + isLoginCTA = false, + resendCode, +}: ResendEmailProps) => { const t = useTranslations('auth.resendEmail'); const { palette } = useTheme(); @@ -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) { diff --git a/apps/nextjs-website/src/components/organisms/Auth/ConfirmLogin.tsx b/apps/nextjs-website/src/components/organisms/Auth/ConfirmLogin.tsx index 78616f996a..74d6d3dc97 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ConfirmLogin.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ConfirmLogin.tsx @@ -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; + resendCode: () => Promise; } -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(null); const [submitting, setSubmitting] = useState(false); const [code, setCode] = useState(''); + const [codeError, setCodeError] = useState(false); + const [emptyCode, setEmptyCode] = useState(false); const onConfirmLoginHandler = useCallback(() => { + setCodeError(false); + setEmptyCode(false); setSubmitting(true); + onConfirmLogin(code).catch((e) => { - setError(e.message); - setSubmitting(false); + if (e.message === 'Challenge response cannot be empty') { + setEmptyCode(true); + } else if (e.name === 'NotAuthorizedException') { + setCodeError(true); + } }); }, [onConfirmLogin, code]); @@ -57,53 +64,65 @@ const ConfirmLogin = ({ email, onConfirmLogin }: confirmLoginProps) => { - {confirmLogin.title} + {confirmLogin('title')} {email && ( - {confirmLogin.body(email)} + {confirmLogin('confirmationCodeSent')} + + {email} + +
+ {confirmLogin('confirmationCodeExpires')}
)} - {confirmLogin.code} + {confirmLogin('code')} setCode(e.target.value)} + helperText={ + codeError + ? confirmLogin('invalidCode') + : emptyCode + ? confirmLogin('emptyCode') + : '' + } + error={codeError || emptyCode} sx={{ backgroundColor: palette.background.paper, }} /> {email && ( - + )} - + - setError(null)} - > - {error} - ); }; diff --git a/apps/nextjs-website/src/components/organisms/Auth/LoginForm.tsx b/apps/nextjs-website/src/components/organisms/Auth/LoginForm.tsx index 9a721c8dec..1cf54c0d31 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/LoginForm.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/LoginForm.tsx @@ -1,55 +1,60 @@ 'use client'; -import { translations } from '@/_contents/translations'; import { LoginFunction } from '@/lib/types/loginFunction'; import { useAuthenticator } from '@aws-amplify/ui-react'; import { Visibility, VisibilityOff } from '@mui/icons-material'; import { Box, - Typography, - Stack, - Checkbox, - Grid, - TextField, - Divider, Button, Card, - FormControlLabel, + Checkbox, + Divider, FormControl, - InputLabel, - OutlinedInput, - InputAdornment, + FormControlLabel, + FormHelperText, + Grid, IconButton, - Snackbar, - Alert, + InputAdornment, + Stack, + TextField, + Typography, useTheme, } from '@mui/material'; import { IllusLogin } from '@pagopa/mui-italia'; import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { redirect } from 'next/navigation'; -import { MouseEvent, useCallback, useState } from 'react'; -import { snackbarAutoHideDurationMs } from '@/config'; +import { MouseEvent, useCallback, useEffect, useState } from 'react'; +import { validateEmail, validateField } from '@/helpers/auth.helpers'; interface LoginFormProps { onLogin: LoginFunction; + noAccount: boolean; } -const LoginForm = ({ onLogin }: LoginFormProps) => { - const { - auth: { login, signUp }, - shared, - } = translations; - const t = useTranslations('errors'); +interface LoginFieldsError { + email: string | null; + password: string | null; +} + +const LoginForm = ({ onLogin, noAccount = 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 [error, setError] = useState(null); const { authStatus } = useAuthenticator((context) => [context.authStatus]); + const [fieldErrors, setFieldErrors] = useState({ + email: null, + password: null, + }); + if (authStatus === 'authenticated') { redirect('/'); } @@ -66,14 +71,44 @@ const LoginForm = ({ onLogin }: LoginFormProps) => { [] ); + const validateForm = useCallback(() => { + const emailError = validateEmail(username); + + const passwordError = validateField(password); + setFieldErrors({ + email: emailError ? shared(emailError) : null, + password: passwordError ? shared(passwordError) : null, + }); + + return !emailError && !passwordError; + }, [username, shared, password]); + + const setNotloggedOnError = useCallback(() => { + if (noAccount) { + setFieldErrors((prevFieldErrors) => ({ + ...prevFieldErrors, + email: login('noAccountError'), + password: login('noAccountError'), + })); + } + }, [noAccount, login]); + + useEffect(() => { + setNotloggedOnError(); + }, [noAccount, setNotloggedOnError]); + const onLoginHandler = useCallback(() => { + const valid = validateForm(); + if (!valid) { + return; + } setSubmitting(true); onLogin({ username, password }) - .catch((e) => setError(t(e.code))) + .catch((e) => console.log(errors(e.code))) .finally(() => { setSubmitting(false); }); - }, [onLogin, username, password, t]); + }, [validateForm, onLogin, username, password, errors]); return ( { - {login.loginToYourAccount} + {login('loginToYourAccount')} setUsername(e.target.value)} + helperText={fieldErrors.email} + error={!!fieldErrors.email || noAccount} + required sx={{ + width: '100%', backgroundColor: palette.background.paper, }} /> - - - {shared.password} - - + setPassword(e.target.value)} - endAdornment={ - - - {showPassword ? : } - - - } - label={shared.password} - inputProps={{ - sx: { - padding: '8.5px 14px', - }, + label={`${shared('password')}`} + variant='outlined' + size='small' + error={!!fieldErrors.password || noAccount} + required + InputProps={{ + endAdornment: ( + + + {showPassword ? : } + + + ), }} /> + {(fieldErrors.password || noAccount) && ( + + {fieldErrors.password} + + )} } - label={login.rememberMe} + label={login('rememberMe')} /> @@ -167,7 +210,7 @@ const LoginForm = ({ onLogin }: LoginFormProps) => { variant='caption-semibold' color={palette.primary.main} > - {login.forgotPassword} + {login('forgotPassword')} @@ -179,7 +222,7 @@ const LoginForm = ({ onLogin }: LoginFormProps) => { alignItems='center' > - {login.noAccount} + {login('noAccount')} { variant='caption-semibold' color={palette.primary.main} > - {signUp.action} + {signUp('action')} - setError(null)} - > - {error} - ); }; diff --git a/apps/nextjs-website/src/helpers/auth.helpers.ts b/apps/nextjs-website/src/helpers/auth.helpers.ts index 2fa104a6a9..4edf065bea 100644 --- a/apps/nextjs-website/src/helpers/auth.helpers.ts +++ b/apps/nextjs-website/src/helpers/auth.helpers.ts @@ -2,3 +2,18 @@ export const passwordMatcher = /(?=(.*[0-9]))(?=.*[!@#$%^&*()\\[\]{}\-_+=~`|:;"'<>,./?])(?=.*[a-z])(?=(.*[A-Z]))(?=(.*)).{8,}/; export const emailMatcher = /^[\w-\\.\\+]+@([\w-]+\.)+[\w-]{2,4}$/; + +export const validateField = (value: string): string | null => + !value || value.trim().length === 0 ? 'requiredFieldError' : null; + +export const validateEmail = (value: string): string | null => { + return validateField(value) || !emailMatcher.test(value) + ? 'emailFieldError' + : null; +}; + +export const validatePassword = (value: string): string | null => { + return validateField(value) || !passwordMatcher.test(value) + ? 'passwordError' + : null; +}; diff --git a/apps/nextjs-website/src/lib/types/loginSteps.ts b/apps/nextjs-website/src/lib/types/loginSteps.ts index fae7bbda48..3dc26a389d 100644 --- a/apps/nextjs-website/src/lib/types/loginSteps.ts +++ b/apps/nextjs-website/src/lib/types/loginSteps.ts @@ -1,4 +1,5 @@ export enum LoginSteps { LOG_IN = 'LOG_IN', MFA_CHALLENGE = 'MFA_CHALLENGE', + CONFIRM_ACCOUNT = 'CONFIRM_ACCOUNT', } diff --git a/apps/nextjs-website/src/messages/it.json b/apps/nextjs-website/src/messages/it.json index 29dd509611..ae3d26e84d 100644 --- a/apps/nextjs-website/src/messages/it.json +++ b/apps/nextjs-website/src/messages/it.json @@ -137,6 +137,7 @@ "rememberMe": "Ricordami", "forgotPassword": "Hai dimenticato la password?", "noAccount": "Non hai un account?", + "noAccountError": "Nome utente o password non corretti.", "accountAlreadyConfirmed": { "yourAccountIsAlreadyConfirmed": "Hai già validato questo indirizzo email", "goToLogin": "Vai al Login" @@ -157,10 +158,12 @@ "signUp": { "action": "Iscriviti", "createYourAccount": "Crea il tuo account", - "confirmComunications": "Inviami e-mail relative alle risorse e agli aggiornamenti sui prodotti. Se questa casella è selezionata, PagoPA ti invierà di tanto in tanto delle e-mail utili e pertinenti. Puoi annullare l'iscrizione in qualsiasi momento.", - "acceptPolicy": "Cliccando su “Iscriviti” accetti la nostra informativa sul trattamento dei dati personali per la Privacy Policy.", + "passwordMismatchError": "Le password non combaciano", + "confirmComunications": "Inviami email relative alle risorse e agli aggiornamenti sui prodotti. Se questa casella è selezionata, PagoPA ti invierà di tanto in tanto delle email utili e pertinenti. Puoi annullare l'iscrizione in qualsiasi momento.", + "acceptPolicy": "Cliccando su iscriviti dichiari di aver preso visione della nostra informativa privacy e dei nostri termini e condizioni d'uso.", "alreadyHaveAnAccount": "Hai già un account?", "whyCreateAccount": "Perché iscriversi a PagoPA DevPortal", + "passwordError": "Non rispetta i requisiti.", "passwordPolicy": "Minimo 8 caratteri, almeno un numero, almeno una lettera maiuscola e almeno un carattere speciale", "emailSent": "Email inviata a {email}", "advantages": { @@ -203,11 +206,13 @@ }, "confirmLogin": { "title": "Verifica la tua identità", - "body": "Abbiamo inviato un codice di verifica a {email}
Il codice scade tra 3 minuti.", "code": "Codice di verifica", + "confirmationCodeSent": "Abbiamo inviato un codice di verifica a ", + "confirmationCodeExpires": "Il codice scade tra 3 minuti.", "checkJunkMail": "Non hai ricevuto alcuna email? Controlla la posta indesiderata oppure", "continue": "Continua", - "resendEmail": "Reinvia email" + "resendEmail": "Reinvia email", + "invalidCode": "Il codice inserito non è corretto" }, "accountActivated": { "goToLogin": "Vai al login", @@ -218,9 +223,9 @@ "title": "Recupera password", "body": "Inserisci il tuo indirizzo e-mail e ti invieremo le istruzioni per impostare una nuova password.", "goBackToLogin": "Torna al login", - "send": "Invia", + "send": "Conferma", "checkEmailTitle": "Controlla l'email", - "checkEmail": "Se esiste un account associato a {email}, riceverai una e-mail con un link per impostare una nuova password.", + "checkEmail": "Se esiste un account associato a {email}, riceverai una email con un link per impostare una nuova password.", "resendEmailPrompt": "Non hai ricevuto l'email? Controlla nella posta indesiderata oppure", "resendEmail": "Reinvia e-mail", "wrongEmail": "L'indirizzo e-mail è errato?", @@ -252,6 +257,7 @@ "role": "Ruolo", "requiredFieldError": "Questo campo non può essere vuoto", "emailFieldError": "Inserisci un indirizzo email valido", + "emailAlreadyTaken": "Esiste già un account per questo indirizzo email", "goToModel": "Vai al modello", "edit": "Modifica", "cancel": "Annulla", From 4762a88b634755df36fbb3b1298578ee23622d86 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Mon, 5 Feb 2024 14:45:02 +0100 Subject: [PATCH 02/28] Add changeset --- .changeset/twenty-drinks-talk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/twenty-drinks-talk.md diff --git a/.changeset/twenty-drinks-talk.md b/.changeset/twenty-drinks-talk.md new file mode 100644 index 0000000000..fb08adcac1 --- /dev/null +++ b/.changeset/twenty-drinks-talk.md @@ -0,0 +1,5 @@ +--- +"nextjs-website": patch +--- + +Fix sign-in and confirm sign-in UI and UX From c60e89ea4c1c719f367f795f273107daf894116a Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Mon, 5 Feb 2024 18:10:35 +0100 Subject: [PATCH 03/28] Fix sign-up and confirm sign-up UI and UX --- .changeset/lazy-poets-melt.md | 5 + .../src/_contents/translations.ts | 6 +- .../src/app/auth/sign-up/page.tsx | 61 +-- .../molecules/CheckItem/CheckItem.tsx | 19 +- .../organisms/Auth/ConfirmSignUp.tsx | 26 +- .../components/organisms/Auth/SignUpForm.tsx | 427 ++++++++++-------- apps/nextjs-website/src/config.ts | 21 + apps/nextjs-website/src/messages/it.json | 3 +- 8 files changed, 315 insertions(+), 253 deletions(-) create mode 100644 .changeset/lazy-poets-melt.md diff --git a/.changeset/lazy-poets-melt.md b/.changeset/lazy-poets-melt.md new file mode 100644 index 0000000000..8b4151999f --- /dev/null +++ b/.changeset/lazy-poets-melt.md @@ -0,0 +1,5 @@ +--- +"nextjs-website": patch +--- + +Fix sign-up and confirm sign-up UI and UX diff --git a/apps/nextjs-website/src/_contents/translations.ts b/apps/nextjs-website/src/_contents/translations.ts index c25ed73fae..064c1eea4b 100644 --- a/apps/nextjs-website/src/_contents/translations.ts +++ b/apps/nextjs-website/src/_contents/translations.ts @@ -325,7 +325,7 @@ export const translations = { checkJunkMail: 'Non hai ricevuto alcuna email? Controlla la posta indesiderata oppure', continue: 'Continua', - resendEmail: 'Reinvia e-mail', + resendEmail: 'Reinvia email', }, accountActivated: { goToLogin: 'Vai al login', @@ -383,7 +383,7 @@ export const translations = { `Abbiamo inviato una e-mail a ${email}. Clicca sul bottone contenuto al suo interno per verificarla.`, didntReceiveEmail: "Non hai ricevuto l'e-mail? Controlla se nella posta indesiderata oppure", - resendEmail: 'Reinvia e-mail', + resendEmail: 'Reinvia email', wrongEmail: "L'indirizzo email è errato?", }, resetPassword: { @@ -396,7 +396,7 @@ export const translations = { `Se esiste un account associato a ${email}, riceverai una e-mail con un link per impostare una nuova password.`, resendEmailPrompt: "Non hai ricevuto l'email? Controlla nella posta indesiderata oppure", - resendEmail: 'Reinvia e-mail', + resendEmail: 'Reinvia email', wrongEmail: "L'indirizzo e-mail è errato?", passwordSet: 'Password impostata correttamente', newPassword: 'Imposta una nuova password', diff --git a/apps/nextjs-website/src/app/auth/sign-up/page.tsx b/apps/nextjs-website/src/app/auth/sign-up/page.tsx index 3d88e5402a..51565a9eef 100644 --- a/apps/nextjs-website/src/app/auth/sign-up/page.tsx +++ b/apps/nextjs-website/src/app/auth/sign-up/page.tsx @@ -1,37 +1,21 @@ 'use client'; -import { translations } from '@/_contents/translations'; import CheckItem from '@/components/molecules/CheckItem/CheckItem'; import ConfirmSignUp from '@/components/organisms/Auth/ConfirmSignUp'; import SignUpForm from '@/components/organisms/Auth/SignUpForm'; import { SignUpSteps } from '@/lib/types/signUpSteps'; -import { - Alert, - Box, - Grid, - Snackbar, - Typography, - useMediaQuery, -} from '@mui/material'; +import { Box, Grid, Typography, useMediaQuery } from '@mui/material'; import { Auth } from 'aws-amplify'; import { useRouter, useSearchParams } from 'next/navigation'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { SignUpUserData } from '@/lib/types/sign-up'; import { useTranslations } from 'next-intl'; -import { snackbarAutoHideDurationMs } from '@/config'; - -interface Info { - message: string; - isError: boolean; -} +import { signUpAdvantages } from '@/config'; const SignUp = () => { - const { - auth: { signUp }, - } = translations; - const t = useTranslations('errors'); const params = useSearchParams(); const router = useRouter(); const isSmallScreen = useMediaQuery('(max-width: 1000px)'); + const signUp = useTranslations('auth.signUp'); const [userData, setUserData] = useState({ username: decodeURIComponent(params.get('email') || ''), @@ -48,7 +32,7 @@ const SignUp = () => { params.get('step') || SignUpSteps.SIGN_UP ); - const [info, setInfo] = useState(null); + const [userAlreadyExist, setUserAlreadyExist] = useState(false); const goToConfirmSignUp = useCallback(() => { router.replace( @@ -60,6 +44,7 @@ const SignUp = () => { }, [router, userData.username]); const onSignUp = useCallback(async () => { + setUserAlreadyExist(false); const { company, firstName, @@ -82,12 +67,11 @@ const SignUp = () => { 'custom:company_type': company, }, }).catch((error) => { - if (error.code.includes('UsernameExistsException')) { - setInfo({ message: t(error.code), isError: true }); - goToConfirmSignUp(); - return true; + if (error.code === 'UsernameExistsException') { + setUserAlreadyExist(true); + } else { + setUserAlreadyExist(false); } - setInfo({ message: t(error.code), isError: true }); return false; }); @@ -97,7 +81,7 @@ const SignUp = () => { goToConfirmSignUp(); return !!result.user; } - }, [userData, t, goToConfirmSignUp]); + }, [userData, goToConfirmSignUp]); const onBackStep = useCallback(() => { router.replace( @@ -109,6 +93,8 @@ const SignUp = () => { return null; }, [router, userData.username]); + const advantages = useMemo(() => signUpAdvantages, []); + return ( <> { > - {signUp.whyCreateAccount} + {signUp('whyCreateAccount')} - {signUp.advantages.map((advantage, index) => { + {advantages.map((advantage, index) => { return ( ); })} @@ -151,6 +140,7 @@ const SignUp = () => { userData={userData} setUserData={setUserData} onSignUp={onSignUp} + userAlreadyExist={userAlreadyExist} /> )} {signUpStep === SignUpSteps.CONFIRM_SIGN_UP && ( @@ -159,15 +149,6 @@ const SignUp = () => { - setInfo(null)} - > - - {info?.message} - - ); }; diff --git a/apps/nextjs-website/src/components/molecules/CheckItem/CheckItem.tsx b/apps/nextjs-website/src/components/molecules/CheckItem/CheckItem.tsx index 3757590654..9b70f0d934 100644 --- a/apps/nextjs-website/src/components/molecules/CheckItem/CheckItem.tsx +++ b/apps/nextjs-website/src/components/molecules/CheckItem/CheckItem.tsx @@ -1,13 +1,16 @@ 'use client'; import { CheckCircle } from '@mui/icons-material'; -import { Grid, Typography, useTheme } from '@mui/material'; +import { Box, Chip, Grid, Typography, useTheme } from '@mui/material'; +import { useTranslations } from 'next-intl'; interface CheckItemProps { title: string; description: string; + isComingSoon: boolean; } -const CheckItem = ({ title, description }: CheckItemProps) => { +const CheckItem = ({ title, description, isComingSoon }: CheckItemProps) => { + const shared = useTranslations('shared'); const { palette } = useTheme(); return ( @@ -16,6 +19,18 @@ const CheckItem = ({ title, description }: CheckItemProps) => { + + {isComingSoon && ( + + )} + { - const { - auth: { confirmSignUp }, - shared, - } = translations; + const confirmSignUp = useTranslations('auth.confirmSignUp'); + const shared = useTranslations('shared'); return ( @@ -32,12 +30,20 @@ const ConfirmSignUp = ({ email, onBack }: ConfirmSignUpProps) => { - {confirmSignUp.confirmSignUp} + {confirmSignUp('title')} - {confirmSignUp.description(email)} + {confirmSignUp('emailSent')} + + {email} + +
+ {confirmSignUp('clickToConfirm')}
- + { flexDirection='row' > - {confirmSignUp.wrongEmail} + {confirmSignUp('wrongEmail')} - {shared.goBack} + {shared('goBack')}
diff --git a/apps/nextjs-website/src/components/organisms/Auth/SignUpForm.tsx b/apps/nextjs-website/src/components/organisms/Auth/SignUpForm.tsx index f327314b91..5ae570e818 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/SignUpForm.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/SignUpForm.tsx @@ -1,33 +1,31 @@ 'use client'; -import { translations } from '@/_contents/translations'; -import RequiredTextField, { - ValidatorFunction, -} from '@/components/molecules/RequiredTextField/RequiredTextField'; -import { emailMatcher, passwordMatcher } from '@/helpers/auth.helpers'; +import { + validateEmail, + validateField, + validatePassword, +} from '@/helpers/auth.helpers'; import { SignUpUserData } from '@/lib/types/sign-up'; import { useAuthenticator } from '@aws-amplify/ui-react'; import { Visibility, VisibilityOff } from '@mui/icons-material'; import { Box, - Typography, - Stack, - Checkbox, - Grid, - TextField, - Divider, Button, Card, - FormControlLabel, + Checkbox, + Divider, FormControl, - InputLabel, - Select, - MenuItem, - OutlinedInput, - InputAdornment, - IconButton, + FormControlLabel, FormHelperText, + Grid, + IconButton, + InputAdornment, + MenuItem, + Stack, + TextField, + Typography, useTheme, } from '@mui/material'; +import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { redirect } from 'next/navigation'; import { @@ -36,22 +34,35 @@ import { SetStateAction, useCallback, useEffect, + useMemo, useState, } from 'react'; +import { companyRoles as configCompanyRoles } from '@/config'; interface SignUpFormProps { userData: SignUpUserData; - setUserData: Dispatch>; - onSignUp: () => Promise; + userAlreadyExist: boolean; } -const SignUpForm = ({ userData, setUserData, onSignUp }: SignUpFormProps) => { - const { - auth: { login, signUp }, - shared, - } = translations; +interface SignUpFieldsError { + name: string | null; + surname: string | null; + email: string | null; + password: string | null; + confirmPassword: string | null; +} + +const SignUpForm = ({ + userData, + setUserData, + onSignUp, + userAlreadyExist, +}: SignUpFormProps) => { + const login = useTranslations('auth.login'); + const signUp = useTranslations('auth.signUp'); + const shared = useTranslations('shared'); const { company, @@ -66,10 +77,14 @@ const SignUpForm = ({ userData, setUserData, onSignUp }: SignUpFormProps) => { const { palette } = useTheme(); const [showPassword, setShowPassword] = useState(false); - const [isPasswordValid, setIsPasswordValid] = useState(false); - const [isPasswordDirty, setIsPasswordDirty] = useState(false); - const [isFormValid, setIsFormValid] = useState(false); - const [submitting, setSubmitting] = useState(false); + + const [fieldErrors, setFieldErrors] = useState({ + name: null, + surname: null, + email: null, + password: null, + confirmPassword: null, + }); const { authStatus } = useAuthenticator((context) => [context.authStatus]); @@ -85,63 +100,64 @@ const SignUpForm = ({ userData, setUserData, onSignUp }: SignUpFormProps) => { event.preventDefault(); }; - const emailValidators: ValidatorFunction[] = [ - (value: string) => ({ - valid: emailMatcher.test(value), - error: shared.emailFieldError, - }), - ]; - - const validatePassword = useCallback(() => { - setIsPasswordValid(passwordMatcher.test(password)); - }, [password]); - - useEffect(() => { - validatePassword(); - }, [password, validatePassword]); - - const onSignUpClick = useCallback(() => { + const validateForm = useCallback(() => { const { username, confirmPassword, firstName, lastName, password } = userData; - if ( - [firstName, lastName, username, password, confirmPassword].some( - (value) => !value || value.trim().length === 0 - ) - ) { - return; - } + const nameError = validateField(firstName); + const surnameError = validateField(lastName); + const emailError = validateEmail(username); + const emailEmptyError = validateField(username); + const passwordError = validatePassword(password); + const confirmPasswordError = password !== confirmPassword; - if (password !== confirmPassword) { - return; - } + setFieldErrors({ + name: nameError ? shared('requiredFieldError') : null, + surname: surnameError ? shared('requiredFieldError') : null, + email: emailEmptyError + ? shared('requiredFieldError') + : emailError + ? shared(emailError) + : null, + password: passwordError ? signUp('passwordPolicy') : null, + confirmPassword: confirmPasswordError + ? signUp('passwordMismatchError') + : null, + }); - setSubmitting(true); + return ( + !nameError && + !surnameError && + !emailError && + !passwordError && + !confirmPasswordError + ); + }, [shared, signUp, userData]); - onSignUp().finally(() => { - setSubmitting(false); - }); - }, [onSignUp, userData]); + const setEmailErrorIfUserExists = useCallback(() => { + if (userAlreadyExist) { + setFieldErrors((prevFieldErrors) => ({ + ...prevFieldErrors, + email: shared('emailAlreadyTaken'), + })); + } + }, [userAlreadyExist, shared]); - const validateForm = useCallback(() => { - const { username, confirmPassword, firstName, lastName, password } = - userData; + useEffect(() => { + setEmailErrorIfUserExists(); + }, [userAlreadyExist, setEmailErrorIfUserExists]); - const areFieldsValid = [ - firstName, - lastName, - username, - password, - confirmPassword, - ].every((value) => value && value.trim().length > 0); - const isPasswordEqual = password === confirmPassword; + const onSignUpClick = useCallback(() => { + const valid = validateForm(); - setIsFormValid(areFieldsValid && isPasswordEqual && isPasswordValid); - }, [isPasswordValid, userData]); + if (!valid) { + return; + } - useEffect(() => { - validateForm(); - }, [validateForm, isPasswordValid, userData]); + onSignUp(); + }, [onSignUp, validateForm]); + + const companyRoles = useMemo(() => configCompanyRoles, []); if (authStatus === 'authenticated') { redirect('/'); @@ -153,16 +169,17 @@ const SignUpForm = ({ userData, setUserData, onSignUp }: SignUpFormProps) => { - {signUp.createYourAccount} + {signUp('createYourAccount')} - {shared.requiredFields} + {shared('requiredFields')}
- setUserData((prevData) => ({ @@ -170,12 +187,19 @@ const SignUpForm = ({ userData, setUserData, onSignUp }: SignUpFormProps) => { firstName: value, })) } - helperText={shared.requiredFieldError} + helperText={fieldErrors.name} + error={!!fieldErrors.name} + required + sx={{ + backgroundColor: 'white', + width: '100%', + }} /> - setUserData((prevData) => ({ @@ -183,152 +207,161 @@ const SignUpForm = ({ userData, setUserData, onSignUp }: SignUpFormProps) => { lastName: value, })) } - helperText={shared.requiredFieldError} + helperText={fieldErrors.surname} + error={!!fieldErrors.surname} + required + sx={{ + backgroundColor: 'white', + width: '100%', + }} /> - setUserData((prevData) => ({ ...prevData, username: value, })) } - helperText={shared.emailFieldError} - customValidators={emailValidators} + helperText={fieldErrors.email} + error={!!fieldErrors.email} + required + sx={{ + backgroundColor: 'white', + width: '100%', + }} /> - - - {shared.password} - - label) + p': { + display: 'none', + }, + '& div.MuiFormControl-root:has(>label) + p.Mui-error': { + display: 'block', + }, + '& div.MuiFormControl-root:has(>label.Mui-focused) + p': { + display: 'block', + }, + }} + > + setUserData((prevData) => ({ ...prevData, password: value, })) } - onBlur={() => setIsPasswordDirty(true)} - error={isPasswordDirty && !isPasswordValid} - endAdornment={ - - - {showPassword ? : } - - - } - value={password} - label={shared.password} - inputProps={{ - sx: { - padding: '8.5px 14px', - }, + label={`${shared('password')}`} + variant='outlined' + size='small' + error={!!fieldErrors.password} + InputProps={{ + endAdornment: ( + + + {showPassword ? : } + + + ), }} + value={password} /> - - {signUp.passwordPolicy} + + {fieldErrors.password ? `${signUp('passwordError')} ` : ''} + {signUp('passwordPolicy')} - - - {shared.confirmPassword} - - + setUserData((prevData) => ({ ...prevData, confirmPassword: value, })) } - endAdornment={ - - - {showPassword ? : } - - - } - value={confirmPassword} - label={shared.confirmPassword} - error={isPasswordDirty && password !== confirmPassword} - inputProps={{ - sx: { - padding: '8.5px 14px', - }, + label={`${shared('confirmPassword')}`} + variant='outlined' + size='small' + error={!!fieldErrors.confirmPassword} + required + InputProps={{ + endAdornment: ( + + + {showPassword ? : } + + + ), }} + value={confirmPassword} /> + {fieldErrors.confirmPassword && ( + + {signUp('passwordMismatchError')} + + )} - - - {shared.company} - - + @@ -357,44 +390,44 @@ const SignUpForm = ({ userData, setUserData, onSignUp }: SignUpFormProps) => { sx={{ marginTop: '-4px' }} /> } - label={signUp.confirmComunications} + label={signUp('confirmComunications')} sx={{ alignItems: 'flex-start' }} /> - - {signUp.acceptPolicy1} - - {signUp.acceptPolicy2} - - {signUp.acceptPolicy3} - - {signUp.acceptPolicy4} - + {signUp.rich('acceptPolicy', { + terms: (chunks) => ( + + {chunks} + + ), + policy: (chunks) => ( + + {chunks} + + ), + })} @@ -409,7 +442,7 @@ const SignUpForm = ({ userData, setUserData, onSignUp }: SignUpFormProps) => { flexDirection='row' > - {signUp.alreadyHaveAnAccount} + {signUp('alreadyHaveAnAccount')} { variant='caption-semibold' color={palette.primary.main} > - {login.action} + {login('action')} diff --git a/apps/nextjs-website/src/config.ts b/apps/nextjs-website/src/config.ts index 7102061f4b..b7af18eeb1 100644 --- a/apps/nextjs-website/src/config.ts +++ b/apps/nextjs-website/src/config.ts @@ -39,6 +39,27 @@ export const baseUrl = isProduction export const defaultOgTagImage = `${baseUrl}/images/dev-portal-home.jpg`; export const resetResendEmailAfterMs = 4_000; +export const companyRoles = [ + 'ente-pubblico', + 'partner-tecnologico', + 'psp', + 'gestore-di-pubblico-servizio', + 'azienda-privata', + 'altro', +]; + +export const signUpAdvantages = [ + 'exclusive_contents', + 'product_updates', + 'api_keys', + 'support', +]; + +export const webinarQuestionConfig = { + url: process.env.NEXT_PUBLIC_WEBINAR_QUESTION_URL, + resource: process.env.NEXT_PUBLIC_WEBINAR_QUESTION_SHEET_NAME, +}; + export const defaultLanguage = { id: 'it', value: 'Italiano' }; export const languages = [defaultLanguage]; diff --git a/apps/nextjs-website/src/messages/it.json b/apps/nextjs-website/src/messages/it.json index ae3d26e84d..4f82475a08 100644 --- a/apps/nextjs-website/src/messages/it.json +++ b/apps/nextjs-website/src/messages/it.json @@ -199,7 +199,8 @@ }, "confirmSignUp": { "title": "Confermaci che sei tu", - "emailSent": "Abbiamo inviato una email a {email}. Clicca sul pulsante contenuto al suo interno per verificarla.", + "emailSent": "Abbiamo inviato una email a ", + "clickToConfirm": "Clicca sul pulsante contenuto al suo interno per verificarla.", "didntReceiveEmail": "Non hai ricevuto l'email? Controlla la posta indesiderata oppure", "resendEmail": "Invia nuovamente l'email", "wrongEmail": "L'indirizzo email è errato?" From d6d4272c995023abfd6ebede2e0da7b5f0431821 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Tue, 6 Feb 2024 12:00:14 +0100 Subject: [PATCH 04/28] Remove duplicated state constant --- apps/nextjs-website/src/app/auth/login/page.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/nextjs-website/src/app/auth/login/page.tsx b/apps/nextjs-website/src/app/auth/login/page.tsx index f4f790f9bc..6bf2b94a85 100644 --- a/apps/nextjs-website/src/app/auth/login/page.tsx +++ b/apps/nextjs-website/src/app/auth/login/page.tsx @@ -14,7 +14,6 @@ const Login = () => { const router = useRouter(); const [logInStep, setLogInStep] = useState(LoginSteps.LOG_IN); const [user, setUser] = useState(null); - const [userName, setUserName] = useState(null); const [noAccount, setNoAccount] = useState(false); const [username, setUsername] = useState(''); @@ -30,7 +29,7 @@ const Login = () => { password, }).catch((error) => { if (error.code === 'UserNotConfirmedException') { - setUserName(username); + setUsername(username); setLogInStep(LoginSteps.CONFIRM_ACCOUNT); } else { setNoAccount(true); @@ -38,7 +37,7 @@ const Login = () => { return false; }); - setUserName(username); + setUsername(username); if (user) { setUser(user); @@ -68,13 +67,13 @@ const Login = () => { const onBackStep = useCallback(() => { router.replace( - `/auth/login?email=${encodeURIComponent(userName || '')}&step=${ + `/auth/login?email=${encodeURIComponent(username || '')}&step=${ LoginSteps.LOG_IN }` ); setLogInStep(LoginSteps.LOG_IN); return null; - }, [router, userName]); + }, [router, username]); return ( <> @@ -91,13 +90,13 @@ const Login = () => { )} {logInStep === LoginSteps.MFA_CHALLENGE && ( )} {logInStep === LoginSteps.CONFIRM_ACCOUNT && ( - + )} From 07605aa40a653640617698a4689f9413830caade Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Tue, 6 Feb 2024 12:03:45 +0100 Subject: [PATCH 05/28] Remove useless fragment --- .../src/app/auth/login/page.tsx | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/apps/nextjs-website/src/app/auth/login/page.tsx b/apps/nextjs-website/src/app/auth/login/page.tsx index 6bf2b94a85..72ab51edb1 100644 --- a/apps/nextjs-website/src/app/auth/login/page.tsx +++ b/apps/nextjs-website/src/app/auth/login/page.tsx @@ -76,31 +76,29 @@ const Login = () => { }, [router, username]); return ( - <> - - - {logInStep === LoginSteps.LOG_IN && ( - - )} - {logInStep === LoginSteps.MFA_CHALLENGE && ( - - )} - {logInStep === LoginSteps.CONFIRM_ACCOUNT && ( - - )} - - - + + + {logInStep === LoginSteps.LOG_IN && ( + + )} + {logInStep === LoginSteps.MFA_CHALLENGE && ( + + )} + {logInStep === LoginSteps.CONFIRM_ACCOUNT && ( + + )} + + ); }; From 53fccf2849c2265c4d1989fe01895d5f6846377e Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Tue, 6 Feb 2024 12:31:33 +0100 Subject: [PATCH 06/28] Rename constant and add an explicit cast to avoid a linting warning --- apps/nextjs-website/src/app/auth/login/page.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/nextjs-website/src/app/auth/login/page.tsx b/apps/nextjs-website/src/app/auth/login/page.tsx index 72ab51edb1..38b177e777 100644 --- a/apps/nextjs-website/src/app/auth/login/page.tsx +++ b/apps/nextjs-website/src/app/auth/login/page.tsx @@ -9,30 +9,31 @@ 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 [noAccount, setNoAccount] = useState(false); - const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); + const [noAccountError, setNoAccountError] = useState(false); + const onLogin: LoginFunction = useCallback(async ({ username, password }) => { - setNoAccount(false); + setNoAccountError(false); setUsername(username); setPassword(password); const user = await Auth.signIn({ username, password, - }).catch((error) => { + } as SignInOpts).catch((error) => { if (error.code === 'UserNotConfirmedException') { setUsername(username); setLogInStep(LoginSteps.CONFIRM_ACCOUNT); } else { - setNoAccount(true); + setNoAccountError(true); } return false; }); @@ -85,7 +86,7 @@ const Login = () => { spacing={6} > {logInStep === LoginSteps.LOG_IN && ( - + )} {logInStep === LoginSteps.MFA_CHALLENGE && ( Date: Tue, 6 Feb 2024 12:48:29 +0100 Subject: [PATCH 07/28] test(DEV-1333): add auth helpers' tests --- .../__tests__/helpers/auth.helpers.test.ts | 45 +++++++++++-------- .../src/helpers/auth.helpers.ts | 18 +++++--- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/apps/nextjs-website/src/__tests__/helpers/auth.helpers.test.ts b/apps/nextjs-website/src/__tests__/helpers/auth.helpers.test.ts index 578267c373..ae528c4709 100644 --- a/apps/nextjs-website/src/__tests__/helpers/auth.helpers.test.ts +++ b/apps/nextjs-website/src/__tests__/helpers/auth.helpers.test.ts @@ -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('mail@example.com')).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); }); }); diff --git a/apps/nextjs-website/src/helpers/auth.helpers.ts b/apps/nextjs-website/src/helpers/auth.helpers.ts index 4edf065bea..32ca00855e 100644 --- a/apps/nextjs-website/src/helpers/auth.helpers.ts +++ b/apps/nextjs-website/src/helpers/auth.helpers.ts @@ -7,13 +7,19 @@ export const validateField = (value: string): string | null => !value || value.trim().length === 0 ? 'requiredFieldError' : null; export const validateEmail = (value: string): string | null => { - return validateField(value) || !emailMatcher.test(value) - ? 'emailFieldError' - : null; + const isNotValid = validateField(value); + if (isNotValid) { + return isNotValid; + } + + return !emailMatcher.test(value) ? 'emailFieldError' : null; }; export const validatePassword = (value: string): string | null => { - return validateField(value) || !passwordMatcher.test(value) - ? 'passwordError' - : null; + const isNotValid = validateField(value); + if (isNotValid) { + return isNotValid; + } + + return !passwordMatcher.test(value) ? 'passwordError' : null; }; From efafaadc65dc293621f0fc7e2da2907621d8287c Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Tue, 6 Feb 2024 14:43:28 +0100 Subject: [PATCH 08/28] Fix reset password request page UI and UX --- .../src/app/auth/password-reset/page.tsx | 95 ++++++------------- .../organisms/Auth/ResetPasswordForm.tsx | 76 ++++++++------- .../organisms/Auth/ResetPasswordSuccess.tsx | 32 ++++--- apps/nextjs-website/src/messages/it.json | 11 ++- 4 files changed, 95 insertions(+), 119 deletions(-) diff --git a/apps/nextjs-website/src/app/auth/password-reset/page.tsx b/apps/nextjs-website/src/app/auth/password-reset/page.tsx index 4c2da659d8..e50592d23c 100644 --- a/apps/nextjs-website/src/app/auth/password-reset/page.tsx +++ b/apps/nextjs-website/src/app/auth/password-reset/page.tsx @@ -1,49 +1,32 @@ 'use client'; -import { Alert, Box, Grid, Snackbar } from '@mui/material'; + +import { Grid } from '@mui/material'; import { useCallback, useState } from 'react'; -import { translations } from '@/_contents/translations'; -import { ValidatorFunction } from '@/components/molecules/RequiredTextField/RequiredTextField'; import { SendResetPasswordSteps } from '@/lib/types/sendResetPasswordSteps'; import { Auth } from 'aws-amplify'; -import { emailMatcher } from '@/helpers/auth.helpers'; import { useRouter } from 'next/navigation'; import ResetPasswordForm from '@/components/organisms/Auth/ResetPasswordForm'; import ResetPasswordSuccess from '@/components/organisms/Auth/ResetPasswordSuccess'; -import { snackbarAutoHideDurationMs } from '@/config'; -interface Info { - message: string; - isError: boolean; -} +import PageBackgroundWrapper from '@/components/atoms/PageBackgroundWrapper/PageBackgroundWrapper'; + const PasswordReset = () => { - const [info, setInfo] = useState(null); const [email, setEmail] = useState(''); const [sendResetPasswordSteps, setSendResetPasswordSteps] = useState( SendResetPasswordSteps.SEND_EMAIL ); - const { shared } = translations; - const router = useRouter(); const handleResetPassword = useCallback(async () => { - const success = await Auth.forgotPassword(email).catch((error) => { - setInfo({ message: error.message, isError: true }); + const success = await Auth.forgotPassword(email).catch(() => { return false; }); if (success) { - setInfo({ message: 'Invio Email in corso...', isError: false }); setSendResetPasswordSteps(SendResetPasswordSteps.EMAIL_SEND_CONFIRM); } }, [email]); - const emailValidators: ValidatorFunction[] = [ - (value: string) => ({ - valid: emailMatcher.test(value), - error: shared.emailFieldError, - }), - ]; - const onBackStep = useCallback(() => { router.replace( `/auth/password-reset?email=${email}&step=${SendResetPasswordSteps.SEND_EMAIL}` @@ -53,53 +36,29 @@ const PasswordReset = () => { }, [router, email]); return ( - <> - - - {sendResetPasswordSteps === SendResetPasswordSteps.SEND_EMAIL ? ( - - ) : ( - - )} - - - setInfo(null)} + + - - {info?.message} - - - + {sendResetPasswordSteps === SendResetPasswordSteps.SEND_EMAIL ? ( + + ) : ( + + )} + + ); }; diff --git a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx index 64bfe058cc..4e3ed499a7 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx @@ -8,38 +8,50 @@ import { Card, Grid, Box, + TextField, + useTheme, } from '@mui/material'; -import { emailMatcher } from '@/helpers/auth.helpers'; -import { translations } from '@/_contents/translations'; -import RequiredTextField, { - ValidatorFunction, -} from '@/components/molecules/RequiredTextField/RequiredTextField'; +import { validateEmail } from '@/helpers/auth.helpers'; import { IllusDataSecurity } from '@pagopa/mui-italia'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; - -const { - auth: { resetPassword }, - shared, -} = translations; +import { Dispatch, SetStateAction, useCallback, useState } from 'react'; +import { useTranslations } from 'next-intl'; interface ResetPasswordFormProps { email: string; setEmail: Dispatch>; handleResetPassword: () => Promise; - emailValidators: ValidatorFunction[]; } const ResetPasswordForm = ({ email, setEmail, handleResetPassword, - emailValidators, }: ResetPasswordFormProps) => { - const [isSubmitDisabled, setSubmitDisabled] = useState(false); + const resetPassword = useTranslations('auth.resetPassword'); + const shared = useTranslations('shared'); + + const [emailError, setEmailError] = useState(null); + + const validateForm = useCallback(() => { + const emailError = validateEmail(email); + + if (emailError) { + setEmailError(resetPassword('emailFieldError')); + } + + return !emailError; + }, [email, resetPassword]); + + const onResetPassword = useCallback(() => { + const valid = validateForm(); + if (!valid) { + return; + } + + handleResetPassword(); + }, [handleResetPassword, validateForm]); - useEffect(() => { - setSubmitDisabled(!emailMatcher.test(email)); - }, [email]); + const { palette } = useTheme(); return ( - {resetPassword.title} + {resetPassword('title')} - {resetPassword.body} + {resetPassword('body')} - setEmail(value)} - helperText={shared.requiredFieldError} - customValidators={emailValidators} + helperText={emailError} + error={!!emailError} + size='small' + sx={{ + backgroundColor: palette.background.paper, + width: '100%', + }} /> - @@ -98,7 +108,7 @@ const ResetPasswordForm = ({ href='/auth/login' sx={{ fontWeight: 600, cursor: 'pointer' }} > - {resetPassword.goBackToLogin} + {resetPassword('goBackToLogin')} diff --git a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordSuccess.tsx b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordSuccess.tsx index 26d2bd56ba..7488b6016a 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordSuccess.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordSuccess.tsx @@ -1,4 +1,3 @@ -import { translations } from '@/_contents/translations'; import { Box, Card, @@ -9,6 +8,7 @@ import { Typography, } from '@mui/material'; import { IllusEmailValidation } from '@pagopa/mui-italia'; +import { useTranslations } from 'next-intl'; interface ResetPasswordSuccessProps { email: string; @@ -16,16 +16,14 @@ interface ResetPasswordSuccessProps { resendEmail: () => Promise; } -const { - auth: { resetPassword }, - shared, -} = translations; - const ResetPasswordSuccess = ({ email, onBack, resendEmail, }: ResetPasswordSuccessProps) => { + const resetPassword = useTranslations('auth.resetPassword'); + const shared = useTranslations('shared'); + return ( - {resetPassword.checkEmailTitle} + {resetPassword('checkEmailTitle')} - {resetPassword.checkEmail(email)} + {resetPassword('checkEmailStart')} + + {email} + +
+ {resetPassword('checkEmailEnd')}
- {resetPassword.resendEmailPrompt}{' '} + {resetPassword('resendEmailPrompt')}{' '} - {resetPassword.resendEmail} + {resetPassword('resendEmail')} @@ -69,10 +72,13 @@ const ResetPasswordSuccess = ({ flexDirection='row' > - {resetPassword.wrongEmail} + {resetPassword('wrongEmail')} {' '} - - {shared.goBack} + + {shared('goBack')} diff --git a/apps/nextjs-website/src/messages/it.json b/apps/nextjs-website/src/messages/it.json index 4f82475a08..bcbfe52ab3 100644 --- a/apps/nextjs-website/src/messages/it.json +++ b/apps/nextjs-website/src/messages/it.json @@ -149,11 +149,11 @@ "code": "Codice di verifica", "checkJunkMail": "Non hai ricevuto alcuna email? Controlla la posta indesiderata oppure", "continue": "Continua", - "ctaLabel": "Reinvia e-mail", - "resendEmail": "Reinvia e-mail" + "ctaLabel": "Reinvia email", + "resendEmail": "Reinvia email" }, "resendEmail": { - "label": "Reinvia e-mail" + "label": "Reinvia email" }, "signUp": { "action": "Iscriviti", @@ -226,9 +226,10 @@ "goBackToLogin": "Torna al login", "send": "Conferma", "checkEmailTitle": "Controlla l'email", - "checkEmail": "Se esiste un account associato a {email}, riceverai una email con un link per impostare una nuova password.", + "checkEmailStart": "Se esiste un account associato a ", + "checkEmailEnd": "riceverai una email con un link per impostare una nuova password.", "resendEmailPrompt": "Non hai ricevuto l'email? Controlla nella posta indesiderata oppure", - "resendEmail": "Reinvia e-mail", + "resendEmail": "Reinvia email", "wrongEmail": "L'indirizzo e-mail è errato?", "passwordSet": "Password impostata correttamente", "newPassword": "Imposta una nuova password", From 6378f8e5c1b532c8c6b149e1f075766256716c35 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Tue, 6 Feb 2024 14:54:25 +0100 Subject: [PATCH 09/28] Fix translations --- apps/nextjs-website/src/messages/it.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/nextjs-website/src/messages/it.json b/apps/nextjs-website/src/messages/it.json index bcbfe52ab3..a3671ddfea 100644 --- a/apps/nextjs-website/src/messages/it.json +++ b/apps/nextjs-website/src/messages/it.json @@ -13,7 +13,7 @@ "title": "Profilo", "agreements": { "newsletter": { - "description": "Inviami e-mail relative alle risorse e agli aggiornamenti sui prodotti. Se questa opzione è attiva, PagoPA ti invierà di tanto in tanto delle e-mail utili e pertinenti.", + "description": "Inviami email relative alle risorse e agli aggiornamenti sui prodotti. Se questa opzione è attiva, PagoPA ti invierà di tanto in tanto delle email utili e pertinenti.", "error": { "subscribe": "C'è stato un errore nell'iscrizione, riprova più tardi o contatta l'assistenza tecnica.", "unsubscribe": "C'è stato un errore nella cancellazione, riprova più tardi o contatta l'assistenza tecnica." @@ -54,7 +54,7 @@ "company": "Tipologia di ente o azienda", "sector": "Settore", "products": "Prodotti di interesse", - "email": "Indirizzo e-mail", + "email": "Indirizzo email", "password": "Password" } }, @@ -222,15 +222,15 @@ }, "resetPassword": { "title": "Recupera password", - "body": "Inserisci il tuo indirizzo e-mail e ti invieremo le istruzioni per impostare una nuova password.", + "body": "Inserisci il tuo indirizzo email e ti invieremo le istruzioni per impostare una nuova password.", "goBackToLogin": "Torna al login", - "send": "Conferma", + "send": "Invia", "checkEmailTitle": "Controlla l'email", "checkEmailStart": "Se esiste un account associato a ", "checkEmailEnd": "riceverai una email con un link per impostare una nuova password.", "resendEmailPrompt": "Non hai ricevuto l'email? Controlla nella posta indesiderata oppure", "resendEmail": "Reinvia email", - "wrongEmail": "L'indirizzo e-mail è errato?", + "wrongEmail": "L'indirizzo email è errato?", "passwordSet": "Password impostata correttamente", "newPassword": "Imposta una nuova password", "rememberPassword": "Ricordi la tua password?", @@ -494,7 +494,7 @@ "CodeDeliveryFailureException": "Impossibile inviare il codice di verifica.", "ForbiddenException": "La tua richiesta non è stata consentita dal sistema di autenticazione.", "InternalErrorException": "Si è verificato un errore interno al sistema di autenticazione.", - "InvalidEmailRoleAccessPolicyException": "Il sistema di autenticazione non è autorizzato a utilizzare la tua identità e-mail.", + "InvalidEmailRoleAccessPolicyException": "Il sistema di autenticazione non è autorizzato a utilizzare la tua identità email.", "InvalidLambdaResponseException": "Il sistema di autenticazione ha riscontrato una risposta non valida.", "InvalidParameterException": "Il servizio di autenticazione ha riscontrato un parametro non valido.", "InvalidPasswordException": "Password non valida.", @@ -506,7 +506,7 @@ "UserNotFoundException": "Utente non registrato.", "NotAuthorizedException": "Operazione non autorizzata.", "PasswordResetRequiredException": "É stato richiesto un reset della password.", - "UserNotConfirmedException": "Utente non confermato, controlla nella e-mail e apri il link di conferma.", + "UserNotConfirmedException": "Utente non confermato, controlla nella email e apri il link di conferma.", "CodeMismatchException": "Codice non valido.", "ExpiredCodeException": "Codice scaduto.", "TooManyFailedAttemptsException": "Troppi tentativi errati.", From a3b8615e5f7f2a7ee209ffe58e7a327ecd5abac2 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Tue, 6 Feb 2024 16:11:48 +0100 Subject: [PATCH 10/28] Fix change password page UI and UX --- .../src/_contents/translations.ts | 80 ------- .../src/app/auth/change-password/page.tsx | 78 ++++--- .../src/app/auth/confirmation/page.tsx | 12 +- .../Auth/AccountAlreadyConfirmed.tsx | 31 +++ .../organisms/Auth/ChangePasswordForm.tsx | 209 ++++++++++-------- .../organisms/Auth/PasswordChangedCard.tsx | 44 ++-- .../organisms/Auth/ResetPasswordForm.tsx | 2 +- apps/nextjs-website/src/messages/it.json | 20 +- 8 files changed, 218 insertions(+), 258 deletions(-) create mode 100644 apps/nextjs-website/src/components/organisms/Auth/AccountAlreadyConfirmed.tsx diff --git a/apps/nextjs-website/src/_contents/translations.ts b/apps/nextjs-website/src/_contents/translations.ts index 064c1eea4b..f5070ece99 100644 --- a/apps/nextjs-website/src/_contents/translations.ts +++ b/apps/nextjs-website/src/_contents/translations.ts @@ -310,61 +310,7 @@ export const translations = { }, }, auth: { - login: { - action: 'Accedi', - loginToYourAccount: 'Accedi al tuo account DevPortal', - rememberMe: 'Ricordami', - forgotPassword: 'Hai dimenticato la password?', - noAccount: 'Non hai un account?', - }, - confirmLogin: { - title: 'Verifica la tua identità', - body: (email: string) => - `Abbiamo inviato un codice di verifica a ${email} Il codice scade tra 3 minuti.`, - code: 'Codice di verifica', - checkJunkMail: - 'Non hai ricevuto alcuna email? Controlla la posta indesiderata oppure', - continue: 'Continua', - resendEmail: 'Reinvia email', - }, - accountActivated: { - goToLogin: 'Vai al login', - welcomeMessage: 'Ti diamo il benvenuto su PagoPA DevPortal.', - yourAccountIsActive: 'Il tuo account è attivo', - }, signUp: { - action: 'Iscriviti', - createYourAccount: 'Crea il tuo account', - confirmComunications: - "Inviami e-mail relative alle risorse e agli aggiornamenti sui prodotti. Se questa casella è selezionata, PagoPA ti invierà di tanto in tanto delle e-mail utili e pertinenti. Puoi annullare l'iscrizione in qualsiasi momento.", - acceptPolicy1: - 'Cliccando su iscriviti dichiari di aver preso visione della nostra ', - acceptPolicy2: 'privacy policy', - acceptPolicy3: ' e dei nostri ', - acceptPolicy4: "termini e condizioni d'uso", - alreadyHaveAnAccount: 'Hai già un account?', - whyCreateAccount: 'Perché iscriversi a PagoPA DevPortal', - passwordPolicy: - 'Minimo 8 caratteri, almeno un numero, almeno una lettera maiuscola e almeno un carattere speciale', - emailSent: (email: string) => `Email inviata a ${email}`, - advantages: [ - { - title: 'Accedi a contenuti esclusivi', - text: 'Assisti ai webinar di PagoPA e interagisci con noi sugli argomenti del momento.', - }, - { - title: 'Ricevi aggiornamenti sui nostri prodotti', - text: "Non perdere neanche un nuovo contenuto, guida o tutorial. Puoi scegliere di ricevere via email le novità sui prodotti PagoPA e sull'integrazione tecnologica. ", - }, - { - title: 'Accedi ai tool per velocizzare i tuoi sviluppi (in arrivo)', - text: 'Sblocca tutto il potenziale del DevPortal con API Key, Mocker, SDK ed altro ancora.', - }, - { - title: 'Richiedi assistenza su un canale dedicato (in arrivo)', - text: 'Avvicinati con facilità alle soluzioni di cui hai bisogno e risolvi le difficoltà con il nostro aiuto.', - }, - ], companyRoles: [ { title: 'Ente pubblico', value: 'public-authority' }, { title: 'Partner tecnologico', value: 'tech-partner' }, @@ -377,31 +323,5 @@ export const translations = { { title: 'Altro', value: 'other' }, ], }, - confirmSignUp: { - confirmSignUp: 'Conferma che sei tu', - description: (email: string) => - `Abbiamo inviato una e-mail a ${email}. Clicca sul bottone contenuto al suo interno per verificarla.`, - didntReceiveEmail: - "Non hai ricevuto l'e-mail? Controlla se nella posta indesiderata oppure", - resendEmail: 'Reinvia email', - wrongEmail: "L'indirizzo email è errato?", - }, - resetPassword: { - title: 'Recupera password', - body: 'Inserisci il tuo indirizzo e-mail e ti invieremo le istruzioni per impostare una nuova password.', - goBackToLogin: 'Torna al login', - send: 'Invia', - checkEmailTitle: "Controlla l'email", - checkEmail: (email: string) => - `Se esiste un account associato a ${email}, riceverai una e-mail con un link per impostare una nuova password.`, - resendEmailPrompt: - "Non hai ricevuto l'email? Controlla nella posta indesiderata oppure", - resendEmail: 'Reinvia email', - wrongEmail: "L'indirizzo e-mail è errato?", - passwordSet: 'Password impostata correttamente', - newPassword: 'Imposta una nuova password', - rememberPassword: 'Ricordi la tua password?', - invalidLinkError: 'Il link non è valido', - }, }, }; diff --git a/apps/nextjs-website/src/app/auth/change-password/page.tsx b/apps/nextjs-website/src/app/auth/change-password/page.tsx index aecc39f98b..ee2a79bd20 100644 --- a/apps/nextjs-website/src/app/auth/change-password/page.tsx +++ b/apps/nextjs-website/src/app/auth/change-password/page.tsx @@ -1,5 +1,5 @@ 'use client'; -import { Alert, Box, Grid, Snackbar } from '@mui/material'; +import { Grid } from '@mui/material'; import PasswordChangedCard from '@/components/organisms/Auth/PasswordChangedCard'; import ChangePasswordForm from '@/components/organisms/Auth/ChangePasswordForm'; import { ResetPasswordSteps } from '@/lib/types/resetPasswordSteps'; @@ -7,13 +7,22 @@ import { useCallback, useState, useEffect } from 'react'; import { useSearchParams } from 'next/navigation'; import { Auth } from 'aws-amplify'; import PageNotFound from '@/app/not-found'; -import { translations } from '@/_contents/translations'; -import { snackbarAutoHideDurationMs } from '@/config'; +import { useTranslations } from 'next-intl'; +import Spinner from '@/components/atoms/Spinner/Spinner'; +import PageBackgroundWrapper from '@/components/atoms/PageBackgroundWrapper/PageBackgroundWrapper'; +import SingleCard from '@/components/atoms/SingleCard/SingleCard'; +import { IllusError } from '@pagopa/mui-italia/dist/illustrations/Error'; + +enum State { + loading = 'loading', + error = 'error', + errorLink = 'errorLink', + success = 'success', +} const ChangePassword = () => { - const { - auth: { resetPassword }, - } = translations; + const confirmation = useTranslations('auth.confirmation'); + const searchParams = useSearchParams(); const username = searchParams.get('username') || ''; const code = searchParams.get('code') || ''; @@ -22,16 +31,15 @@ const ChangePassword = () => { ResetPasswordSteps.CHANGE_PASSWORD ); const [password, setPassword] = useState(''); - const [error, setError] = useState(null); - const [isValidLink, setIsValidLink] = useState(false); + const [state, setState] = useState(State.loading); const onChangePassword = useCallback(async () => { const success = await Auth.forgotPasswordSubmit( username, code, password - ).catch((err) => { - setError(err.message); + ).catch(() => { + setState(State.errorLink); return false; }); @@ -42,28 +50,24 @@ const ChangePassword = () => { useEffect(() => { if (username != '' && code != '') { - setIsValidLink(true); + setState(State.success); } else { - setError(resetPassword.invalidLinkError); + setState(State.errorLink); } }, []); - return ( - <> - {isValidLink ? ( - + switch (state) { + case State.error: + return ; + case State.errorLink: + return ( + + } title={confirmation('title')} /> + + ); + case State.success: + return ( + { )} - - ) : ( - - )} - setError(null)} - > - {error} - - - ); + + ); + default: + return ; + } }; export default ChangePassword; diff --git a/apps/nextjs-website/src/app/auth/confirmation/page.tsx b/apps/nextjs-website/src/app/auth/confirmation/page.tsx index 68aa193f35..6ff51ded5b 100644 --- a/apps/nextjs-website/src/app/auth/confirmation/page.tsx +++ b/apps/nextjs-website/src/app/auth/confirmation/page.tsx @@ -10,10 +10,12 @@ import PageBackgroundWrapper from '@/components/atoms/PageBackgroundWrapper/Page import SingleCard from '@/components/atoms/SingleCard/SingleCard'; import { isProduction } from '@/config'; import { IllusError } from '@pagopa/mui-italia'; +import AccountAlreadyConfirmed from '@/components/organisms/Auth/AccountAlreadyConfirmed'; enum State { loading = 'loading', resendCode = 'resendCode', + alreadyConfirmed = 'alreadyConfirmed', error = 'error', } @@ -38,7 +40,13 @@ const Confirmation = () => { // TODO: remove console warn and handle errors: [CodeMismatchException, ExpiredCodeException, InternalErrorException, LimitExceededException] // see apps/nextjs-website/src/app/auth/email-confirmation/page.tsx !isProduction && console.warn(error); - setState(State.resendCode); + switch (error.code) { + case 'LimitExceededException': + setState(State.error); + break; + default: + setState(State.resendCode); + } }); } else { setState(State.error); @@ -63,6 +71,8 @@ const Confirmation = () => { switch (state) { case State.error: return ; + case State.alreadyConfirmed: + return ; case State.resendCode: return ( diff --git a/apps/nextjs-website/src/components/organisms/Auth/AccountAlreadyConfirmed.tsx b/apps/nextjs-website/src/components/organisms/Auth/AccountAlreadyConfirmed.tsx new file mode 100644 index 0000000000..8b55e4a33f --- /dev/null +++ b/apps/nextjs-website/src/components/organisms/Auth/AccountAlreadyConfirmed.tsx @@ -0,0 +1,31 @@ +import PageBackgroundWrapper from '@/components/atoms/PageBackgroundWrapper/PageBackgroundWrapper'; +import SingleCard from '@/components/atoms/SingleCard/SingleCard'; +import { Button } from '@mui/material'; +import { IllusError } from '@pagopa/mui-italia'; +import { useTranslations } from 'next-intl'; +import Link from 'next/link'; + +const AccountAlreadyConfirmed = () => { + const accountAlreadyConfirmed = useTranslations( + 'auth.login.accountAlreadyConfirmed' + ); + return ( + + } + title={accountAlreadyConfirmed('yourAccountIsAlreadyConfirmed')} + cta={ + + } + /> + + ); +}; +export default AccountAlreadyConfirmed; diff --git a/apps/nextjs-website/src/components/organisms/Auth/ChangePasswordForm.tsx b/apps/nextjs-website/src/components/organisms/Auth/ChangePasswordForm.tsx index 2778e7001c..6d50470ed9 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ChangePasswordForm.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ChangePasswordForm.tsx @@ -10,23 +10,22 @@ import { Grid, IconButton, InputAdornment, - InputLabel, Link, - OutlinedInput, Stack, + TextField, Typography, + useTheme, } from '@mui/material'; -import { translations } from '@/_contents/translations'; import { - useState, - MouseEvent, - useCallback, - useEffect, Dispatch, + MouseEvent, SetStateAction, + useCallback, + useState, } from 'react'; import { Visibility, VisibilityOff } from '@mui/icons-material'; -import { passwordMatcher } from '@/helpers/auth.helpers'; +import { validatePassword } from '@/helpers/auth.helpers'; +import { useTranslations } from 'next-intl'; interface ChangePasswordFormProps { onChangePassword: () => Promise; @@ -34,23 +33,31 @@ interface ChangePasswordFormProps { password: string; } +interface ChangePasswordFieldsError { + passwordError: boolean | null; + confirmPasswordError: string | null; +} + const ChangePasswordForm = ({ onChangePassword, setPassword, password, }: ChangePasswordFormProps) => { - const { - shared, - auth: { login, signUp, resetPassword }, - } = translations; + const login = useTranslations('auth.login'); + const resetPassword = useTranslations('auth.resetPassword'); + const signUp = useTranslations('auth.signUp'); + const shared = useTranslations('shared'); const [showPassword, setShowPassword] = useState(false); - const [isPasswordDirty, setIsPasswordDirty] = useState(false); - const [isPasswordValid, setIsPasswordValid] = useState(false); - const [isFormValid, setIsFormValid] = useState(false); - const [isSubmitDisabled, setSubmitDisabled] = useState(false); - const [password_confirm, setPasswordConfirm] = useState(''); + const [passwordConfirm, setPasswordConfirm] = useState(''); + + const { palette } = useTheme(); + + const [fieldErrors, setFieldErrors] = useState({ + passwordError: null, + confirmPasswordError: null, + }); const handleClickShowPassword = () => setShowPassword((show) => !show); const handleMouseDownPassword = (event: MouseEvent) => { @@ -63,27 +70,29 @@ const ChangePasswordForm = ({ event.preventDefault(); }; - const validatePassword = useCallback(() => { - setIsPasswordValid(passwordMatcher.test(password)); - }, [password]); + const validateForm = useCallback(() => { + const passwordError = validatePassword(password); + const confirmPasswordHasErrors = password !== passwordConfirm; - useEffect(() => { - validatePassword(); - }, [password, validatePassword]); + setFieldErrors({ + passwordError: passwordError ? true : null, + confirmPasswordError: confirmPasswordHasErrors + ? signUp('passwordMismatchError') + : null, + }); - const validateForm = useCallback(() => { - const areFieldsValid = [password, password_confirm].every( - (value) => value && value.trim().length > 0 - ); - const isPasswordEqual = password === password_confirm; + return !passwordError && !confirmPasswordHasErrors; + }, [password, passwordConfirm, signUp]); + + const onChangePasswordClick = useCallback(() => { + const valid = validateForm(); - setIsFormValid(areFieldsValid && isPasswordEqual && isPasswordValid); - setSubmitDisabled(!isFormValid); - }, [isFormValid, isPasswordValid, password, password_confirm]); + if (!valid) { + return; + } - useEffect(() => { - validateForm(); - }, [validateForm, isPasswordValid]); + onChangePassword(); + }, [onChangePassword, validateForm]); return ( - {resetPassword.newPassword} + {resetPassword('newPassword')} - - - {shared.password} - - label) + p': { + display: 'none', + }, + '& div.MuiFormControl-root:has(>label) + p.Mui-error': { + display: 'block', + }, + '& div.MuiFormControl-root:has(>label.Mui-focused) + p': { + display: 'block', + }, + }} + > + setPassword(value)} - onBlur={() => setIsPasswordDirty(true)} - error={isPasswordDirty && !isPasswordValid} - endAdornment={ - - - {showPassword ? : } - - - } - value={password} - label={shared.password} - inputProps={{ - sx: { - padding: '8.5px 14px', - }, + variant='outlined' + size='small' + error={!!fieldErrors.passwordError} + InputProps={{ + endAdornment: ( + + + {showPassword ? : } + + + ), }} + value={password} + label={shared('password')} /> - - {signUp.passwordPolicy} + + {signUp('passwordPolicy')} - - {shared.confirmPassword} - - setPasswordConfirm(value) } - endAdornment={ - - - {showPassword ? : } - - - } - value={password_confirm} - label={shared.confirmPassword} - error={isPasswordDirty && password !== password_confirm} - inputProps={{ - sx: { - padding: '8.5px 14px', - }, + InputProps={{ + endAdornment: ( + + + {showPassword ? : } + + + ), }} + value={passwordConfirm} + label={shared('confirmPassword')} + error={!!fieldErrors.confirmPasswordError} + size='small' /> + {fieldErrors.confirmPasswordError && ( + + {signUp('passwordMismatchError')} + + )} @@ -184,12 +199,10 @@ const ChangePasswordForm = ({ @@ -203,9 +216,17 @@ const ChangePasswordForm = ({ flexDirection='row' > - {resetPassword.rememberPassword} + {resetPassword('rememberPassword')} + + + {login('action')} - {login.action} diff --git a/apps/nextjs-website/src/components/organisms/Auth/PasswordChangedCard.tsx b/apps/nextjs-website/src/components/organisms/Auth/PasswordChangedCard.tsx index 92358ef544..06c762ce5b 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/PasswordChangedCard.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/PasswordChangedCard.tsx @@ -1,40 +1,22 @@ -import { translations } from '@/_contents/translations'; import IconFireworks from '@/components/atoms/IconFireworks/IconFireworks'; -import { Box, Typography, Stack, Grid, Button, Card } from '@mui/material'; +import { Button } from '@mui/material'; +import { useTranslations } from 'next-intl'; import Link from 'next/link'; +import SingleCard from '@/components/atoms/SingleCard/SingleCard'; const PasswordChangedCard = () => { - const { - auth: { resetPassword }, - } = translations; + const resetPassword = useTranslations('auth.resetPassword'); - // TODO: refactor using SingleCard component return ( - - - - - - - - - {resetPassword.passwordSet} - - - - - - - - - - + } + title={resetPassword('passwordSet')} + cta={ + + } + /> ); }; diff --git a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx index 4e3ed499a7..d377590fc9 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx @@ -108,7 +108,7 @@ const ResetPasswordForm = ({ href='/auth/login' sx={{ fontWeight: 600, cursor: 'pointer' }} > - {resetPassword('goBackToLogin')} + {resetPassword('goToLogin')} diff --git a/apps/nextjs-website/src/messages/it.json b/apps/nextjs-website/src/messages/it.json index a3671ddfea..9167cd46b5 100644 --- a/apps/nextjs-website/src/messages/it.json +++ b/apps/nextjs-website/src/messages/it.json @@ -221,20 +221,20 @@ "yourAccountIsActive": "Il tuo account è attivo" }, "resetPassword": { - "title": "Recupera password", "body": "Inserisci il tuo indirizzo email e ti invieremo le istruzioni per impostare una nuova password.", - "goBackToLogin": "Torna al login", - "send": "Invia", - "checkEmailTitle": "Controlla l'email", - "checkEmailStart": "Se esiste un account associato a ", "checkEmailEnd": "riceverai una email con un link per impostare una nuova password.", - "resendEmailPrompt": "Non hai ricevuto l'email? Controlla nella posta indesiderata oppure", - "resendEmail": "Reinvia email", - "wrongEmail": "L'indirizzo email è errato?", - "passwordSet": "Password impostata correttamente", + "checkEmailStart": "Se esiste un account associato a ", + "checkEmailTitle": "Controlla l'email", + "goToLogin": "Vai al login", + "invalidLinkError": "Il link non è valido", "newPassword": "Imposta una nuova password", + "passwordSet": "Password impostata correttamente", "rememberPassword": "Ricordi la tua password?", - "invalidLinkError": "Il link non è valido" + "resendEmail": "Reinvia email", + "resendEmailPrompt": "Non hai ricevuto l'email? Controlla nella posta indesiderata oppure", + "send": "Invia", + "title": "Recupera password", + "wrongEmail": "L'indirizzo email è errato?" }, "expiredCode": { "expiredLink": "Il link che hai usato è scaduto", From 7c690e7c879197a43f5e1f464d60c7ded923b748 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Tue, 6 Feb 2024 17:18:42 +0100 Subject: [PATCH 11/28] Move ResentEmail component on bottom of the page --- .../components/organisms/Auth/ConfirmLogin.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/nextjs-website/src/components/organisms/Auth/ConfirmLogin.tsx b/apps/nextjs-website/src/components/organisms/Auth/ConfirmLogin.tsx index 74d6d3dc97..e216060897 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ConfirmLogin.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ConfirmLogin.tsx @@ -100,15 +100,6 @@ const ConfirmLogin = ({ }} /> - {email && ( - - )} From 62d881687f56923b861e64d3e9ccadb65e293f25 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Wed, 14 Feb 2024 15:37:56 +0100 Subject: [PATCH 17/28] Disable submit button on click while asking for password reset --- .../organisms/Auth/ResetPasswordForm.tsx | 15 ++++++++++++--- apps/nextjs-website/src/messages/it.json | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx index 4e3ed499a7..308832f18a 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx @@ -29,6 +29,7 @@ const ResetPasswordForm = ({ }: ResetPasswordFormProps) => { const resetPassword = useTranslations('auth.resetPassword'); const shared = useTranslations('shared'); + const [submitting, setSubmitting] = useState(false); const [emailError, setEmailError] = useState(null); @@ -48,8 +49,12 @@ const ResetPasswordForm = ({ return; } - handleResetPassword(); - }, [handleResetPassword, validateForm]); + setSubmitting(true); + + handleResetPassword().finally(() => { + setSubmitting(false); + }); + }, [handleResetPassword, validateForm, submitting]); const { palette } = useTheme(); @@ -89,7 +94,11 @@ const ResetPasswordForm = ({ /> - diff --git a/apps/nextjs-website/src/messages/it.json b/apps/nextjs-website/src/messages/it.json index a3671ddfea..50d1aa052d 100644 --- a/apps/nextjs-website/src/messages/it.json +++ b/apps/nextjs-website/src/messages/it.json @@ -234,7 +234,8 @@ "passwordSet": "Password impostata correttamente", "newPassword": "Imposta una nuova password", "rememberPassword": "Ricordi la tua password?", - "invalidLinkError": "Il link non è valido" + "invalidLinkError": "Il link non è valido", + "emailFieldError": "Inserisci un indirizzo email valido" }, "expiredCode": { "expiredLink": "Il link che hai usato è scaduto", From c39c8a5116a018e73451c11b30b5e684117b7311 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Wed, 14 Feb 2024 15:53:28 +0100 Subject: [PATCH 18/28] Remove un unnecessary dependency from React Hook useCallback's dependency array --- .../src/components/organisms/Auth/ResetPasswordForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx index 308832f18a..917539425f 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx @@ -54,7 +54,7 @@ const ResetPasswordForm = ({ handleResetPassword().finally(() => { setSubmitting(false); }); - }, [handleResetPassword, validateForm, submitting]); + }, [handleResetPassword, validateForm]); const { palette } = useTheme(); From 49ce7939f4030f70cc96297c1c83843d8fca40f3 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Wed, 14 Feb 2024 16:23:18 +0100 Subject: [PATCH 19/28] Disable submit button on click while changing password --- .../src/components/organisms/Auth/ChangePasswordForm.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/nextjs-website/src/components/organisms/Auth/ChangePasswordForm.tsx b/apps/nextjs-website/src/components/organisms/Auth/ChangePasswordForm.tsx index 6d50470ed9..b4f084793b 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ChangePasswordForm.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ChangePasswordForm.tsx @@ -48,6 +48,8 @@ const ChangePasswordForm = ({ const signUp = useTranslations('auth.signUp'); const shared = useTranslations('shared'); + const [submitting, setSubmitting] = useState(false); + const [showPassword, setShowPassword] = useState(false); const [passwordConfirm, setPasswordConfirm] = useState(''); @@ -91,7 +93,11 @@ const ChangePasswordForm = ({ return; } - onChangePassword(); + setSubmitting(true); + + onChangePassword().finally(() => { + setSubmitting(false); + }); }, [onChangePassword, validateForm]); return ( @@ -201,6 +207,7 @@ const ChangePasswordForm = ({ onClick={() => { onChangePasswordClick(); }} + disabled={submitting} > {resetPassword('send')} From bac7c87dff9df2f11f974bf146ed565fed95a233 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Thu, 15 Feb 2024 12:00:08 +0100 Subject: [PATCH 20/28] Catch missing Cognito error --- apps/nextjs-website/src/app/auth/confirmation/page.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/nextjs-website/src/app/auth/confirmation/page.tsx b/apps/nextjs-website/src/app/auth/confirmation/page.tsx index 6ff51ded5b..c7fb11ef6f 100644 --- a/apps/nextjs-website/src/app/auth/confirmation/page.tsx +++ b/apps/nextjs-website/src/app/auth/confirmation/page.tsx @@ -41,6 +41,9 @@ const Confirmation = () => { // see apps/nextjs-website/src/app/auth/email-confirmation/page.tsx !isProduction && console.warn(error); switch (error.code) { + case 'AliasExistsException': + setState(State.alreadyConfirmed); + break; case 'LimitExceededException': setState(State.error); break; From d57a9c750b59d858fd8196260429bafb0330d9e7 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Thu, 15 Feb 2024 13:24:32 +0100 Subject: [PATCH 21/28] Remove ghost translations --- apps/nextjs-website/src/_contents/translations.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/apps/nextjs-website/src/_contents/translations.ts b/apps/nextjs-website/src/_contents/translations.ts index f5070ece99..6311084949 100644 --- a/apps/nextjs-website/src/_contents/translations.ts +++ b/apps/nextjs-website/src/_contents/translations.ts @@ -309,19 +309,4 @@ export const translations = { }, }, }, - auth: { - signUp: { - companyRoles: [ - { title: 'Ente pubblico', value: 'public-authority' }, - { title: 'Partner tecnologico', value: 'tech-partner' }, - { title: 'PSP', value: 'psp' }, - { - title: 'Gestore di pubblico servizio', - value: 'operator-of-public-service', - }, - { title: 'Azienda privata', value: 'private-company' }, - { title: 'Altro', value: 'other' }, - ], - }, - }, }; From cfe1f5ec54dc1f0c31cbad1b00a854d3aa92a0e5 Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Thu, 15 Feb 2024 13:28:08 +0100 Subject: [PATCH 22/28] Revert "Remove ghost translations" This reverts commit d57a9c750b59d858fd8196260429bafb0330d9e7. --- apps/nextjs-website/src/_contents/translations.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/nextjs-website/src/_contents/translations.ts b/apps/nextjs-website/src/_contents/translations.ts index 6311084949..f5070ece99 100644 --- a/apps/nextjs-website/src/_contents/translations.ts +++ b/apps/nextjs-website/src/_contents/translations.ts @@ -309,4 +309,19 @@ export const translations = { }, }, }, + auth: { + signUp: { + companyRoles: [ + { title: 'Ente pubblico', value: 'public-authority' }, + { title: 'Partner tecnologico', value: 'tech-partner' }, + { title: 'PSP', value: 'psp' }, + { + title: 'Gestore di pubblico servizio', + value: 'operator-of-public-service', + }, + { title: 'Azienda privata', value: 'private-company' }, + { title: 'Altro', value: 'other' }, + ], + }, + }, }; From 01c243dd734b11ec8ba6966e4ae7ddfd043d5077 Mon Sep 17 00:00:00 2001 From: Marco Ponchia Date: Thu, 15 Feb 2024 14:25:32 +0100 Subject: [PATCH 23/28] Update apps/nextjs-website/src/config.ts --- apps/nextjs-website/src/config.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/apps/nextjs-website/src/config.ts b/apps/nextjs-website/src/config.ts index cc86f38195..fa7cd5f4b9 100644 --- a/apps/nextjs-website/src/config.ts +++ b/apps/nextjs-website/src/config.ts @@ -41,27 +41,6 @@ export const defaultOgTagImage = `${baseUrl}/images/dev-portal-home.jpg`; export const resetResendEmailAfterMs = 4_000; export const fetchWebinarsQuestionsIntervalMs = 2_500; -export const companyRoles = [ - 'ente-pubblico', - 'partner-tecnologico', - 'psp', - 'gestore-di-pubblico-servizio', - 'azienda-privata', - 'altro', -]; - -export const signUpAdvantages = [ - 'exclusive_contents', - 'product_updates', - 'api_keys', - 'support', -]; - -export const webinarQuestionConfig = { - url: process.env.NEXT_PUBLIC_WEBINAR_QUESTION_URL, - resource: process.env.NEXT_PUBLIC_WEBINAR_QUESTION_SHEET_NAME, -}; - export const defaultLanguage = { id: 'it', value: 'Italiano' }; export const languages = [defaultLanguage]; From ac527873c078be934fed19b194255249f2cfd5f3 Mon Sep 17 00:00:00 2001 From: Marco Ponchia Date: Thu, 15 Feb 2024 14:26:31 +0100 Subject: [PATCH 24/28] Delete .changeset/lazy-poets-melt.md remove changeset of previus PR --- .changeset/lazy-poets-melt.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/lazy-poets-melt.md diff --git a/.changeset/lazy-poets-melt.md b/.changeset/lazy-poets-melt.md deleted file mode 100644 index 8b4151999f..0000000000 --- a/.changeset/lazy-poets-melt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"nextjs-website": patch ---- - -Fix sign-up and confirm sign-up UI and UX From 86c100d4763376442655a8a9490c24227c823ca5 Mon Sep 17 00:00:00 2001 From: Marco Ponchia Date: Thu, 15 Feb 2024 15:15:38 +0100 Subject: [PATCH 25/28] Revert missing updated label after merge --- .../src/components/organisms/Auth/ResetPasswordForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx index 917539425f..3270d88914 100644 --- a/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx +++ b/apps/nextjs-website/src/components/organisms/Auth/ResetPasswordForm.tsx @@ -117,7 +117,7 @@ const ResetPasswordForm = ({ href='/auth/login' sx={{ fontWeight: 600, cursor: 'pointer' }} > - {resetPassword('goBackToLogin')} + {resetPassword('goToLogin')} From f8d85b4040a7d5b01a1ca1a828d287553f70579b Mon Sep 17 00:00:00 2001 From: Marco Bottaro Date: Fri, 16 Feb 2024 10:18:21 +0100 Subject: [PATCH 26/28] Remove useless router replace --- apps/nextjs-website/src/app/auth/password-reset/page.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/nextjs-website/src/app/auth/password-reset/page.tsx b/apps/nextjs-website/src/app/auth/password-reset/page.tsx index e50592d23c..8e6b93a483 100644 --- a/apps/nextjs-website/src/app/auth/password-reset/page.tsx +++ b/apps/nextjs-website/src/app/auth/password-reset/page.tsx @@ -28,9 +28,6 @@ const PasswordReset = () => { }, [email]); const onBackStep = useCallback(() => { - router.replace( - `/auth/password-reset?email=${email}&step=${SendResetPasswordSteps.SEND_EMAIL}` - ); setSendResetPasswordSteps(SendResetPasswordSteps.SEND_EMAIL); return null; }, [router, email]); From 26de4e5151ab370ddd7ee55bc59c8768e1cc498a Mon Sep 17 00:00:00 2001 From: marcobottaro <39835990+marcobottaro@users.noreply.github.com> Date: Fri, 16 Feb 2024 10:24:06 +0100 Subject: [PATCH 27/28] Update apps/nextjs-website/src/messages/it.json Co-authored-by: Marco Comi <9998393+kin0992@users.noreply.github.com> --- apps/nextjs-website/src/messages/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/nextjs-website/src/messages/it.json b/apps/nextjs-website/src/messages/it.json index 21aecd4f79..0f3ecc28f9 100644 --- a/apps/nextjs-website/src/messages/it.json +++ b/apps/nextjs-website/src/messages/it.json @@ -231,7 +231,7 @@ "checkEmailStart": "Se esiste un account associato a ", "checkEmailTitle": "Controlla l'email", "emailFieldError": "Inserisci un indirizzo email valido", - "goToLogin": "Vai al login", + "goToLogin": "Torna al login", "invalidLinkError": "Il link non è valido", "newPassword": "Imposta una nuova password", "passwordSet": "Password impostata correttamente", From e8474b3360bd24775028e61a3437621304c13ec8 Mon Sep 17 00:00:00 2001 From: marcobottaro <39835990+marcobottaro@users.noreply.github.com> Date: Fri, 16 Feb 2024 10:24:17 +0100 Subject: [PATCH 28/28] Update apps/nextjs-website/src/messages/it.json Co-authored-by: Marco Comi <9998393+kin0992@users.noreply.github.com> --- apps/nextjs-website/src/messages/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/nextjs-website/src/messages/it.json b/apps/nextjs-website/src/messages/it.json index 0f3ecc28f9..a6ca099a7f 100644 --- a/apps/nextjs-website/src/messages/it.json +++ b/apps/nextjs-website/src/messages/it.json @@ -230,7 +230,7 @@ "checkEmailEnd": "riceverai una email con un link per impostare una nuova password.", "checkEmailStart": "Se esiste un account associato a ", "checkEmailTitle": "Controlla l'email", - "emailFieldError": "Inserisci un indirizzo email valido", + "emailFieldError": "L’indirizzo email non è valido", "goToLogin": "Torna al login", "invalidLinkError": "Il link non è valido", "newPassword": "Imposta una nuova password",