whatsapp qr code polling, validate token before save, fix bot startup timing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+51
-51
@@ -13,10 +13,10 @@ type WhatsAppConfig = {
|
||||
phone: string | null;
|
||||
};
|
||||
|
||||
type SSEEvent = {
|
||||
type: 'qr' | 'authenticated' | 'disconnected' | 'waiting';
|
||||
qr?: string;
|
||||
phone?: string;
|
||||
type QRResponse = {
|
||||
qr: string | null;
|
||||
running: boolean;
|
||||
phone: string | null;
|
||||
};
|
||||
|
||||
const SetupGuide = () => {
|
||||
@@ -72,7 +72,7 @@ export const WhatsAppBotConfig = () => {
|
||||
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const fetchConfig = useCallback(() => {
|
||||
client
|
||||
@@ -81,60 +81,59 @@ export const WhatsAppBotConfig = () => {
|
||||
.catch(() => {});
|
||||
}, [client]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
setIsLoading(false);
|
||||
const stopPolling = useCallback(() => {
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current);
|
||||
pollingRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const connectSSE = useCallback(() => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('token') ?? sessionStorage.getItem('token') ?? '';
|
||||
const url = `/api/channels/whatsapp/qr?token=${encodeURIComponent(token)}`;
|
||||
const es = new EventSource(url);
|
||||
eventSourceRef.current = es;
|
||||
|
||||
es.onmessage = async (ev) => {
|
||||
try {
|
||||
const data = JSON.parse(ev.data) as SSEEvent;
|
||||
if (data.type === 'qr' && data.qr) {
|
||||
const pollQR = useCallback(() => {
|
||||
client
|
||||
.get<QRResponse>('/channels/whatsapp/qr')
|
||||
.then(async (data) => {
|
||||
if (data.running) {
|
||||
// Authenticated — stop polling, update config
|
||||
stopPolling();
|
||||
setQrDataUrl(null);
|
||||
fetchConfig();
|
||||
return;
|
||||
}
|
||||
if (data.qr) {
|
||||
const dataUrl = await QRCode.toDataURL(data.qr, { width: 256, margin: 2 });
|
||||
setQrDataUrl(dataUrl);
|
||||
} else if (data.type === 'authenticated') {
|
||||
setQrDataUrl(null);
|
||||
es.close();
|
||||
fetchConfig();
|
||||
} else if (data.type === 'disconnected') {
|
||||
setQrDataUrl(null);
|
||||
es.close();
|
||||
fetchConfig();
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
};
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [client, fetchConfig, stopPolling]);
|
||||
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
setQrDataUrl(null);
|
||||
};
|
||||
}, [fetchConfig]);
|
||||
const startPolling = useCallback(() => {
|
||||
stopPolling();
|
||||
pollQR();
|
||||
pollingRef.current = setInterval(pollQR, 2000);
|
||||
}, [pollQR, stopPolling]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
}
|
||||
};
|
||||
client
|
||||
.get<WhatsAppConfig>('/channels/whatsapp/config')
|
||||
.then((data) => {
|
||||
setConfig(data);
|
||||
// Auto-poll QR if bot is enabled but waiting for scan
|
||||
if (data.enabled && !data.running) {
|
||||
startPolling();
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIsLoading(false));
|
||||
|
||||
return stopPolling;
|
||||
}, []);
|
||||
|
||||
const handleConnect = async () => {
|
||||
setIsConnecting(true);
|
||||
try {
|
||||
await client.put('/channels/whatsapp/config', { enabled: true });
|
||||
connectSSE();
|
||||
startPolling();
|
||||
} catch {
|
||||
toast.error('Failed to start WhatsApp connection');
|
||||
} finally {
|
||||
@@ -146,10 +145,7 @@ export const WhatsAppBotConfig = () => {
|
||||
setIsDisconnecting(true);
|
||||
try {
|
||||
await client.put('/channels/whatsapp/config', { enabled: false });
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
stopPolling();
|
||||
setQrDataUrl(null);
|
||||
toast.success('WhatsApp disconnected');
|
||||
fetchConfig();
|
||||
@@ -170,7 +166,11 @@ export const WhatsAppBotConfig = () => {
|
||||
className={`h-2.5 w-2.5 rounded-full shrink-0 ${config.running ? 'bg-green-500' : 'bg-duck-dark/20 dark:bg-foreground/20'}`}
|
||||
/>
|
||||
<span className="text-sm text-duck-dark dark:text-foreground">
|
||||
{config.running ? `Connected as +${config.phone}` : config.enabled ? 'Starting...' : 'Not connected'}
|
||||
{config.running
|
||||
? `Connected as +${config.phone}`
|
||||
: config.enabled
|
||||
? 'Waiting for QR scan...'
|
||||
: 'Not connected'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -189,7 +189,7 @@ export const WhatsAppBotConfig = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config?.running ? (
|
||||
{config?.running || config?.enabled ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
|
||||
Reference in New Issue
Block a user