Alert component code improvement

1. Removed redundant alert component, it now passes alert color as variable
2. Added related translations
3. The missing the alert content for login and register responses will be added once the login flow is clear
4. Made alert component a context provider to be globally accessible

Change-Id: Iab3c822b6296a4060c38c4a2a1fdb2fa937dec52
diff --git a/client/src/components/AlertSnackbar.tsx b/client/src/components/AlertSnackbar.tsx
deleted file mode 100644
index a4781ff..0000000
--- a/client/src/components/AlertSnackbar.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2022 Savoir-faire Linux Inc.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this program.  If not, see
- * <https://www.gnu.org/licenses/>.
- */
-import { Alert, AlertColor, AlertProps, AlertTitle, Snackbar, SnackbarProps } from '@mui/material';
-import { useTranslation } from 'react-i18next';
-
-type AlertSnackbarProps = AlertProps & {
-  severity: AlertColor;
-  open?: boolean;
-  snackBarProps?: Partial<SnackbarProps>;
-};
-
-export function AlertSnackbar({ severity, open, snackBarProps, children, ...alertProps }: AlertSnackbarProps) {
-  const { t } = useTranslation();
-
-  return (
-    <Snackbar
-      open={open}
-      {...snackBarProps}
-      anchorOrigin={{
-        vertical: 'top',
-        horizontal: 'center',
-        ...snackBarProps?.anchorOrigin,
-      }}
-    >
-      <Alert severity={severity} {...alertProps}>
-        <AlertTitle>{t('severity', { context: `${severity}` })}</AlertTitle>
-        {children}
-      </Alert>
-    </Snackbar>
-  );
-}
diff --git a/client/src/components/Input.tsx b/client/src/components/Input.tsx
index ddc09af..20da5e7 100644
--- a/client/src/components/Input.tsx
+++ b/client/src/components/Input.tsx
@@ -156,6 +156,7 @@
       <InfosDialog {...dialogHandler.props} title={t('password_rules_dialog_title')} content={<PasswordRules />} />
       <TextField
         required
+        error={passwordError}
         label={t('password_input_label')}
         type={showPassword ? 'text' : 'password'}
         variant="standard"
diff --git a/client/src/contexts/AlertSnackbarProvider.tsx b/client/src/contexts/AlertSnackbarProvider.tsx
new file mode 100644
index 0000000..e5da7be
--- /dev/null
+++ b/client/src/contexts/AlertSnackbarProvider.tsx
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2022 Savoir-faire Linux Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see
+ * <https://www.gnu.org/licenses/>.
+ */
+import { Alert, AlertColor, AlertProps, AlertTitle, Snackbar, SnackbarProps } from '@mui/material';
+import { createContext, useCallback, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { SetState, WithChildren } from '../utils/utils';
+type AlertSnackbarProps = AlertProps & {
+  severity: AlertColor;
+  open?: boolean;
+  snackBarProps?: Partial<SnackbarProps>;
+};
+
+export type AlertContent = {
+  messageI18nKey: AlertMessageKeys;
+  messageI18nContext?: object;
+  severity: AlertColor;
+  alertOpen: boolean;
+};
+
+export function AlertSnackbar({ severity, open, snackBarProps, children, ...alertProps }: AlertSnackbarProps) {
+  const { t } = useTranslation();
+
+  return (
+    <Snackbar
+      open={open}
+      {...snackBarProps}
+      anchorOrigin={{
+        vertical: 'top',
+        horizontal: 'center',
+        ...snackBarProps?.anchorOrigin,
+      }}
+    >
+      <Alert severity={severity} {...alertProps}>
+        <AlertTitle>{t('severity', { context: `${severity}` })}</AlertTitle>
+        {children}
+      </Alert>
+    </Snackbar>
+  );
+}
+
+type IAlertSnackbarContext = {
+  alertContent: AlertContent;
+  setAlertContent: SetState<AlertContent>;
+};
+
+const defaultAlertSnackbarContext: IAlertSnackbarContext = {
+  alertContent: {
+    messageI18nKey: '',
+    messageI18nContext: {},
+    severity: 'info',
+    alertOpen: false,
+  },
+  setAlertContent: () => {},
+};
+
+type AlertMessageKeys =
+  | 'missed_incoming_call'
+  | 'unknown_error_alert'
+  | 'username_input_helper_text_empty'
+  | 'password_input_helper_text_empty'
+  | 'login_invalid_password'
+  | 'registration_success'
+  | '';
+
+export const AlertSnackbarContext = createContext<IAlertSnackbarContext>(defaultAlertSnackbarContext);
+
+const AlertSnackbarProvider = ({ children }: WithChildren) => {
+  const { t } = useTranslation();
+  const [alertContent, setAlertContent] = useState<AlertContent>(defaultAlertSnackbarContext.alertContent);
+  const closeAlert = () => {
+    setAlertContent((prev) => {
+      return {
+        ...prev,
+        alertOpen: false,
+      };
+    });
+  };
+
+  //This is to explicitly let i18n know that these keys should be extracted
+  const getAlertMessageText = useCallback(
+    (messageI18nKey: AlertMessageKeys, messageI18nContext?: object): string => {
+      switch (messageI18nKey) {
+        case 'missed_incoming_call':
+          return t('missed_incoming_call', { ...messageI18nContext });
+        case 'unknown_error_alert':
+          return t('unknown_error_alert');
+        case 'username_input_helper_text_empty':
+          return t('username_input_helper_text_empty');
+        case 'password_input_helper_text_empty':
+          return t('password_input_helper_text_empty');
+        case 'login_invalid_password':
+          return t('login_invalid_password');
+        case 'registration_success':
+          return t('registration_success');
+        default:
+          return t('unknown_error_alert');
+      }
+    },
+    [t]
+  );
+
+  const value = {
+    alertContent,
+    setAlertContent,
+  };
+
+  return (
+    <>
+      <AlertSnackbar severity={alertContent.severity} open={alertContent.alertOpen} onClose={closeAlert}>
+        {getAlertMessageText(alertContent.messageI18nKey, alertContent.messageI18nContext)}
+      </AlertSnackbar>
+      <AlertSnackbarContext.Provider value={value}>{children}</AlertSnackbarContext.Provider>
+    </>
+  );
+};
+
+export default AlertSnackbarProvider;
diff --git a/client/src/contexts/CallManagerProvider.tsx b/client/src/contexts/CallManagerProvider.tsx
index b99df69..b9fbb0b 100644
--- a/client/src/contexts/CallManagerProvider.tsx
+++ b/client/src/contexts/CallManagerProvider.tsx
@@ -20,13 +20,13 @@
 import { useTranslation } from 'react-i18next';
 import { useNavigate } from 'react-router-dom';
 
-import { AlertSnackbar } from '../components/AlertSnackbar';
 import { RemoteVideoOverlay } from '../components/VideoOverlay';
 import { useUrlParams } from '../hooks/useUrlParams';
 import { ConversationMember } from '../models/conversation-member';
 import { ConversationRouteParams } from '../router';
 import { useConversationInfosQuery, useMembersQuery } from '../services/conversationQueries';
 import { SetState, WithChildren } from '../utils/utils';
+import { AlertSnackbarContext } from './AlertSnackbarProvider';
 import CallProvider, { CallRole } from './CallProvider';
 import WebRtcProvider from './WebRtcProvider';
 import { WebSocketContext } from './WebSocketProvider';
@@ -59,11 +59,11 @@
 export default ({ children }: WithChildren) => {
   const [callData, setCallData] = useState<CallData>();
   const webSocket = useContext(WebSocketContext);
+  const { setAlertContent } = useContext(AlertSnackbarContext);
   const navigate = useNavigate();
   const { data: conversationInfos } = useConversationInfosQuery(callData?.conversationId);
   const { data: members } = useMembersQuery(callData?.conversationId);
   const { urlParams } = useUrlParams<ConversationRouteParams>();
-  const [missedCallConversationId, setMissedCallConversationId] = useState<string>();
   const { t } = useTranslation();
 
   const failStartCall = useCallback(() => {
@@ -90,7 +90,12 @@
       if (callData) {
         // TODO: Currently, we display a notification if already in a call.
         //       In the future, we should handle receiving a call while already in another.
-        setMissedCallConversationId(conversationId);
+        setAlertContent({
+          messageI18nKey: 'missed_incoming_call',
+          messageI18nContext: { conversationId },
+          severity: 'info',
+          alertOpen: true,
+        });
         return;
       }
 
@@ -103,7 +108,7 @@
     return () => {
       webSocket.unbind(WebSocketMessageType.CallBegin, callBeginListener);
     };
-  }, [webSocket, navigate, startCall, callData]);
+  }, [webSocket, navigate, startCall, callData, setAlertContent, t]);
 
   const value = useMemo(
     () => ({
@@ -118,13 +123,6 @@
 
   return (
     <>
-      <AlertSnackbar
-        severity={'info'}
-        open={missedCallConversationId !== undefined}
-        onClose={() => setMissedCallConversationId(undefined)}
-      >
-        {t('missed_incoming_call', { conversationId: missedCallConversationId })}
-      </AlertSnackbar>
       <CallManagerContext.Provider value={value}>
         <WebRtcProvider>
           <CallProvider>
diff --git a/client/src/index.tsx b/client/src/index.tsx
index f5cd876..03e43a9 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -25,6 +25,7 @@
 import { Provider } from 'react-redux';
 import { RouterProvider } from 'react-router-dom';
 
+import AlertSnackbarProvider from './contexts/AlertSnackbarProvider';
 import CustomThemeProvider from './contexts/CustomThemeProvider';
 import { store } from './redux/store';
 import { router } from './router';
@@ -48,8 +49,10 @@
     {/* <StrictMode> */}
     <QueryClientProvider client={queryClient}>
       <CustomThemeProvider>
-        <RouterProvider router={router} />
-        <ReactQueryDevtools initialIsOpen={false} />
+        <AlertSnackbarProvider>
+          <RouterProvider router={router} />
+          <ReactQueryDevtools initialIsOpen={false} />
+        </AlertSnackbarProvider>
       </CustomThemeProvider>
     </QueryClientProvider>
     {/* </StrictMode> */}
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index 3a8ffaf..25eff58 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -96,6 +96,7 @@
   "ongoing_call_unmuted": "Ongoing call",
   "outgoing_call": "Outgoing call",
   "password_input_helper_text": "",
+  "password_input_helper_text_empty": "Password is missing",
   "password_input_helper_text_medium": "Medium",
   "password_input_helper_text_registration_failed": "Choose another password!",
   "password_input_helper_text_strong": "Strong",
@@ -144,6 +145,7 @@
   "share_window": "Share window",
   "unknown_error_alert": "Something went wrong, please try again",
   "username_input_helper_text": "",
+  "username_input_helper_text_empty": "Username is missing",
   "username_input_helper_text_invalid": "Username doesn't follow required pattern",
   "username_input_helper_text_registration_failed": "Username not correct!",
   "username_input_helper_text_success": "Username available",
diff --git a/client/src/locale/fr/translation.json b/client/src/locale/fr/translation.json
index c92e201..acc3752 100644
--- a/client/src/locale/fr/translation.json
+++ b/client/src/locale/fr/translation.json
@@ -96,6 +96,7 @@
   "ongoing_call_unmuted": "Appel en cours",
   "outgoing_call": "Appel sortant",
   "password_input_helper_text": "",
+  "password_input_helper_text_empty": "Mot de passe manquant",
   "password_input_helper_text_medium": "Moyen",
   "password_input_helper_text_registration_failed": "Choisissez un autre mot de passe!",
   "password_input_helper_text_strong": "Fort",
@@ -144,6 +145,7 @@
   "share_window": "Partager la fenêtre",
   "unknown_error_alert": "Une erreur s'est produite. Veuillez réessayer",
   "username_input_helper_text": "",
+  "username_input_helper_text_empty": "Nom d'utilisateur manquant",
   "username_input_helper_text_invalid": "Le nom d'utilisateur ne suit pas le modèle",
   "username_input_helper_text_registration_failed": "Nom d'utilisateur incorrect!",
   "username_input_helper_text_success": "Nom d'utilisateur disponible",
diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx
index f0cd005..f865bb8 100644
--- a/client/src/pages/Login.tsx
+++ b/client/src/pages/Login.tsx
@@ -28,14 +28,14 @@
 } from '@mui/material';
 import { Theme, useTheme } from '@mui/material/styles';
 import { HttpStatusCode } from 'jami-web-common';
-import { ChangeEvent, FormEvent, ReactNode, useState } from 'react';
+import { ChangeEvent, FormEvent, useContext, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { Form, Link, useNavigate } from 'react-router-dom';
 
-import { AlertSnackbar } from '../components/AlertSnackbar';
 import { PasswordInput, UsernameInput } from '../components/Input';
 import ProcessingRequest from '../components/ProcessingRequest';
 import withAuthUI from '../components/WithAuthUI';
+import { AlertSnackbarContext } from '../contexts/AlertSnackbarProvider';
 import { loginUser, setAccessToken } from '../utils/auth';
 import { inputWidth } from '../utils/constants';
 function LoginForm() {
@@ -47,7 +47,9 @@
   const [password, setPassword] = useState<string>('');
   const [isJams, setIsJams] = useState<boolean>(false);
   const [loading, setLoading] = useState<boolean>(false);
-  const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
+
+  const { setAlertContent } = useContext(AlertSnackbarContext);
+
   const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
 
   const handleUsername = (event: ChangeEvent<HTMLInputElement>) => {
@@ -62,12 +64,16 @@
     setIsJams(event.target.value === 'true');
   };
 
-  const login = async (event: FormEvent) => {
+  const login = (event: FormEvent) => {
     event.preventDefault();
 
-    if (!(username.length > 0) || !(password.length > 0)) {
-      //TODO: set alert message
-      console.log('Empty Credentials');
+    if (username === '') {
+      setAlertContent({ messageI18nKey: 'username_input_helper_text_empty', severity: 'error', alertOpen: true });
+      return;
+    }
+
+    if (password === '') {
+      setAlertContent({ messageI18nKey: 'password_input_helper_text_empty', severity: 'error', alertOpen: true });
       return;
     }
 
@@ -92,9 +98,9 @@
           console.log(e.response.data);
           // setErrorAlertContent(t('login_username_not_found'));
         } else if (status === HttpStatusCode.Unauthorized) {
-          setErrorAlertContent(t('login_invalid_password'));
+          setAlertContent({ messageI18nKey: 'login_invalid_password', severity: 'error', alertOpen: true });
         } else {
-          setErrorAlertContent(t('unknown_error_alert'));
+          setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
         }
       })
       .finally(() => {
@@ -106,10 +112,6 @@
     <>
       <ProcessingRequest open={loading} />
 
-      <AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
-        {errorAlertContent}
-      </AlertSnackbar>
-
       <Stack
         sx={{
           minHeight: `${isMobile ? 'auto' : '100%'}`,
diff --git a/client/src/pages/Registration.tsx b/client/src/pages/Registration.tsx
index e85136a..e857f7b 100644
--- a/client/src/pages/Registration.tsx
+++ b/client/src/pages/Registration.tsx
@@ -28,14 +28,14 @@
 } from '@mui/material';
 import { Theme, useTheme } from '@mui/material/styles';
 import { HttpStatusCode } from 'jami-web-common';
-import { ChangeEvent, FormEvent, ReactNode, useEffect, useState } from 'react';
+import { ChangeEvent, FormEvent, useContext, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { Form, Link, useNavigate } from 'react-router-dom';
 
-import { AlertSnackbar } from '../components/AlertSnackbar';
 import { NameStatus, PasswordInput, PasswordStatus, UsernameInput } from '../components/Input';
 import ProcessingRequest from '../components/ProcessingRequest';
 import withAuthUI from '../components/WithAuthUI';
+import { AlertSnackbarContext } from '../contexts/AlertSnackbarProvider';
 import {
   checkIfUserameIsRegistered,
   checkPasswordStrength,
@@ -58,8 +58,7 @@
   const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
   const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
 
-  const [errorAlertContent, setErrorAlertContent] = useState<ReactNode>(undefined);
-  const [successAlertContent, setSuccessAlertContent] = useState<ReactNode>(undefined);
+  const { setAlertContent } = useContext(AlertSnackbarContext);
 
   const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
 
@@ -87,7 +86,7 @@
               // console.log(e.response.data);
               setUsernameStatus('success');
             } else {
-              setErrorAlertContent(t('unknown_error_alert'));
+              setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
             }
           });
       }, 1000);
@@ -96,10 +95,11 @@
     return () => {
       clearTimeout(timetoutId);
     };
-  }, [t, username, usernameStatus]);
+  }, [t, username, usernameStatus, setAlertContent]);
 
-  const login = async () => {
+  const login = () => {
     setLoading(true);
+
     loginUser(username, password, isJams)
       .then((response) => {
         if (response.status === HttpStatusCode.Ok) {
@@ -113,15 +113,15 @@
         if (status === HttpStatusCode.BadRequest) {
           //TODO: the only bad request response defined in the server is missing credentials. add the response message to the locale.
           console.log(e.response.data);
-          setErrorAlertContent(t('unknown_error_alert'));
+          // setAlertContent(t('unknown_error_alert'));
         } else if (status === HttpStatusCode.NotFound) {
           //TODO: there are two different not found responses that could be returned by the server, use message to differentiate them?
           console.log(e.response.data);
-          // setErrorAlertContent(t('login_username_not_found'));
+          // setAlertContent(t('login_username_not_found'));
         } else if (status === HttpStatusCode.Unauthorized) {
-          setErrorAlertContent(t('login_invalid_password'));
+          setAlertContent({ messageI18nKey: 'login_invalid_password', severity: 'error', alertOpen: true });
         } else {
-          setErrorAlertContent(t('unknown_error_alert'));
+          setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
         }
       })
       .finally(() => {
@@ -134,7 +134,7 @@
     registerUser(username, password, isJams)
       .then((response) => {
         if (response.status === HttpStatusCode.Created) {
-          setSuccessAlertContent(t('registration_success'));
+          setAlertContent({ messageI18nKey: 'registration_success', severity: 'success', alertOpen: true });
           login();
         }
       })
@@ -144,15 +144,15 @@
         if (status === HttpStatusCode.BadRequest) {
           //TODO: more than one bad request response defined in the server is missing credentials. add the response message to the locale.
           console.log(e.response.data);
-          // setErrorAlertContent(t('unknown_error_alert'));
+          // setAlertContent(t('unknown_error_alert'));
         } else if (status === HttpStatusCode.Conflict) {
           //TODO: there are two different conflict responses that could be returned by the server, use message to differentiate them?
           console.log(e.response.data);
-          // setErrorAlertContent(t('login_username_not_found'));
+          // setAlertContent(t('login_username_not_found'));
         } else if (status === HttpStatusCode.Unauthorized) {
           //TODO: this is a response for JAMS, add message to the locale
         } else {
-          setErrorAlertContent(t('unknown_error_alert'));
+          setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
         }
       })
       .finally(() => {
@@ -216,18 +216,6 @@
     <>
       <ProcessingRequest open={loading} />
 
-      <AlertSnackbar
-        severity={'success'}
-        open={!!successAlertContent}
-        onClose={() => setSuccessAlertContent(undefined)}
-      >
-        {successAlertContent}
-      </AlertSnackbar>
-
-      <AlertSnackbar severity={'error'} open={!!errorAlertContent} onClose={() => setErrorAlertContent(undefined)}>
-        {errorAlertContent}
-      </AlertSnackbar>
-
       <Stack
         sx={{
           minHeight: `${isMobile ? 'auto' : '100%'}`,