feat: show an alert when connection with whatsapp brokes

This commit is contained in:
canove
2020-10-27 19:59:38 -03:00
parent 79e27b71cc
commit 4e259a3de7
13 changed files with 360 additions and 296 deletions

View File

@@ -2,6 +2,7 @@ import { Request, Response } from "express";
import { getWbot } from "../libs/wbot";
import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService";
import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession";
import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService";
const store = async (req: Request, res: Response): Promise<Response> => {
const { whatsappId } = req.params;
@@ -14,9 +15,11 @@ const store = async (req: Request, res: Response): Promise<Response> => {
const update = async (req: Request, res: Response): Promise<Response> => {
const { whatsappId } = req.params;
const whatsapp = await ShowWhatsAppService(whatsappId);
await whatsapp.update({ session: "" });
const { whatsapp } = await UpdateWhatsAppService({
whatsappId,
whatsappData: { session: "" }
});
StartWhatsAppSession(whatsapp);

View File

@@ -21,12 +21,6 @@ export const initWbot = async (whatsapp: Whatsapp): Promise<Session> => {
sessionCfg = JSON.parse(whatsapp.session);
}
const currentSessionIndex = sessions.findIndex(s => s.id === whatsapp.id);
if (currentSessionIndex !== -1) {
sessions[currentSessionIndex].destroy();
sessions.splice(currentSessionIndex, 1);
}
const wbot: Session = new Client({
session: sessionCfg
});

View File

@@ -7,6 +7,7 @@ import Whatsapp from "../../models/Whatsapp";
interface WhatsappData {
name?: string;
status?: string;
session?: string;
isDefault?: boolean;
}
@@ -29,7 +30,7 @@ const UpdateWhatsAppService = async ({
isDefault: Yup.boolean()
});
const { name, status, isDefault } = whatsappData;
const { name, status, isDefault, session } = whatsappData;
try {
await schema.validate({ name, status, isDefault });
@@ -58,6 +59,7 @@ const UpdateWhatsAppService = async ({
await whatsapp.update({
name,
status,
session,
isDefault
});

View File

@@ -1,4 +1,4 @@
import React from "react";
import React, { useContext, useEffect, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import ListItem from "@material-ui/core/ListItem";
@@ -15,6 +15,9 @@ import GroupIcon from "@material-ui/icons/Group";
import ContactPhoneIcon from "@material-ui/icons/ContactPhone";
import { i18n } from "../../translate/i18n";
import { Badge } from "@material-ui/core";
import { WhatsAppsContext } from "../../context/WhatsApp/WhatsAppsContext";
function ListItemLink(props) {
const { icon, primary, to, className } = props;
@@ -39,13 +42,44 @@ function ListItemLink(props) {
const MainListItems = () => {
const userProfile = localStorage.getItem("profile");
const { whatsApps } = useContext(WhatsAppsContext);
const [connectionWarning, setConnectionWarning] = useState(false);
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
if (whatsApps.length > 0) {
const offlineWhats = whatsApps.filter(whats => {
if (
whats.status === "qrcode" ||
whats.status === "PAIRING" ||
whats.status === "DISCONNECTED" ||
whats.status === "TIMEOUT" ||
whats.status === "OPENING"
)
return true;
else return false;
});
if (offlineWhats.length > 0) {
setConnectionWarning(true);
} else {
setConnectionWarning(false);
}
}
}, 2000);
return () => clearTimeout(delayDebounceFn);
}, [whatsApps]);
return (
<div>
<ListItemLink to="/" primary="Dashboard" icon={<DashboardIcon />} />
<ListItemLink
to="/connections"
primary={i18n.t("mainDrawer.listItems.connections")}
icon={<SyncAltIcon />}
icon={
<Badge badgeContent={connectionWarning ? "!" : 0} color="error">
<SyncAltIcon />
</Badge>
}
/>
<ListItemLink
to="/tickets"

View File

@@ -106,7 +106,7 @@ const useStyles = makeStyles(theme => ({
},
}));
const LoggedInLayout = ({ appTitle, children }) => {
const LoggedInLayout = ({ children }) => {
const drawerState = localStorage.getItem("drawerOpen");
const userId = +localStorage.getItem("userId");
const classes = useStyles();

View File

@@ -11,6 +11,48 @@ const useAuth = () => {
const [isAuth, setIsAuth] = useState(false);
const [loading, setLoading] = useState(true);
api.interceptors.request.use(
config => {
const token = localStorage.getItem("token");
if (token) {
config.headers["Authorization"] = `Bearer ${JSON.parse(token)}`;
setIsAuth(true);
}
return config;
},
error => {
Promise.reject(error);
}
);
api.interceptors.response.use(
response => {
return response;
},
async error => {
const originalRequest = error.config;
if (error?.response?.status === 403 && !originalRequest._retry) {
originalRequest._retry = true;
const { data } = await api.post("/auth/refresh_token");
if (data) {
localStorage.setItem("token", JSON.stringify(data.token));
api.defaults.headers.Authorization = `Bearer ${data.token}`;
}
return api(originalRequest);
}
if (error?.response?.status === 401) {
localStorage.removeItem("token");
localStorage.removeItem("username");
localStorage.removeItem("profile");
localStorage.removeItem("userId");
api.defaults.headers.Authorization = undefined;
setIsAuth(false);
}
return Promise.reject(error);
}
);
useEffect(() => {
const token = localStorage.getItem("token");
if (token) {
@@ -18,48 +60,6 @@ const useAuth = () => {
setIsAuth(true);
}
setLoading(false);
api.interceptors.request.use(
config => {
const token = localStorage.getItem("token");
if (token) {
config.headers["Authorization"] = `Bearer ${JSON.parse(token)}`;
setIsAuth(true);
}
return config;
},
error => {
Promise.reject(error);
}
);
api.interceptors.response.use(
response => {
return response;
},
async error => {
const originalRequest = error.config;
if (error?.response?.status === 403 && !originalRequest._retry) {
originalRequest._retry = true;
const { data } = await api.post("/auth/refresh_token");
if (data) {
localStorage.setItem("token", JSON.stringify(data.token));
api.defaults.headers.Authorization = `Bearer ${data.token}`;
}
return api(originalRequest);
}
if (error?.response?.status === 401) {
localStorage.removeItem("token");
localStorage.removeItem("username");
localStorage.removeItem("profile");
localStorage.removeItem("userId");
api.defaults.headers.Authorization = undefined;
setIsAuth(false);
}
return Promise.reject(error);
}
);
}, []);
const handleLogin = async (e, user) => {
@@ -106,7 +106,7 @@ const useAuth = () => {
history.push("/login");
};
return { isAuth, loading, handleLogin, handleLogout };
return { isAuth, setIsAuth, loading, handleLogin, handleLogout };
};
export default useAuth;

View File

@@ -0,0 +1,17 @@
import React, { createContext } from "react";
import useWhatsApps from "./useWhatsApps";
const WhatsAppsContext = createContext();
const WhatsAppsProvider = ({ children }) => {
const { loading, whatsApps } = useWhatsApps();
return (
<WhatsAppsContext.Provider value={{ whatsApps, loading }}>
{children}
</WhatsAppsContext.Provider>
);
};
export { WhatsAppsContext, WhatsAppsProvider };

View File

@@ -0,0 +1,114 @@
import { useState, useEffect, useReducer } from "react";
import { toast } from "react-toastify";
import openSocket from "socket.io-client";
import api from "../../services/api";
import { i18n } from "../../translate/i18n";
const reducer = (state, action) => {
if (action.type === "LOAD_WHATSAPPS") {
const whatsApps = action.payload;
return [...whatsApps];
}
if (action.type === "UPDATE_WHATSAPPS") {
const whatsApp = action.payload;
const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id);
if (whatsAppIndex !== -1) {
state[whatsAppIndex] = whatsApp;
return [...state];
} else {
return [whatsApp, ...state];
}
}
if (action.type === "UPDATE_SESSION") {
const whatsApp = action.payload;
const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id);
if (whatsAppIndex !== -1) {
state[whatsAppIndex].status = whatsApp.status;
state[whatsAppIndex].updatedAt = whatsApp.updatedAt;
state[whatsAppIndex].qrcode = whatsApp.qrcode;
state[whatsAppIndex].retries = whatsApp.retries;
return [...state];
} else {
return [...state];
}
}
if (action.type === "DELETE_WHATSAPPS") {
const whatsAppId = action.payload;
const whatsAppIndex = state.findIndex(s => s.id === whatsAppId);
if (whatsAppIndex !== -1) {
state.splice(whatsAppIndex, 1);
}
return [...state];
}
if (action.type === "RESET") {
return [];
}
};
const useWhatsApps = () => {
const [whatsApps, dispatch] = useReducer(reducer, []);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
const fetchSession = async () => {
try {
const { data } = await api.get("/whatsapp/");
dispatch({ type: "LOAD_WHATSAPPS", payload: data });
setLoading(false);
} catch (err) {
setLoading(false);
const errorMsg = err.response?.data?.error;
if (errorMsg) {
if (i18n.exists(`backendErrors.${errorMsg}`)) {
toast.error(i18n.t(`backendErrors.${errorMsg}`));
} else {
toast.error(err.response.data.error);
}
} else {
toast.error("Unknown error");
}
}
};
fetchSession();
}, []);
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("whatsapp", data => {
if (data.action === "update") {
dispatch({ type: "UPDATE_WHATSAPPS", payload: data.whatsapp });
}
});
socket.on("whatsapp", data => {
if (data.action === "delete") {
dispatch({ type: "DELETE_WHATSAPPS", payload: data.whatsappId });
}
});
socket.on("whatsappSession", data => {
if (data.action === "update") {
dispatch({ type: "UPDATE_SESSION", payload: data.session });
}
});
return () => {
socket.disconnect();
};
}, []);
return { whatsApps, loading };
};
export default useWhatsApps;

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect, useReducer, useCallback } from "react";
import openSocket from "socket.io-client";
import React, { useState, useCallback, useContext } from "react";
import { toast } from "react-toastify";
import { format, parseISO } from "date-fns";
@@ -16,13 +15,13 @@ import {
Paper,
Tooltip,
Typography,
CircularProgress,
} from "@material-ui/core";
import {
Edit,
CheckCircle,
SignalCellularConnectedNoInternet2Bar,
SignalCellularConnectedNoInternet0Bar,
Schedule,
SignalCellular4Bar,
CropFree,
DeleteOutline,
@@ -37,58 +36,9 @@ import TableRowSkeleton from "../../components/TableRowSkeleton";
import api from "../../services/api";
import WhatsAppModal from "../../components/WhatsAppModal";
import ConfirmationModal from "../../components/ConfirmationModal";
import ButtonWithSpinner from "../../components/ButtonWithSpinner";
import QrcodeModal from "../../components/QrcodeModal";
import { i18n } from "../../translate/i18n";
const reducer = (state, action) => {
if (action.type === "LOAD_WHATSAPPS") {
const whatsApps = action.payload;
return [...whatsApps];
}
if (action.type === "UPDATE_WHATSAPPS") {
const whatsApp = action.payload;
const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id);
if (whatsAppIndex !== -1) {
state[whatsAppIndex] = whatsApp;
return [...state];
} else {
return [whatsApp, ...state];
}
}
if (action.type === "UPDATE_SESSION") {
const whatsApp = action.payload;
const whatsAppIndex = state.findIndex(s => s.id === whatsApp.id);
if (whatsAppIndex !== -1) {
state[whatsAppIndex].status = whatsApp.status;
state[whatsAppIndex].updatedAt = whatsApp.updatedAt;
state[whatsAppIndex].qrcode = whatsApp.qrcode;
state[whatsAppIndex].retries = whatsApp.retries;
return [...state];
} else {
return [...state];
}
}
if (action.type === "DELETE_WHATSAPPS") {
const whatsAppId = action.payload;
const whatsAppIndex = state.findIndex(s => s.id === whatsAppId);
if (whatsAppIndex !== -1) {
state.splice(whatsAppIndex, 1);
}
return [...state];
}
if (action.type === "RESET") {
return [];
}
};
import { WhatsAppsContext } from "../../context/WhatsApp/WhatsAppsContext";
const useStyles = makeStyles(theme => ({
mainPaper: {
@@ -112,6 +62,9 @@ const useStyles = makeStyles(theme => ({
tooltipPopper: {
textAlign: "center",
},
buttonProgress: {
color: green[500],
},
}));
const CustomToolTip = ({ title, content, children }) => {
@@ -141,12 +94,10 @@ const CustomToolTip = ({ title, content, children }) => {
const Connections = () => {
const classes = useStyles();
const [whatsApps, dispatch] = useReducer(reducer, []);
const { whatsApps, loading } = useContext(WhatsAppsContext);
const [whatsAppModalOpen, setWhatsAppModalOpen] = useState(false);
const [qrModalOpen, setQrModalOpen] = useState(false);
const [selectedWhatsApp, setSelectedWhatsApp] = useState(null);
const [loading, setLoading] = useState(true);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const confirmationModalInitialState = {
action: "",
@@ -159,63 +110,19 @@ const Connections = () => {
confirmationModalInitialState
);
useEffect(() => {
setLoading(true);
const fetchSession = async () => {
try {
const { data } = await api.get("/whatsapp/");
dispatch({ type: "LOAD_WHATSAPPS", payload: data });
setLoading(false);
} catch (err) {
setLoading(false);
const errorMsg = err.response?.data?.error;
if (errorMsg) {
if (i18n.exists(`backendErrors.${errorMsg}`)) {
toast.error(i18n.t(`backendErrors.${errorMsg}`));
} else {
toast.error(err.response.data.error);
}
} else {
toast.error("Unknown error");
}
}
};
fetchSession();
}, []);
useEffect(() => {
const socket = openSocket(process.env.REACT_APP_BACKEND_URL);
socket.on("whatsapp", data => {
if (data.action === "update") {
dispatch({ type: "UPDATE_WHATSAPPS", payload: data.whatsapp });
}
});
socket.on("whatsapp", data => {
if (data.action === "delete") {
dispatch({ type: "DELETE_WHATSAPPS", payload: data.whatsappId });
}
});
socket.on("whatsappSession", data => {
if (data.action === "update") {
dispatch({ type: "UPDATE_SESSION", payload: data.session });
}
});
return () => {
socket.disconnect();
};
}, []);
const handleStartWhatsAppSession = async whatsAppId => {
try {
await api.post(`/whatsappsession/${whatsAppId}`);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
const errorMsg = err.response?.data?.error;
if (errorMsg) {
if (i18n.exists(`backendErrors.${errorMsg}`)) {
toast.error(i18n.t(`backendErrors.${errorMsg}`));
} else {
toast.error(err.response.data.error);
}
} else {
toast.error("Unknown error");
}
}
};
@@ -224,9 +131,15 @@ const Connections = () => {
try {
await api.put(`/whatsappsession/${whatsAppId}`);
} catch (err) {
console.log(err);
if (err.response && err.response.data && err.response.data.error) {
toast.error(err.response.data.error);
const errorMsg = err.response?.data?.error;
if (errorMsg) {
if (i18n.exists(`backendErrors.${errorMsg}`)) {
toast.error(i18n.t(`backendErrors.${errorMsg}`));
} else {
toast.error(err.response.data.error);
}
} else {
toast.error("Unknown error");
}
}
};
@@ -256,108 +169,6 @@ const Connections = () => {
setWhatsAppModalOpen(true);
};
const renderActionButtons = whatsApp => {
return (
<>
{whatsApp.status === "qrcode" && (
<Button
size="small"
variant="contained"
color="primary"
onClick={() => handleOpenQrModal(whatsApp)}
>
{i18n.t("connections.buttons.qrcode")}
</Button>
)}
{whatsApp.status === "DISCONNECTED" && (
<>
<Button
size="small"
variant="outlined"
color="primary"
onClick={() => handleStartWhatsAppSession(whatsApp.id)}
>
{i18n.t("connections.buttons.tryAgain")}
</Button>{" "}
<Button
size="small"
variant="outlined"
color="secondary"
onClick={() => handleRequestNewQrCode(whatsApp.id)}
>
{i18n.t("connections.buttons.newQr")}
</Button>
</>
)}
{(whatsApp.status === "CONNECTED" ||
whatsApp.status === "PAIRING" ||
whatsApp.status === "TIMEOUT") && (
<Button
size="small"
variant="outlined"
color="secondary"
onClick={() => {
handleOpenConfirmationModal("disconnect", whatsApp.id);
}}
>
{i18n.t("connections.buttons.disconnect")}
</Button>
)}
{whatsApp.status === "OPENING" && (
<ButtonWithSpinner
size="small"
variant="outlined"
loading={true}
color="default"
>
{i18n.t("connections.buttons.connecting")}
</ButtonWithSpinner>
)}
</>
);
};
const renderStatusToolTips = whatsApp => {
return (
<div className={classes.customTableCell}>
{whatsApp.status === "DISCONNECTED" && (
<CustomToolTip
title={i18n.t("connections.toolTips.disconnected.title")}
content={i18n.t("connections.toolTips.disconnected.content")}
>
<SignalCellularConnectedNoInternet0Bar color="secondary" />
</CustomToolTip>
)}
{whatsApp.status === "OPENING" && (
<CustomToolTip title={i18n.t("connections.toolTips.opening.title")}>
<Schedule color="disabled" />
</CustomToolTip>
)}
{whatsApp.status === "qrcode" && (
<CustomToolTip
title={i18n.t("connections.toolTips.qrcode.title")}
content={i18n.t("connections.toolTips.qrcode.content")}
>
<CropFree />
</CustomToolTip>
)}
{whatsApp.status === "CONNECTED" && (
<CustomToolTip title={i18n.t("connections.toolTips.connected.title")}>
<SignalCellular4Bar style={{ color: green[500] }} />
</CustomToolTip>
)}
{(whatsApp.status === "TIMEOUT" || whatsApp.status === "PAIRING") && (
<CustomToolTip
title={i18n.t("connections.toolTips.timeout.title")}
content={i18n.t("connections.toolTips.timeout.content")}
>
<SignalCellularConnectedNoInternet2Bar color="secondary" />
</CustomToolTip>
)}
</div>
);
};
const handleOpenConfirmationModal = (action, whatsAppId) => {
if (action === "disconnect") {
setConfirmModalInfo({
@@ -418,6 +229,101 @@ const Connections = () => {
setConfirmModalInfo(confirmationModalInitialState);
};
const renderActionButtons = whatsApp => {
return (
<>
{whatsApp.status === "qrcode" && (
<Button
size="small"
variant="contained"
color="primary"
onClick={() => handleOpenQrModal(whatsApp)}
>
{i18n.t("connections.buttons.qrcode")}
</Button>
)}
{whatsApp.status === "DISCONNECTED" && (
<>
<Button
size="small"
variant="outlined"
color="primary"
onClick={() => handleStartWhatsAppSession(whatsApp.id)}
>
{i18n.t("connections.buttons.tryAgain")}
</Button>{" "}
<Button
size="small"
variant="outlined"
color="secondary"
onClick={() => handleRequestNewQrCode(whatsApp.id)}
>
{i18n.t("connections.buttons.newQr")}
</Button>
</>
)}
{(whatsApp.status === "CONNECTED" ||
whatsApp.status === "PAIRING" ||
whatsApp.status === "TIMEOUT") && (
<Button
size="small"
variant="outlined"
color="secondary"
onClick={() => {
handleOpenConfirmationModal("disconnect", whatsApp.id);
}}
>
{i18n.t("connections.buttons.disconnect")}
</Button>
)}
{whatsApp.status === "OPENING" && (
<Button size="small" variant="outlined" disabled color="default">
{i18n.t("connections.buttons.connecting")}
</Button>
)}
</>
);
};
const renderStatusToolTips = whatsApp => {
return (
<div className={classes.customTableCell}>
{whatsApp.status === "DISCONNECTED" && (
<CustomToolTip
title={i18n.t("connections.toolTips.disconnected.title")}
content={i18n.t("connections.toolTips.disconnected.content")}
>
<SignalCellularConnectedNoInternet0Bar color="secondary" />
</CustomToolTip>
)}
{whatsApp.status === "OPENING" && (
<CircularProgress size={24} className={classes.buttonProgress} />
)}
{whatsApp.status === "qrcode" && (
<CustomToolTip
title={i18n.t("connections.toolTips.qrcode.title")}
content={i18n.t("connections.toolTips.qrcode.content")}
>
<CropFree />
</CustomToolTip>
)}
{whatsApp.status === "CONNECTED" && (
<CustomToolTip title={i18n.t("connections.toolTips.connected.title")}>
<SignalCellular4Bar style={{ color: green[500] }} />
</CustomToolTip>
)}
{(whatsApp.status === "TIMEOUT" || whatsApp.status === "PAIRING") && (
<CustomToolTip
title={i18n.t("connections.toolTips.timeout.title")}
content={i18n.t("connections.toolTips.timeout.content")}
>
<SignalCellularConnectedNoInternet2Bar color="secondary" />
</CustomToolTip>
)}
</div>
);
};
return (
<MainContainer>
<ConfirmationModal

View File

@@ -12,6 +12,7 @@ import Settings from "../pages/Settings/";
import Users from "../pages/Users";
import Contacts from "../pages/Contacts/";
import { AuthProvider } from "../context/Auth/AuthContext";
import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext";
import Route from "./Route";
const Routes = () => {
@@ -21,24 +22,26 @@ const Routes = () => {
<Switch>
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
<LoggedInLayout>
<Route exact path="/" component={Dashboard} isPrivate />
<Route
exact
path="/tickets/:ticketId?"
component={Tickets}
isPrivate
/>
<Route
exact
path="/connections"
component={Connections}
isPrivate
/>
<Route exact path="/contacts" component={Contacts} isPrivate />
<Route exact path="/users" component={Users} isPrivate />
<Route exact path="/Settings" component={Settings} isPrivate />
</LoggedInLayout>
<WhatsAppsProvider>
<LoggedInLayout>
<Route exact path="/" component={Dashboard} isPrivate />
<Route
exact
path="/tickets/:ticketId?"
component={Tickets}
isPrivate
/>
<Route
exact
path="/connections"
component={Connections}
isPrivate
/>
<Route exact path="/contacts" component={Contacts} isPrivate />
<Route exact path="/users" component={Users} isPrivate />
<Route exact path="/Settings" component={Settings} isPrivate />
</LoggedInLayout>
</WhatsAppsProvider>
</Switch>
<ToastContainer autoClose={3000} />
</AuthProvider>

View File

@@ -65,9 +65,6 @@ const messages = {
content:
"Make sure your cell phone is connected to the internet and try again, or request a new QR Code",
},
opening: {
title: "Starting session...",
},
qrcode: {
title: "Waiting for QR Code read",
content:

View File

@@ -67,9 +67,6 @@ const messages = {
content:
"Asegúrese de que su teléfono celular esté conectado a Internet y vuelva a intentarlo o solicite un nuevo código QR",
},
opening: {
title: "Iniciando sesión ...",
},
qrcode: {
title: "Esperando la lectura del código QR",
content:

View File

@@ -66,9 +66,6 @@ const messages = {
content:
"Certifique-se de que seu celular esteja conectado à internet e tente novamente, ou solicite um novo QR Code",
},
opening: {
title: "Iniciando a sessão...",
},
qrcode: {
title: "Esperando leitura do QR Code",
content: