fix: resolve new session creation state race condition and backdrop closure in AppWPP

This commit is contained in:
2026-10-15 14:27:22 -03:00
parent f01101cab0
commit d3b7dcbc1e
2 changed files with 118 additions and 70 deletions
+101 -62
View File
@@ -74,9 +74,12 @@ export default function AppWPP() {
}, []);
// Salva no localStorage sempre que `servers` mudar
const updateServersState = (updatedList) => {
setServers(updatedList);
saveServers(updatedList);
const updateServersState = (updater) => {
setServers((prevServers) => {
const next = typeof updater === 'function' ? updater(prevServers) : updater;
saveServers(next);
return next;
});
};
const showToast = (message, type = 'info') => {
@@ -91,79 +94,97 @@ export default function AppWPP() {
// Servidor CRUD
const handleSaveServer = (serverData) => {
let updated;
const exists = servers.some((s) => s.id === serverData.id);
if (exists) {
updated = servers.map((s) => (s.id === serverData.id ? { ...s, ...serverData } : s));
showToast(`Servidor "${serverData.name}" atualizado!`, 'success');
} else {
updated = [...servers, serverData];
setActiveServerId(serverData.id);
showToast(`Servidor "${serverData.name}" adicionado com sucesso!`, 'success');
}
updateServersState(updated);
updateServersState((prevServers) => {
const exists = prevServers.some((s) => s.id === serverData.id);
let updated;
if (exists) {
updated = prevServers.map((s) => (s.id === serverData.id ? { ...s, ...serverData } : s));
} else {
updated = [...prevServers, serverData];
setActiveServerId(serverData.id);
}
return updated;
});
showToast(`Servidor "${serverData.name}" salvo com sucesso!`, 'success');
};
const handleRemoveServer = () => {
if (!activeServer) return;
if (!activeServerId) return;
const currentName = activeServer?.name || 'Servidor';
setConfirmModal({
isOpen: true,
title: 'Remover Servidor',
message: `Tem certeza que deseja remover o servidor "${activeServer.name}"?`,
message: `Tem certeza que deseja remover o servidor "${currentName}"?`,
description: 'Todas as sessões cadastradas para este servidor deixarão de ser exibidas na listagem.',
confirmText: 'Remover Servidor',
type: 'danger',
onConfirm: () => {
const updated = servers.filter((s) => s.id !== activeServer.id);
updateServersState(updated);
setActiveServerId(updated.length > 0 ? updated[0].id : null);
showToast(`Servidor "${activeServer.name}" removido.`, 'info');
updateServersState((prevServers) => {
const updated = prevServers.filter((s) => s.id !== activeServerId);
setActiveServerId(updated.length > 0 ? updated[0].id : null);
return updated;
});
showToast(`Servidor "${currentName}" removido.`, 'info');
}
});
};
// Sessão CRUD
const handleSaveSession = async (sessionData, originalName) => {
if (!activeServer) return;
if (!activeServerId) return;
let updatedSessions = [...(activeServer.sessions || [])];
if (originalName) {
updatedSessions = updatedSessions.map((s) => (s.name === originalName ? sessionData : s));
showToast(`Sessão "${sessionData.name}" atualizada!`, 'success');
} else {
if (updatedSessions.some((s) => s.name === sessionData.name)) {
showToast('Já existe uma sessão com este nome.', 'error');
return;
let isDuplicate = false;
updateServersState((prevServers) => {
const srv = prevServers.find((s) => s.id === activeServerId);
if (!srv) return prevServers;
const currentSessions = [...(srv.sessions || [])];
let updatedSessions;
if (originalName) {
updatedSessions = currentSessions.map((s) => (s.name === originalName ? sessionData : s));
} else {
if (currentSessions.some((s) => s.name === sessionData.name)) {
isDuplicate = true;
return prevServers;
}
updatedSessions = [...currentSessions, sessionData];
}
updatedSessions.push(sessionData);
showToast(`Sessão "${sessionData.name}" criada!`, 'success');
return prevServers.map((s) => (s.id === activeServerId ? { ...s, sessions: updatedSessions } : s));
});
if (isDuplicate) {
showToast('Já existe uma sessão com este nome.', 'error');
return;
}
const updatedServer = { ...activeServer, sessions: updatedSessions };
const updatedServers = servers.map((s) => (s.id === activeServer.id ? updatedServer : s));
updateServersState(updatedServers);
if (originalName) {
showToast(`Sessão "${sessionData.name}" atualizada!`, 'success');
} else {
showToast(`Sessão "${sessionData.name}" criada com sucesso!`, 'success');
// Se for nova sessão, tenta gerar token e iniciar
if (!originalName) {
try {
const token = await generateToken(activeServer, sessionData.name);
if (token) {
sessionData.token = token;
updateServersState(
servers.map((s) =>
s.id === activeServer.id
? { ...s, sessions: updatedSessions.map((x) => (x.name === sessionData.name ? { ...x, token } : x)) }
: s
)
);
// Se for nova sessão, tenta gerar token e iniciar no servidor WPP
const currentServer = servers.find((s) => s.id === activeServerId) || activeServer;
if (currentServer) {
if (!sessionData.token) {
try {
const token = await generateToken(currentServer, sessionData.name);
if (token) {
updateSessionProperty(sessionData.name, { token });
}
} catch (e) {
console.warn('Erro ao gerar token automático:', e);
}
}
} catch (e) {}
handleStartSession(sessionData.name, true);
handleStartSession(sessionData.name, true);
}
}
};
const handleRemoveSession = (sessionName) => {
if (!activeServer) return;
if (!activeServerId) return;
setConfirmModal({
isOpen: true,
title: 'Excluir Sessão',
@@ -172,11 +193,13 @@ export default function AppWPP() {
confirmText: 'Excluir Sessão',
type: 'danger',
onConfirm: () => {
const updatedSessions = activeServer.sessions.filter((s) => s.name !== sessionName);
const updatedServers = servers.map((s) =>
s.id === activeServer.id ? { ...s, sessions: updatedSessions } : s
updateServersState((prevServers) =>
prevServers.map((s) => {
if (s.id !== activeServerId) return s;
const updatedSessions = (s.sessions || []).filter((sess) => sess.name !== sessionName);
return { ...s, sessions: updatedSessions };
})
);
updateServersState(updatedServers);
showToast(`Sessão "${sessionName}" removida.`, 'info');
}
});
@@ -284,14 +307,18 @@ export default function AppWPP() {
};
const updateSessionProperty = (sessionName, props) => {
if (!activeServer) return;
const updatedSessions = activeServer.sessions.map((s) =>
s.name === sessionName ? { ...s, ...props } : s
if (!activeServerId) return;
updateServersState((prevServers) =>
prevServers.map((srv) => {
if (srv.id !== activeServerId) return srv;
const currentSessions = srv.sessions || [];
const exists = currentSessions.some((s) => s.name === sessionName);
const updatedSessions = exists
? currentSessions.map((s) => (s.name === sessionName ? { ...s, ...props } : s))
: [...currentSessions, { name: sessionName, ...props }];
return { ...srv, sessions: updatedSessions };
})
);
const updatedServers = servers.map((srv) =>
srv.id === activeServer.id ? { ...srv, sessions: updatedSessions } : srv
);
updateServersState(updatedServers);
};
// Filtro de sessões
@@ -469,8 +496,20 @@ export default function AppWPP() {
<p>
{searchQuery
? 'Nenhuma sessão corresponde à busca informada.'
: 'Clique em "+ Nova Sessão" acima para cadastrar sua primeira sessão do WhatsApp.'}
: 'Clique no botão abaixo para cadastrar uma nova sessão do WhatsApp.'}
</p>
<button
type="button"
className="btn-new-session"
style={{ marginTop: '0.75rem' }}
onClick={() => {
setSessionToEdit(null);
setIsSessionModalOpen(true);
}}
>
<Plus size={16} />
<span>Nova Sessão</span>
</button>
</div>
)}
</div>
+17 -8
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { X, MessageSquare, Key, Globe, Shield } from 'lucide-react';
export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit }) {
@@ -7,6 +7,7 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
const [webhook, setWebhook] = useState('');
const [waitQrCode, setWaitQrCode] = useState(false);
const [proxyUrl, setProxyUrl] = useState('');
const inputRef = useRef(null);
useEffect(() => {
if (sessionToEdit) {
@@ -22,19 +23,23 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
setWaitQrCode(false);
setProxyUrl('');
}
if (isOpen) {
setTimeout(() => inputRef.current?.focus(), 100);
}
}, [sessionToEdit, isOpen]);
if (!isOpen) return null;
const handleSubmit = (e) => {
e.preventDefault();
if (!name.trim()) {
const cleanName = name.trim().replace(/\s+/g, '_');
if (!cleanName) {
alert('Informe o nome da sessão.');
return;
}
onSave({
name: name.trim(),
name: cleanName,
token: token.trim(),
webhook: webhook.trim(),
waitQrCode,
@@ -47,14 +52,14 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
};
return (
<div className="modal-backdrop" onClick={onClose}>
<div className="modal-dialog" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '520px' }}>
<div className="modal-backdrop">
<div className="modal-dialog" style={{ maxWidth: '520px' }}>
<div className="modal-header">
<div className="modal-title-box">
<MessageSquare size={20} className="text-emerald" />
<h3 className="modal-title">{sessionToEdit ? 'Editar Sessão' : 'Nova Sessão WhatsApp'}</h3>
</div>
<button type="button" className="modal-close-btn" onClick={onClose}>
<button type="button" className="modal-close-btn" onClick={onClose} title="Fechar">
<X size={20} />
</button>
</div>
@@ -64,13 +69,17 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
<div className="form-group">
<label className="wpp-form-label">Nome da Sessão (Identificador)</label>
<input
ref={inputRef}
type="text"
className="password-input"
placeholder="Ex: atendimento_01, admin, clinica"
placeholder="Ex: atendimento_01, admin, clinica (sem espaços)"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<span style={{ fontSize: '0.75rem', color: '#94a3b8' }}>
Nome de identificação único para a rota da API (espaços serão convertidos em "_").
</span>
</div>
<div className="form-group">
@@ -78,7 +87,7 @@ export default function ModalSession({ isOpen, onClose, onSave, sessionToEdit })
<input
type="text"
className="password-input"
placeholder="Token JWT gerado pelo servidor"
placeholder="Token JWT (gerado automaticamente se vazio)"
value={token}
onChange={(e) => setToken(e.target.value)}
/>