Replace fetch with Axios in client

Replace `.then` with await syntax where possible.

GitLab: #142
Change-Id: I6c132f49f152afa7e20919a1c70c539f2ad54878
diff --git a/client/package.json b/client/package.json
index 2bab75d..52106b2 100644
--- a/client/package.json
+++ b/client/package.json
@@ -55,7 +55,6 @@
     "react-draggable": "^4.4.5",
     "react-dropzone": "^14.2.3",
     "react-emoji-render": "^1.2.4",
-    "react-fetch-hook": "^1.9.5",
     "react-i18next": "^11.18.6",
     "react-modal": "^3.15.1",
     "react-redux": "^8.0.2",
diff --git a/client/src/App.tsx b/client/src/App.tsx
index c202c37..f5c36a7 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -15,6 +15,7 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
+import axios from 'axios';
 import { useState } from 'react';
 import { json, LoaderFunctionArgs, Outlet, redirect } from 'react-router-dom';
 
@@ -22,10 +23,10 @@
 import { apiUrl } from './utils/constants';
 
 export async function checkSetupStatus(): Promise<boolean> {
-  const url = new URL('/setup/check', apiUrl);
-  const response = await fetch(url);
-  const { isSetupComplete } = await response.json();
-  return isSetupComplete;
+  const { data } = await axios.get('/setup/check', {
+    baseURL: apiUrl,
+  });
+  return data.isSetupComplete;
 }
 
 export async function appLoader({ request }: LoaderFunctionArgs) {
diff --git a/client/src/components/AccountPreferences.tsx b/client/src/components/AccountPreferences.tsx
index f0c5d31..2086f14 100644
--- a/client/src/components/AccountPreferences.tsx
+++ b/client/src/components/AccountPreferences.tsx
@@ -39,7 +39,6 @@
 import { useState } from 'react';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { apiUrl } from '../utils/constants';
 import ConversationAvatar from './ConversationAvatar';
 import ConversationsOverviewCard from './ConversationsOverviewCard';
 import JamiIdCard from './JamiIdCard';
@@ -56,18 +55,8 @@
   },
 };
 
