Rewrite login and register related queries using React Query

Change-Id: Ifb462a30b3100043b29aaa3db7c321950937cad1
diff --git a/client/src/App.tsx b/client/src/App.tsx
index ebebd81..ffc98bd 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -22,7 +22,7 @@
 import { json, LoaderFunctionArgs, Outlet, redirect } from 'react-router-dom';
 
 import WelcomeAnimation from './components/WelcomeAnimation';
-import { getAccessToken } from './utils/auth';
+import { getAccessToken } from './services/authQueries';
 import { apiUrl } from './utils/constants';
 
 export async function checkSetupStatus(): Promise<boolean> {
diff --git a/client/src/components/Input.tsx b/client/src/components/Input.tsx
index 20da5e7..b0e041a 100644
--- a/client/src/components/Input.tsx
+++ b/client/src/components/Input.tsx
@@ -22,7 +22,7 @@
 import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
-import { StrengthValueCode } from '../utils/auth';
+import { StrengthValueCode } from '../services/authQueries';
 import { InfoButton, ToggleVisibilityButton } from './Button';
 import { DialogContentList, InfosDialog, useDialogHandler } from './Dialog';
 import { CheckedIcon, LockIcon, PenIcon, PersonIcon, RoundSaltireIcon } from './SvgIcon';
diff --git a/client/src/components/WithAuthUI.tsx b/client/src/components/WithAuthUI.tsx
index db6e692..2168da5 100644
--- a/client/src/components/WithAuthUI.tsx
+++ b/client/src/components/WithAuthUI.tsx
@@ -20,7 +20,7 @@
 import React from 'react';
 import { Navigate } from 'react-router-dom';
 
-import { getAccessToken } from '../utils/auth';
+import { getAccessToken } from '../services/authQueries';
 import JamiWelcomeLogo from './JamiWelcomeLogo';
 
 export default function withAuthUI(WrappedComponent: React.FunctionComponent): React.FunctionComponent {
diff --git a/client/src/contexts/AlertSnackbarProvider.tsx b/client/src/contexts/AlertSnackbarProvider.tsx
index e5da7be..ac93c6b 100644
--- a/client/src/contexts/AlertSnackbarProvider.tsx
+++ b/client/src/contexts/AlertSnackbarProvider.tsx
@@ -57,6 +57,7 @@
 type IAlertSnackbarContext = {
   alertContent: AlertContent;
   setAlertContent: SetState<AlertContent>;
+  closeAlert: () => void;
 };
 
 const defaultAlertSnackbarContext: IAlertSnackbarContext = {
@@ -67,6 +68,7 @@
     alertOpen: false,
   },
   setAlertContent: () => {},
+  closeAlert: () => {},
 };
 
 type AlertMessageKeys =
@@ -118,6 +120,7 @@
   const value = {
     alertContent,
     setAlertContent,
+    closeAlert,
   };
 
   return (
diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx
index f865bb8..9cd2175 100644
--- a/client/src/pages/Login.tsx
+++ b/client/src/pages/Login.tsx
@@ -27,29 +27,29 @@
   useMediaQuery,
 } from '@mui/material';
 import { Theme, useTheme } from '@mui/material/styles';
-import { HttpStatusCode } from 'jami-web-common';
 import { ChangeEvent, FormEvent, useContext, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Form, Link, useNavigate } from 'react-router-dom';
+import { Form, Link } from 'react-router-dom';
 
 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 { useLoginMutation } from '../services/authQueries';
 import { inputWidth } from '../utils/constants';
 function LoginForm() {
   const theme: Theme = useTheme();
-  const navigate = useNavigate();
   const { t } = useTranslation();
 
   const [username, setUsername] = useState<string>('');
   const [password, setPassword] = useState<string>('');
   const [isJams, setIsJams] = useState<boolean>(false);
-  const [loading, setLoading] = useState<boolean>(false);
 
   const { setAlertContent } = useContext(AlertSnackbarContext);
 
+  const loginMutation = useLoginMutation();
+  const { isLoading } = loginMutation;
+
   const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
 
   const handleUsername = (event: ChangeEvent<HTMLInputElement>) => {
@@ -66,51 +66,21 @@
 
   const login = (event: FormEvent) => {
     event.preventDefault();
-
     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;
     }
 
-    setLoading(true);
-
-    loginUser(username, password, isJams)
-      .then((response) => {
-        if (response.status === HttpStatusCode.Ok) {
-          setAccessToken(response.data.accessToken);
-          navigate('/conversation', { replace: true });
-        }
-      })
-      .catch((e) => {
-        console.log(e);
-        const { status } = e.response;
-        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'));
-        } 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'));
-        } else if (status === HttpStatusCode.Unauthorized) {
-          setAlertContent({ messageI18nKey: 'login_invalid_password', severity: 'error', alertOpen: true });
-        } else {
-          setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
-        }
-      })
-      .finally(() => {
-        setLoading(false);
-      });
+    loginMutation.mutate({ username, password, isJams });
   };
 
   return (
     <>
-      <ProcessingRequest open={loading} />
+      <ProcessingRequest open={isLoading} />
 
       <Stack
         sx={{
diff --git a/client/src/pages/Registration.tsx b/client/src/pages/Registration.tsx
index 1b63d33..0955a4a 100644
--- a/client/src/pages/Registration.tsx
+++ b/client/src/pages/Registration.tsx
@@ -15,146 +15,76 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
-import {
-  Box,
-  Button,
-  FormControl,
-  FormControlLabel,
-  Radio,
-  RadioGroup,
-  Stack,
-  Typography,
-  useMediaQuery,
-} from '@mui/material';
+import { Box, Button, Stack, Typography, useMediaQuery } from '@mui/material';
 import { Theme, useTheme } from '@mui/material/styles';
 import { HttpStatusCode } from 'jami-web-common';
-import { ChangeEvent, FormEvent, useContext, useEffect, useState } from 'react';
+import { ChangeEvent, FormEvent, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Form, Link, useNavigate } from 'react-router-dom';
+import { Form, Link } from 'react-router-dom';
 
 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,
-  loginUser,
-  registerUser,
-  setAccessToken,
-} from '../utils/auth';
+  useCheckIfUsernameIsRegisteredQuery,
+  useLoginMutation,
+  useRegisterMutation,
+} from '../services/authQueries';
 import { inputWidth, jamiUsernamePattern } from '../utils/constants';
 function RegistrationForm() {
   const theme: Theme = useTheme();
-  const navigate = useNavigate();
   const { t } = useTranslation();
 
-  const [loading, setLoading] = useState(false);
-
   const [username, setUsername] = useState<string>('');
+  const [debouncedUsername, setDebouncedUsername] = useState<string>('');
   const [password, setPassword] = useState<string>('');
-  const [isJams, setIsJams] = useState<boolean>(false);
 
   const [usernameStatus, setUsernameStatus] = useState<NameStatus>('default');
   const [passwordStatus, setPasswordStatus] = useState<PasswordStatus>('default');
 
-  const { setAlertContent } = useContext(AlertSnackbarContext);
+  const registerMutation = useRegisterMutation();
+  const loginMutation = useLoginMutation();
+
+  const { isLoading: isRegisterLoading } = registerMutation;
+  const { isLoading: isLoginLoading } = loginMutation;
+
+  const isLoading = isRegisterLoading || isLoginLoading;
 
   const isMobile: boolean = useMediaQuery(theme.breakpoints.only('xs'));
 
+  const { data: response, isError } = useCheckIfUsernameIsRegisteredQuery(debouncedUsername);
+
   useEffect(() => {
-    let timetoutId: ReturnType<typeof setTimeout>;
-    if (usernameStatus === 'valid') {
-      timetoutId = setTimeout(() => {
-        // useCheckIfUsernameIsRegisteredMutation.mutate(username);
-        checkIfUserameIsRegistered(username)
-          .then((response) => {
-            const { status, data } = response;
-            if (status === HttpStatusCode.Ok) {
-              if (data === 'taken') {
-                setUsernameStatus('taken');
-              } else {
-                setUsernameStatus('success');
-              }
-            }
-          })
-          .catch((e) => {
-            const { status } = e.response;
-            if (status === HttpStatusCode.BadRequest) {
-              setUsernameStatus('invalid');
-            } else {
-              setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
-            }
-          });
-      }, 1000);
+    if (response !== undefined) {
+      const { responseMessage } = response;
+      setUsernameStatus(responseMessage);
     }
+    if (isError) {
+      setUsernameStatus('invalid');
+    }
+  }, [response, isError]);
 
+  useEffect(() => {
+    const timeoutId = setTimeout(() => {
+      setDebouncedUsername(username);
+    }, 500);
     return () => {
-      clearTimeout(timetoutId);
+      clearTimeout(timeoutId);
     };
-  }, [t, username, usernameStatus, setAlertContent]);
-
-  const login = () => {
-    setLoading(true);
-
-    loginUser(username, password, isJams)
-      .then((response) => {
-        if (response.status === HttpStatusCode.Ok) {
-          setAccessToken(response.data.accessToken);
-          navigate('/conversation', { replace: true });
-        }
-      })
-      .catch((e) => {
-        console.log(e);
-        const { status } = e.response;
-        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);
-          // 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);
-          // setAlertContent(t('login_username_not_found'));
-        } else if (status === HttpStatusCode.Unauthorized) {
-          setAlertContent({ messageI18nKey: 'login_invalid_password', severity: 'error', alertOpen: true });
-        } else {
-          setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
-        }
-      })
-      .finally(() => {
-        setLoading(false);
-      });
-  };
+  }, [username]);
 
   const createAccount = () => {
-    setLoading(true);
-    registerUser(username, password, isJams)
-      .then((response) => {
-        if (response.status === HttpStatusCode.Created) {
-          setAlertContent({ messageI18nKey: 'registration_success', severity: 'success', alertOpen: true });
-          login();
-        }
-      })
-      .catch((e) => {
-        console.log(e);
-        const { status } = e.response;
-        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);
-          // 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);
-          // setAlertContent(t('login_username_not_found'));
-        } else if (status === HttpStatusCode.Unauthorized) {
-          //TODO: this is a response for JAMS, add message to the locale
-        } else {
-          setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
-        }
-      })
-      .finally(() => {
-        setLoading(false);
-      });
+    registerMutation.mutate(
+      { username, password },
+      {
+        onSuccess: (response) => {
+          if (response.status === HttpStatusCode.Created) {
+            loginMutation.mutate({ username, password, isJams: false });
+          }
+        },
+      }
+    );
   };
 
   const handleUsername = async (event: ChangeEvent<HTMLInputElement>) => {
@@ -186,10 +116,6 @@
     }
   };
 
-  const handleIsJams = (event: ChangeEvent<HTMLInputElement>) => {
-    setIsJams(event.target.value === 'jams');
-  };
-
   const handleSubmit = async (event: FormEvent) => {
     event.preventDefault();
     const usernameOk = usernameStatus === 'success';
@@ -211,7 +137,7 @@
 
   return (
     <>
-      <ProcessingRequest open={loading} />
+      <ProcessingRequest open={isLoading} />
 
       <Stack
         sx={{
@@ -246,20 +172,6 @@
               sx={{ width: theme.typography.pxToRem(inputWidth) }}
             />
           </div>
-          <div>
-            <FormControl
-              sx={{
-                width: theme.typography.pxToRem(inputWidth),
-                alignItems: 'center',
-                justifyContent: 'space-between',
-              }}
-            >
-              <RadioGroup row onChange={handleIsJams} defaultValue="jami">
-                <FormControlLabel value="jami" control={<Radio />} label={t('jami')} />
-                <FormControlLabel value="jams" control={<Radio />} label={t('jams')} />
-              </RadioGroup>
-            </FormControl>
-          </div>
 
           <Button
             variant="contained"
diff --git a/client/src/services/authQueries.ts b/client/src/services/authQueries.ts
new file mode 100644
index 0000000..45f78dd
--- /dev/null
+++ b/client/src/services/authQueries.ts
@@ -0,0 +1,157 @@
+/*
+ * 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 { useMutation, useQuery } from '@tanstack/react-query';
+import axios from 'axios';
+import { passwordStrength } from 'check-password-strength';
+import { AccessToken } from 'jami-web-common';
+import { HttpStatusCode } from 'jami-web-common';
+import { useContext } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+import { NameStatus } from '../components/Input';
+import { AlertSnackbarContext } from '../contexts/AlertSnackbarProvider';
+import { PasswordStrength } from '../enums/passwordStrength';
+import { apiUrl } from '../utils/constants';
+import { jamiUsernamePattern } from '../utils/constants';
+interface PasswordStrengthResult {
+  id: number;
+  value: string;
+  contains: string[];
+  length: number;
+}
+
+interface LoginData {
+  username: string;
+  password: string;
+  isJams: boolean;
+}
+
+interface RegisterData {
+  username: string;
+  password: string;
+}
+
+export interface PasswordCheckResult {
+  strong: boolean;
+  valueCode: StrengthValueCode;
+}
+
+export type StrengthValueCode = 'default' | 'too_weak' | 'weak' | 'medium' | 'strong';
+export type LoginMethod = 'Jami' | 'JAMS';
+
+const idToStrengthValueCode: StrengthValueCode[] = ['too_weak', 'weak', 'medium', 'strong'];
+
+export const useCheckIfUsernameIsRegisteredQuery = (username: string) => {
+  return useQuery({
+    queryKey: ['username', username],
+    queryFn: async () => {
+      const res = await axios.get<NameStatus>(`/ns/username/availability/${username}`, {
+        baseURL: apiUrl,
+      });
+      return { responseMessage: res.data, statusCode: res.status };
+    },
+    enabled: jamiUsernamePattern.test(username),
+  });
+};
+
+export function checkPasswordStrength(password: string): PasswordCheckResult {
+  const strengthResult: PasswordStrengthResult = passwordStrength(password);
+
+  return {
+    strong: strengthResult.id === PasswordStrength.Strong.valueOf(),
+    valueCode: idToStrengthValueCode[strengthResult.id] ?? 'default',
+  };
+}
+
+export const useRegisterMutation = () => {
+  const { setAlertContent } = useContext(AlertSnackbarContext);
+  return useMutation({
+    mutationKey: ['user', 'register'],
+    mutationFn: async (registerData: RegisterData) => {
+      const response = await axios.post(
+        '/auth/new-account',
+        { username: registerData.username, password: registerData.password },
+        { baseURL: apiUrl }
+      );
+      return response;
+    },
+    onSuccess: (response) => {
+      if (response.status === HttpStatusCode.Created) {
+        setAlertContent({ messageI18nKey: 'registration_success', severity: 'success', alertOpen: true });
+      }
+    },
+    onError: (e: any) => {
+      const { status } = e.response;
+      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.
+      } else if (status === HttpStatusCode.Conflict) {
+        //TODO: there are two different conflict responses that could be returned by the server, use message to differentiate them?
+      } else if (status === HttpStatusCode.Unauthorized) {
+        //TODO: this is a response for JAMS, add message to the locale
+      } else {
+        setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
+      }
+    },
+  });
+};
+
+export const useLoginMutation = () => {
+  const navigate = useNavigate();
+  const { setAlertContent, closeAlert } = useContext(AlertSnackbarContext);
+  return useMutation({
+    mutationKey: ['user', 'login'],
+    mutationFn: async (loginData: LoginData) => {
+      const { data } = await axios.post<AccessToken>(
+        '/auth/login',
+        { username: loginData.username, password: loginData.password, isJams: loginData.isJams },
+        { baseURL: apiUrl }
+      );
+      return data;
+    },
+    onMutate: () => {
+      closeAlert();
+    },
+    onSuccess: (response) => {
+      setAccessToken(response.accessToken);
+      navigate('/conversation', { replace: true });
+    },
+    onError: (e: any) => {
+      //e: any is a simple workaround for type check since the error is of type unknown and we can't reference a property on an unknown
+      const { status } = e.response;
+      if (status === HttpStatusCode.BadRequest) {
+        //TODO: the only bad request response defined in the server is missing credentials. add the response message to the locale.
+        //continue when the auth flow is clear
+      } 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?
+        //continue when the auth flow is clear
+      } else if (status === HttpStatusCode.Unauthorized) {
+        setAlertContent({ messageI18nKey: 'login_invalid_password', severity: 'error', alertOpen: true });
+      } else {
+        setAlertContent({ messageI18nKey: 'unknown_error_alert', severity: 'error', alertOpen: true });
+      }
+    },
+  });
+};
+
+export function getAccessToken(): string | undefined {
+  return localStorage.getItem('accessToken') ?? undefined;
+}
+
+export function setAccessToken(accessToken: string): void {
+  localStorage.setItem('accessToken', accessToken);
+}
diff --git a/client/src/utils/auth.ts b/client/src/utils/auth.ts
deleted file mode 100644
index 7336639..0000000
--- a/client/src/utils/auth.ts
+++ /dev/null
@@ -1,69 +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 axios from 'axios';
-import { passwordStrength } from 'check-password-strength';
-import { AccessToken } from 'jami-web-common';
-
-import { PasswordStrength } from '../enums/passwordStrength';
-import { apiUrl } from './constants';
-
-interface PasswordStrengthResult {
-  id: number;
-  value: string;
-  contains: string[];
-  length: number;
-}
-
-export interface PasswordCheckResult {
-  strong: boolean;
-  valueCode: StrengthValueCode;
-}
-
-export type StrengthValueCode = 'default' | 'too_weak' | 'weak' | 'medium' | 'strong';
-export type LoginMethod = 'Jami' | 'JAMS';
-
-const idToStrengthValueCode: StrengthValueCode[] = ['too_weak', 'weak', 'medium', 'strong'];
-
-export function checkIfUserameIsRegistered(username: string) {
-  return axios.get(`/ns/username/availability/${username}`, { baseURL: apiUrl });
-}
-
-export function checkPasswordStrength(password: string): PasswordCheckResult {
-  const strengthResult: PasswordStrengthResult = passwordStrength(password);
-
-  return {
-    strong: strengthResult.id === PasswordStrength.Strong.valueOf(),
-    valueCode: idToStrengthValueCode[strengthResult.id] ?? 'default',
-  };
-}
-
-export async function registerUser(username: string, password: string, isJams: boolean) {
-  return axios.post('/auth/new-account', { username, password, isJams }, { baseURL: apiUrl });
-}
-
-export async function loginUser(username: string, password: string, isJams: boolean) {
-  return axios.post<AccessToken>('/auth/login', { username, password, isJams }, { baseURL: apiUrl });
-}
-
-export function getAccessToken(): string | undefined {
-  return localStorage.getItem('accessToken') ?? undefined;
-}
-
-export function setAccessToken(accessToken: string): void {
-  localStorage.setItem('accessToken', accessToken);
-}
diff --git a/client/src/utils/utils.ts b/client/src/utils/utils.ts
index 53b8a02..d432c56 100644
--- a/client/src/utils/utils.ts
+++ b/client/src/utils/utils.ts
@@ -45,3 +45,13 @@
 };
 
 export type Listener = () => void;
+
+export function debounce<T extends any[]>(fn: (...args: T) => void, timeout = 1000) {
+  let timeoutId: ReturnType<typeof setTimeout>;
+  return (...args: T) => {
+    clearTimeout(timeoutId);
+    timeoutId = setTimeout(() => {
+      fn(...args);
+    }, timeout);
+  };
+}