-type AccountPreferencesProps = {
-  // TODO: Remove account prop after migration to new server
-  account?: Account;
-};
-
-export default function AccountPreferences({ account: _account }: AccountPreferencesProps) {
-  const authContext = useAuthContext(true);
-  const account = _account ?? authContext?.account;
-  const token = authContext?.token;
-  if (!account || !token) {
-    throw new Error('Account not defined');
-  }
+export default function AccountPreferences() {
+  const { account, axiosInstance } = useAuthContext();
 
   const devices: string[][] = [];
   const accountDevices = account.getDevices();
@@ -82,40 +71,21 @@
 
   const [details, setDetails] = useState(account.getDetails());
 
-  const addModerator = () => {
+  const addModerator = async () => {
     if (defaultModeratorUri) {
-      fetch(new URL(`/default-moderators/${defaultModeratorUri}`, apiUrl), {
-        headers: {
-          Authorization: `Bearer ${token}`,
-        },
-        method: 'PUT',
-      });
+      await axiosInstance.put(`/default-moderators/${defaultModeratorUri}`);
       setDefaultModeratorUri('');
     }
   };
 
-  const removeModerator = (uri: string) =>
-    fetch(new URL(`/default-moderators/${uri}`, apiUrl), {
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-      method: 'DELETE',
-    });
+  const removeModerator = async (uri: string) => await axiosInstance.delete(`/default-moderators/${uri}`);
 
-  const handleToggle = (key: keyof AccountDetails, value: boolean) => {
+  const handleToggle = async (key: keyof AccountDetails, value: boolean) => {
     console.log(`handleToggle ${key} ${value}`);
     const newDetails: Partial<AccountDetails> = {};
     newDetails[key] = value ? 'true' : 'false';
     console.log(newDetails);
-    fetch(new URL('/account', apiUrl), {
-      method: 'PATCH',
-      headers: {
-        Accept: 'application/json',
-        Authorization: `Bearer ${token}`,
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify(newDetails),
-    });
+    await axiosInstance.patch('/account', newDetails);
     setDetails({ ...account.updateDetails(newDetails) });
   };
 
diff --git a/client/src/components/ContactList.jsx b/client/src/components/ContactList.jsx
index 3bc040b..578d3c9 100644
--- a/client/src/components/ContactList.jsx
+++ b/client/src/components/ContactList.jsx
@@ -23,7 +23,6 @@
 
 import { useAuthContext } from '../contexts/AuthProvider';
 import { useAppDispatch, useAppSelector } from '../redux/hooks';
-import { apiUrl } from '../utils/constants';
 import ConversationAvatar from './ConversationAvatar';
 
 const customStyles = {
@@ -38,7 +37,7 @@
 };
 
 export default function ContactList() {
-  const { token } = useAuthContext();
+  const { axiosInstance } = useAuthContext();
   const { accountId } = useAppSelector((state) => state.userInfo);
   const dispatch = useAppDispatch();
 
@@ -59,22 +58,19 @@
   const closeModalDetails = () => setModalDetailsIsOpen(false);
   const closeModalDelete = () => setModalDeleteIsOpen(false);
 
-  const getContactDetails = () => {
+  const getContactDetails = async () => {
     const controller = new AbortController();
-    fetch(new URL(`/contacts/${currentContact.id}`, apiUrl), {
-      signal: controller.signal,
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    })
-      .then((res) => res.json())
-      .then((result) => {
-        console.log('CONTACT LIST - DETAILS: ', result);
-      })
-      .catch((e) => console.log('ERROR GET CONTACT DETAILS: ', e));
+    try {
+      const data = await axiosInstance.get(`/contacts/${currentContact.id}`, {
+        signal: controller.signal,
+      });
+      console.log('CONTACT LIST - DETAILS: ', data);
+    } catch (e) {
+      console.log('ERROR GET CONTACT DETAILS: ', e);
+    }
   };
 
-  const removeOrBlock = (block = false) => {
+  const removeOrBlock = async (block = false) => {
     console.log('REMOVE');
     setBlockOrRemove(false);
     const controller = new AbortController();
@@ -82,33 +78,29 @@
     if (block) {
       url += '/block';
     }
-    fetch(new URL(url, apiUrl), {
-      signal: controller.signal,
-      method: block ? 'POST' : 'DELETE',
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    })
-      .then((res) => res.json())
-      .catch((e) => console.log(`ERROR ${block ? 'blocking' : 'removing'} CONTACT : `, e));
+    try {
+      await axiosInstance(url, {
+        signal: controller.signal,
+        method: block ? 'POST' : 'DELETE',
+      });
+    } catch (e) {
+      console.log(`ERROR ${block ? 'blocking' : 'removing'} CONTACT : `, e);
+    }
     closeModalDelete();
   };
 
   useEffect(() => {
     const controller = new AbortController();
-    fetch(new URL(`/contacts`, apiUrl), {
-      signal: controller.signal,
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    })
-      .then((res) => res.json())
-      .then((result) => {
-        console.log('CONTACTS: ', result);
-        setContacts(result);
+    axiosInstance
+      .get(`/contacts`, {
+        signal: controller.signal,
+      })
+      .then(({ data }) => {
+        console.log('CONTACTS: ', data);
+        setContacts(data);
       });
     return () => controller.abort();
-  }, [token]);
+  }, [axiosInstance]);
 
   return (
     <div className="rooms-list">
diff --git a/client/src/components/ConversationListItem.tsx b/client/src/components/ConversationListItem.tsx
index 5f10f6e..aeef7de 100644
--- a/client/src/components/ConversationListItem.tsx
+++ b/client/src/components/ConversationListItem.tsx
@@ -36,7 +36,6 @@
 import { useAuthContext } from '../contexts/AuthProvider';
 import { setRefreshFromSlice } from '../redux/appSlice';
 import { useAppDispatch } from '../redux/hooks';
-import { apiUrl } from '../utils/constants';
 import ConversationAvatar from './ConversationAvatar';
 import {
   AudioCallIcon,
@@ -89,7 +88,7 @@
 };
 
 export default function ConversationListItem({ conversation }: ConversationListItemProps) {
-  const { token } = useAuthContext();
+  const { axiosInstance } = useAuthContext();
   const { conversationId, contactId } = useParams();
   const dispatch = useAppDispatch();
 
@@ -115,47 +114,36 @@
   const closeModalDetails = () => setModalDetailsIsOpen(false);
   const closeModalDelete = () => setModalDeleteIsOpen(false);
 
-  const getContactDetails = () => {
+  const getContactDetails = async () => {
     const controller = new AbortController();
-    fetch(new URL(`/contacts/${userId}`, apiUrl), {
-      signal: controller.signal,
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    })
-      .then((res) => res.json())
-      .then((result) => {
-        console.log('CONTACT LIST - DETAILS: ', result);
-      })
-      .catch((e) => console.log('ERROR GET CONTACT DETAILS: ', e));
+    try {
+      const data = await axiosInstance.get(`/contacts/${userId}`, {
+        signal: controller.signal,
+      });
+      console.log('CONTACT LIST - DETAILS: ', data);
+    } catch (e) {
+      console.log('ERROR GET CONTACT DETAILS: ', e);
+    }
   };
 
-  const removeOrBlock = (block = false) => {
+  const removeOrBlock = async (block = false) => {
     setBlockOrRemove(false);
 
-    console.log('EEEH', conversation.getAccountId(), userId);
-
     const controller = new AbortController();
     let url = `/contacts/${userId}`;
     if (block) {
       url += '/block';
     }
-    fetch(new URL(url, apiUrl), {
-      signal: controller.signal,
-      method: block ? 'POST' : 'DELETE',
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    })
-      .then((res) => res.json())
-      .then(() => {
-        console.log('propre');
-        dispatch(setRefreshFromSlice());
-      })
-      .catch((e) => {
-        console.log(`ERROR ${block ? 'blocking' : 'removing'} CONTACT : `, e);
-        dispatch(setRefreshFromSlice());
+    try {
+      await axiosInstance(url, {
+        signal: controller.signal,
+        method: block ? 'POST' : 'DELETE',
       });
+      dispatch(setRefreshFromSlice());
+    } catch (e) {
+      console.error(`Error ${block ? 'blocking' : 'removing'} contact : `, e);
+      dispatch(setRefreshFromSlice());
+    }
     closeModalDelete();
   };
 
diff --git a/client/src/components/ConversationsOverviewCard.tsx b/client/src/components/ConversationsOverviewCard.tsx
index 8a73cc2..84d022d 100644
--- a/client/src/components/ConversationsOverviewCard.tsx
+++ b/client/src/components/ConversationsOverviewCard.tsx
@@ -21,10 +21,9 @@
 import { useNavigate } from 'react-router';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { apiUrl } from '../utils/constants';
 
 export default function ConversationsOverviewCard() {
-  const { token, account } = useAuthContext();
+  const { axiosInstance, account } = useAuthContext();
   const navigate = useNavigate();
 
   const [conversationCount, setConversationCount] = useState<number | undefined>();
@@ -33,19 +32,16 @@
 
   useEffect(() => {
     const controller = new AbortController();
-    fetch(new URL('/conversations', apiUrl), {
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-      signal: controller.signal,
-    })
-      .then((res) => res.json())
-      .then((result: Conversation[]) => {
-        console.log(result);
-        setConversationCount(result.length);
+    axiosInstance
+      .get<Conversation[]>('/conversations', {
+        signal: controller.signal,
+      })
+      .then(({ data }) => {
+        console.log(data);
+        setConversationCount(data.length);
       });
     return () => controller.abort(); // crash on React18
-  }, [token, accountId]);
+  }, [axiosInstance, accountId]);
 
   return (
     <Card onClick={() => navigate(`/`)}>
diff --git a/client/src/components/UsernameChooser.jsx b/client/src/components/UsernameChooser.jsx
index 3c1f584..77c1d73 100644
--- a/client/src/components/UsernameChooser.jsx
+++ b/client/src/components/UsernameChooser.jsx
@@ -17,8 +17,8 @@
  */
 import { SearchRounded } from '@mui/icons-material';
 import { InputAdornment, TextField } from '@mui/material';
+import axios from 'axios';
 import { useEffect, useState } from 'react';
-import usePromise from 'react-fetch-hook/usePromise';
 
 import { apiUrl } from '../utils/constants.js';
 
@@ -26,17 +26,29 @@
 
 export default function UsernameChooser({ setName, ...props }) {
   const [query, setQuery] = useState('');
+  const [isLoading, setIsLoading] = useState(true);
+  const [error, setError] = useState();
+  const [data, setData] = useState();
 
-  const { isLoading, data, error } = usePromise(
-    () =>
-      isInputValid(query)
-        ? fetch(new URL(`/ns/username/${query}`, apiUrl)).then((res) => {
-            if (res.status === 200) return res.json();
-            else throw res.status;
-          })
-        : new Promise((res, rej) => rej(400)),
-    [query]
-  );
+  useEffect(() => {
+    if (isInputValid(query)) {
+      setIsLoading(true);
+      axios
+        .get(`/ns/username/${query}`, {
+          baseURL: apiUrl,
+        })
+        .then((res) => {
+          setIsLoading(false);
+          if (res.status === 200) {
+            setData(res.data);
+          } else {
+            throw res.status;
+          }
+        });
+    } else {
+      setError(400);
+    }
+  }, [query]);
 
   useEffect(() => {
     if (!isLoading) {
diff --git a/client/src/contexts/AuthProvider.tsx b/client/src/contexts/AuthProvider.tsx
index ac829dd..440c4dc 100644
--- a/client/src/contexts/AuthProvider.tsx
+++ b/client/src/contexts/AuthProvider.tsx
@@ -15,9 +15,10 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
+import axios, { AxiosInstance } from 'axios';
 import { Account } from 'jami-web-common/dist/Account';
 import { HttpStatusCode } from 'jami-web-common/dist/enums/http-status-code';
-import { createContext, useCallback, useContext, useEffect, useState } from 'react';
+import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
 import { useNavigate } from 'react-router-dom';
 
 import ProcessingRequest from '../components/ProcessingRequest';
@@ -28,6 +29,7 @@
   token: string;
   account: Account;
   logout: () => void;
+  axiosInstance: AxiosInstance;
 }
 
 const AuthContext = createContext<IAuthContext | undefined>(undefined);
@@ -42,6 +44,33 @@
     navigate('/login');
   }, [navigate]);
 
+  const axiosInstance = useMemo(() => {
+    if (!token) {
+      return;
+    }
+
+    const instance = axios.create({
+      baseURL: apiUrl,
+      headers: {
+        Authorization: `Bearer ${token}`,
+      },
+    });
+
+    instance.interceptors.response.use(
+      (res) => res,
+      (e) => {
+        switch (e.response?.status) {
+          case HttpStatusCode.Unauthorized:
+            logout();
+            break;
+        }
+        throw e;
+      }
+    );
+
+    return instance;
+  }, [token, logout]);
+
   useEffect(() => {
     const accessToken = localStorage.getItem('accessToken');
 
@@ -54,33 +83,14 @@
   }, [logout]);
 
   useEffect(() => {
-    if (token) {
-      const getAccount = async () => {
-        const url = new URL('/account', apiUrl);
-        const response = await fetch(url, {
-          method: 'GET',
-          headers: {
-            Authorization: `Bearer ${token}`,
-          },
-        });
-
-        if (response.status === HttpStatusCode.Ok) {
-          const serializedAccount = await response.json();
-          const account = Account.from(serializedAccount);
-          setAccount(account);
-        } else {
-          throw new Error(response.statusText);
-        }
-      };
-
-      getAccount().catch((e) => {
-        console.error('Error while retrieving account: ', e);
-        logout();
-      });
+    if (!axiosInstance) {
+      return;
     }
-  }, [token, logout]);
 
-  if (!token || !account) {
+    axiosInstance.get('/account').then(({ data }) => setAccount(Account.from(data)));
+  }, [axiosInstance, logout]);
+
+  if (!token || !account || !axiosInstance) {
     return <ProcessingRequest open />;
   }
 
@@ -90,6 +100,7 @@
         token,
         logout,
         account,
+        axiosInstance,
       }}
     >
       {children}
diff --git a/client/src/contexts/WebRTCProvider.tsx b/client/src/contexts/WebRTCProvider.tsx
index be5a045..86950f7 100644
--- a/client/src/contexts/WebRTCProvider.tsx
+++ b/client/src/contexts/WebRTCProvider.tsx
@@ -211,24 +211,21 @@
 
   const sendWebRTCOffer = useCallback(async () => {
     if (webRTCConnection && socket) {
-      webRTCConnection
-        .createOffer({
-          offerToReceiveAudio: true,
-          offerToReceiveVideo: true,
-        })
-        .then((sdp) => {
-          socket.send({
-            type: WebSocketMessageType.WebRTCOffer,
-            data: {
-              from: account.getId(),
-              to: contactId,
-              message: {
-                sdp: sdp,
-              },
-            },
-          });
-          webRTCConnection.setLocalDescription(new RTCSessionDescription(sdp));
-        });
+      const sdp = await webRTCConnection.createOffer({
+        offerToReceiveAudio: true,
+        offerToReceiveVideo: true,
+      });
+      socket.send({
+        type: WebSocketMessageType.WebRTCOffer,
+        data: {
+          from: account.getId(),
+          to: contactId,
+          message: {
+            sdp,
+          },
+        },
+      });
+      await webRTCConnection.setLocalDescription(new RTCSessionDescription(sdp));
     }
   }, [account, contactId, socket, webRTCConnection]);
 
diff --git a/client/src/locale/en/translation.json b/client/src/locale/en/translation.json
index 3185015..bd21509 100644
--- a/client/src/locale/en/translation.json
+++ b/client/src/locale/en/translation.json
@@ -55,6 +55,7 @@
   "message_input_placeholder_three": "Write to {{member0}}, {{member1}} and {{member2}}",
   "message_input_placeholder_four": "Write to {{member0}}, {{member1}}, {{member2}}, +1 other member",
   "message_input_placeholder_more": "Write to {{member0}}, {{member1}}, {{member2}}, +{{excess}} other members",
+  "conversation_add_contact": "Add contact",
   "login_username_not_found": "Username not found",
   "login_invalid_password": "Incorrect password",
   "login_form_title": "LOGIN",
diff --git a/client/src/locale/fr/translation.json b/client/src/locale/fr/translation.json
index b3098f4..e61c57a 100644
--- a/client/src/locale/fr/translation.json
+++ b/client/src/locale/fr/translation.json
@@ -55,6 +55,7 @@
   "message_input_placeholder_three": "Écrire à {{member0}}, {{member1}} et {{member2}}",
   "message_input_placeholder_four": "Écrire à {{member0}}, {{member1}}, {{member2}}, +1 autre membre",
   "message_input_placeholder_more": "Écrire à {{member01}}, {{member1}}, {{member2}}, +{{excess}} autres membres",
+  "conversation_add_contact": "Ajouter le contact",
   "login_username_not_found": "Nom d'utilisateur introuvable",
   "login_invalid_password": "Mot de passe incorrect",
   "login_form_title": "CONNEXION",
diff --git a/client/src/pages/AddContactPage.tsx b/client/src/pages/AddContactPage.tsx
index 2c2bfb2..442b405 100644
--- a/client/src/pages/AddContactPage.tsx
+++ b/client/src/pages/AddContactPage.tsx
@@ -17,40 +17,29 @@
  */
 import GroupAddRounded from '@mui/icons-material/GroupAddRounded';
 import { Box, Card, CardContent, Container, Fab, Typography } from '@mui/material';
+import { Trans } from 'react-i18next';
 import { useNavigate } from 'react-router-dom';
 
 import { useAuthContext } from '../contexts/AuthProvider';
 import { setRefreshFromSlice } from '../redux/appSlice';
 import { useAppDispatch } from '../redux/hooks';
-import { apiUrl } from '../utils/constants';
 
 type AddContactPageProps = {
   contactId: string;
 };
 
 export default function AddContactPage({ contactId }: AddContactPageProps) {
-  const { token } = useAuthContext();
+  const { axiosInstance } = useAuthContext();
   const navigate = useNavigate();
 
   const dispatch = useAppDispatch();
 
   const handleClick = async () => {
-    const response = await fetch(new URL(`/conversations`, apiUrl), {
-      method: 'POST',
-      headers: {
-        Accept: 'application/json',
-        Authorization: `Bearer ${token}`,
-        'Content-Type': 'application/json',
-      },
-      body: JSON.stringify({ members: [contactId] }),
-    }).then((res) => {
-      dispatch(setRefreshFromSlice());
-      return res.json();
-    });
+    const { data } = await axiosInstance.put(`/contacts/${contactId}`);
+    dispatch(setRefreshFromSlice());
 
-    console.log(response);
-    if (response.conversationId) {
-      navigate(`/conversation/${response.conversationId}`);
+    if (data.conversationId) {
+      navigate(`/conversation/${data.conversationId}`);
     }
   };
 
@@ -63,7 +52,7 @@
           <Box style={{ textAlign: 'center', marginTop: 16 }}>
             <Fab variant="extended" color="primary" onClick={handleClick}>
               <GroupAddRounded />
-              Add contact
+              <Trans key="conversation_add_contact" />
             </Fab>
           </Box>
         </CardContent>
diff --git a/client/src/pages/JamiLogin.tsx b/client/src/pages/JamiLogin.tsx
index 5cd2917..256bb76 100644
--- a/client/src/pages/JamiLogin.tsx
+++ b/client/src/pages/JamiLogin.tsx
@@ -64,14 +64,14 @@
         const accessToken = await loginUser(username, password);
         setAccessToken(accessToken);
         navigate('/settings', { replace: true });
-      } catch (err) {
+      } catch (e) {
         setIsLoggingInUser(false);
-        if (err instanceof UsernameNotFound) {
+        if (e instanceof UsernameNotFound) {
           setErrorAlertContent(t('login_username_not_found'));
-        } else if (err instanceof InvalidPassword) {
+        } else if (e instanceof InvalidPassword) {
           setErrorAlertContent(t('login_invalid_password'));
         } else {
-          throw err;
+          throw e;
         }
       }
     }
diff --git a/client/src/pages/JamiRegistration.tsx b/client/src/pages/JamiRegistration.tsx
index a1024d5..62e59c1 100644
--- a/client/src/pages/JamiRegistration.tsx
+++ b/client/src/pages/JamiRegistration.tsx
@@ -74,14 +74,14 @@
       const accessToken = await loginUser(username, password);
       setAccessToken(accessToken);
       navigate('/settings', { replace: true });
-    } catch (err) {
+    } catch (e) {
       setIsCreatingUser(false);
-      if (err instanceof UsernameNotFound) {
+      if (e instanceof UsernameNotFound) {
         setErrorAlertContent(t('login_username_not_found'));
-      } else if (err instanceof InvalidPassword) {
+      } else if (e instanceof InvalidPassword) {
         setErrorAlertContent(t('login_invalid_password'));
       } else {
-        throw err;
+        throw e;
       }
     }
   };
diff --git a/client/src/pages/Messenger.tsx b/client/src/pages/Messenger.tsx
index 83d1352..fd8dc99 100644
--- a/client/src/pages/Messenger.tsx
+++ b/client/src/pages/Messenger.tsx
@@ -28,13 +28,12 @@
 import { useAuthContext } from '../contexts/AuthProvider';
 import { useAppSelector } from '../redux/hooks';
 import { MessengerRouteParams } from '../router';
-import { apiUrl } from '../utils/constants';
 import { useUrlParams } from '../utils/hooks';
 import AddContactPage from './AddContactPage';
 
 const Messenger = () => {
   const { refresh } = useAppSelector((state) => state.userInfo);
-  const { token, account } = useAuthContext();
+  const { account, axiosInstance } = useAuthContext();
 
   const [conversations, setConversations] = useState<Conversation[] | undefined>(undefined);
   const [searchQuery, setSearchQuery] = useState('');
@@ -47,51 +46,36 @@
   const accountId = account.getId();
 
   useEffect(() => {
-    console.log('REFRESH CONVERSATIONS FROM MESSENGER');
     const controller = new AbortController();
-    fetch(new URL(`/conversations`, apiUrl), {
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-      signal: controller.signal,
-    })
-      .then((res) => res.json())
-      .then((result: Conversation[]) => {
-        console.log(result);
-        setConversations(Object.values(result).map((c) => Conversation.from(accountId, c)));
+    axiosInstance
+      .get<Conversation[]>('/conversations', {
+        signal: controller.signal,
+      })
+      .then(({ data }) => {
+        setConversations(Object.values(data).map((c) => Conversation.from(accountId, c)));
       });
     // return () => controller.abort()
-  }, [token, accountId, refresh]);
+  }, [axiosInstance, accountId, refresh]);
 
   useEffect(() => {
     if (!searchQuery) return;
     const controller = new AbortController();
-    fetch(new URL(`/ns/username/${searchQuery}`, apiUrl), {
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-      signal: controller.signal,
-    })
-      .then((response) => {
-        if (response.status === 200) {
-          return response.json();
-        } else {
-          throw new Error(response.status.toString());
-        }
+    // TODO: Type properly https://git.jami.net/savoirfairelinux/jami-web/-/issues/92
+    axiosInstance
+      .get<{ state: number; address: string; username: string }>(`/ns/username/${searchQuery}`, {
+        signal: controller.signal,
       })
-      .then((response) => {
-        console.log(response);
-        const contact = new Contact(response.address);
-        contact.setRegisteredName(response.username);
+      .then(({ data }) => {
+        const contact = new Contact(data.address);
+        contact.setRegisteredName(data.username);
         setSearchResults(contact ? Conversation.fromSingleContact(accountId, contact) : undefined);
       })
       .catch(() => {
         setSearchResults(undefined);
       });
     // return () => controller.abort() // crash on React18
-  }, [accountId, searchQuery, token]);
+  }, [accountId, searchQuery, axiosInstance]);
 
-  console.log('Messenger render');
   return (
     <Stack direction="row" height="100vh" width="100vw">
       <Stack flexGrow={0} flexShrink={0} overflow="auto">
diff --git a/client/src/pages/SetupLogin.tsx b/client/src/pages/SetupLogin.tsx
index 79079be..4608a59 100644
--- a/client/src/pages/SetupLogin.tsx
+++ b/client/src/pages/SetupLogin.tsx
@@ -17,6 +17,7 @@
  */
 import GroupAddRounded from '@mui/icons-material/GroupAddRounded';
 import { Box, Card, CardContent, Container, Fab, Input, Typography } from '@mui/material';
+import axios from 'axios';
 import { HttpStatusCode } from 'jami-web-common';
 import { FormEvent, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
@@ -38,17 +39,15 @@
   }, []);
 
   const adminCreation = async (password: string) => {
-    const url = new URL('/setup/admin/create', apiUrl);
-
     let response: Response;
     try {
-      response = await fetch(url, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: JSON.stringify({ password }),
-      });
+      response = await axios.post(
+        '/setup/admin/create',
+        { password },
+        {
+          baseURL: apiUrl,
+        }
+      );
     } catch (e) {
       throw new Error(`Admin creation failed`);
     }
@@ -59,18 +58,16 @@
   };
 
   const adminLogin = async (password: string) => {
-    const url = new URL('/setup/admin/login', apiUrl);
-
     let response: Response;
     try {
-      response = await fetch(url, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: JSON.stringify({ password }),
-      });
-    } catch (err) {
+      response = await axios.post(
+        '/setup/admin/login',
+        { password },
+        {
+          baseURL: apiUrl,
+        }
+      );
+    } catch (e) {
       throw new Error(`Admin login failed`);
     }
 
diff --git a/client/src/services/Conversation.ts b/client/src/services/Conversation.ts
index d742730..92cc80d 100644
--- a/client/src/services/Conversation.ts
+++ b/client/src/services/Conversation.ts
@@ -16,59 +16,44 @@
  * <https://www.gnu.org/licenses/>.
  */
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import axios from 'axios';
 
 import { useAuthContext } from '../contexts/AuthProvider';
-import { apiUrl } from '../utils/constants';
 
 export const useConversationQuery = (conversationId: string) => {
-  const { token } = useAuthContext();
-  return useQuery(['conversation', conversationId], () => fetchConversation(conversationId, token), {
-    enabled: !!conversationId,
-  });
+  const { axiosInstance } = useAuthContext();
+  return useQuery(
+    ['conversation', conversationId],
+    async () => {
+      const { data } = await axiosInstance.get(`/conversations/${conversationId}`);
+      return data;
+    },
+    {
+      enabled: !!conversationId,
+    }
+  );
 };
 
 export const useMessagesQuery = (conversationId: string) => {
-  const { token } = useAuthContext();
-  return useQuery(['messages', conversationId], () => fetchMessages(conversationId, token), {
-    enabled: !!conversationId,
-  });
+  const { axiosInstance } = useAuthContext();
+  return useQuery(
+    ['messages', conversationId],
+    async () => {
+      const { data } = await axiosInstance.get(`/conversations/${conversationId}/messages`);
+      return data;
+    },
+    {
+      enabled: !!conversationId,
+    }
+  );
 };
 
 export const useSendMessageMutation = (conversationId: string) => {
-  const { token } = useAuthContext();
+  const { axiosInstance } = useAuthContext();
   const queryClient = useQueryClient();
   return useMutation(
-    (message: string) =>
-      axios.post(
-        new URL(`/conversations/${conversationId}/messages`, apiUrl).toString(),
-        { message },
-        {
-          headers: {
-            Authorization: `Bearer ${token}`,
-          },
-        }
-      ),
+    (message: string) => axiosInstance.post(`/conversations/${conversationId}/messages`, { message }),
     {
       onSuccess: () => queryClient.invalidateQueries(['messages', conversationId]),
     }
   );
 };
-
-const fetchConversation = (conversationId: string, token: string) =>
-  axios
-    .get(new URL(`/conversations/${conversationId}`, apiUrl).toString(), {
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    })
-    .then((result) => result.data);
-
-const fetchMessages = (conversationId: string, token: string) =>
-  axios
-    .get(new URL(`/conversations/${conversationId}/messages`, apiUrl).toString(), {
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    })
-    .then((result) => result.data);
diff --git a/client/src/utils/auth.ts b/client/src/utils/auth.ts
index 8404eee..3704fe4 100644
--- a/client/src/utils/auth.ts
+++ b/client/src/utils/auth.ts
@@ -15,6 +15,7 @@
  * License along with this program.  If not, see
  * <https://www.gnu.org/licenses/>.
  */
+import axios from 'axios';
 import { passwordStrength } from 'check-password-strength';
 import { HttpStatusCode } from 'jami-web-common';
 
@@ -39,68 +40,58 @@
 const idToStrengthValueCode: StrengthValueCode[] = ['too_weak', 'weak', 'medium', 'strong'];
 
 export async function isNameRegistered(name: string): Promise<boolean> {
-  const url = new URL(`/ns/username/${name}`, apiUrl);
-  const response = await fetch(url);
-
-  switch (response.status) {
-    case HttpStatusCode.Ok:
-      return true;
-    case HttpStatusCode.NotFound:
-      return false;
-    default:
-      throw new Error(await response.text());
+  try {
+    await axios.get(`/ns/username/${name}`, {
+      baseURL: apiUrl,
+    });
+    return true;
+  } catch (e: any) {
+    if (e.response?.status !== HttpStatusCode.NotFound) {
+      throw e;
+    }
+    return false;
   }
 }
 
 export function checkPasswordStrength(password: string): PasswordCheckResult {
   const strengthResult: PasswordStrengthResult = passwordStrength(password);
 
-  const checkResult: PasswordCheckResult = {
+  return {
     strong: strengthResult.id === PasswordStrength.Strong.valueOf(),
     valueCode: idToStrengthValueCode[strengthResult.id] ?? 'default',
   };
-
-  return checkResult;
 }
 
 export async function registerUser(username: string, password: string): Promise<void> {
-  const url = new URL('/auth/new-account', apiUrl);
-  const response: Response = await fetch(url, {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
-    body: JSON.stringify({ username, password }),
-  });
-
-  if (response.status !== HttpStatusCode.Created) {
-    throw new Error(await response.text());
-  }
+  await axios.post(
+    '/auth/new-account',
+    { username, password },
+    {
+      baseURL: apiUrl,
+    }
+  );
 }
 
 export async function loginUser(username: string, password: string): Promise<string> {
-  const url = new URL('/auth/login', apiUrl);
-  const response = await fetch(url, {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
-    body: JSON.stringify({ username, password }),
-  });
-
-  switch (response.status) {
-    case HttpStatusCode.Ok:
-      break;
-    case HttpStatusCode.NotFound:
-      throw new UsernameNotFound();
-    case HttpStatusCode.Unauthorized:
-      throw new InvalidPassword();
-    default:
-      throw new Error(await response.text());
+  try {
+    const { data } = await axios.post(
+      '/auth/login',
+      { username, password },
+      {
+        baseURL: apiUrl,
+      }
+    );
+    return data.accessToken;
+  } catch (e: any) {
+    switch (e.response?.status) {
+      case HttpStatusCode.NotFound:
+        throw new UsernameNotFound();
+      case HttpStatusCode.Unauthorized:
+        throw new InvalidPassword();
+      default:
+        throw e;
+    }
   }
-
-  const data: { accessToken: string } = await response.json();
-  return data.accessToken;
 }
 
 export function getAccessToken(): string {
diff --git a/client/src/utils/constants.ts b/client/src/utils/constants.ts
index f5c5423..56c738a 100644
--- a/client/src/utils/constants.ts
+++ b/client/src/utils/constants.ts
@@ -21,4 +21,10 @@
 
 export const jamiLogoDefaultSize = '512px';
 
-export const apiUrl = new URL(import.meta.env.VITE_API_URL);
+const apiUrl: string = import.meta.env.VITE_API_URL;
+
+if (!apiUrl) {
+  throw new Error('VITE_API_URL not defined');
+}
+
+export { apiUrl };
diff --git a/package-lock.json b/package-lock.json
index 9c524bd..9114722 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -97,7 +97,6 @@
         "react-draggable": "^4.4.5",
         "react-dropzone": "^14.2.3",
         "react-emoji-render": "^1.2.4",
-        "react-fetch-hook": "^1.9.5",
         "react-i18next": "^11.18.6",
         "react-modal": "^3.15.1",
         "react-redux": "^8.0.2",
@@ -14387,14 +14386,6 @@
         "react-dom": ">=0.14.0"
       }
     },
-    "node_modules/react-fetch-hook": {
-      "version": "1.9.5",
-      "resolved": "https://registry.npmjs.org/react-fetch-hook/-/react-fetch-hook-1.9.5.tgz",
-      "integrity": "sha512-LhTwUk0iYQ4SUl2laQr5Jc2fk/K4B6ezntDWWBNdlag/cB3n2RYshofIL3bOghE5FEfkoDi6BqGD/YIwqW1/8w==",
-      "peerDependencies": {
-        "react": ">=16.8.0 <19.0.0"
-      }
-    },
     "node_modules/react-i18next": {
       "version": "11.18.6",
       "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz",
@@ -25648,7 +25639,6 @@
         "react-draggable": "^4.4.5",
         "react-dropzone": "^14.2.3",
         "react-emoji-render": "^1.2.4",
-        "react-fetch-hook": "^1.9.5",
         "react-i18next": "^11.18.6",
         "react-modal": "^3.15.1",
         "react-redux": "^8.0.2",
@@ -28337,12 +28327,6 @@
         "string-replace-to-array": "^1.0.1"
       }
     },
-    "react-fetch-hook": {
-      "version": "1.9.5",
-      "resolved": "https://registry.npmjs.org/react-fetch-hook/-/react-fetch-hook-1.9.5.tgz",
-      "integrity": "sha512-LhTwUk0iYQ4SUl2laQr5Jc2fk/K4B6ezntDWWBNdlag/cB3n2RYshofIL3bOghE5FEfkoDi6BqGD/YIwqW1/8w==",
-      "requires": {}
-    },
     "react-i18next": {
       "version": "11.18.6",
       "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz",
diff --git a/server/src/routers/contacts-router.ts b/server/src/routers/contacts-router.ts
index 5439fed..8d51d2b 100644
--- a/server/src/routers/contacts-router.ts
+++ b/server/src/routers/contacts-router.ts
@@ -39,8 +39,13 @@
 });
 
 contactsRouter.put('/:contactId', (req, res) => {
-  jamid.addContact(res.locals.accountId, req.params.contactId);
-  res.sendStatus(HttpStatusCode.NoContent);
+  const accountId = res.locals.accountId;
+  const contactId = req.params.contactId;
+
+  jamid.addContact(accountId, contactId);
+
+  const contactDetails = jamid.getContactDetails(accountId, contactId);
+  res.send(contactDetails);
 });
 
 contactsRouter.delete('/:contactId', (req, res) => {