Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/pages/instance/DashboardInstance/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,25 @@ import { getProvider, getToken, TOKEN_ID } from "@/lib/queries/token";
import { GoQrCodeModal } from "./GoQrCodeModal";
import { GoSendMessageModal } from "./GoSendMessageModal";

/**
* How often the QR code shown in the dialog is refreshed.
*
* The server rotates the QR code every few seconds, but `GET /instance/connect`
* was only called once, when the dialog opened. The image on screen therefore
* went stale: users scanned a code the server had already replaced, the pairing
* failed, and WhatsApp blamed the phone's internet connection.
*
* Polling is safe: once the instance is in `connecting`, the endpoint returns
* the QR code currently held in memory and does not open a new connection.
*/
const QRCODE_REFRESH_INTERVAL_MS = 10_000;

function DashboardInstance() {
const { t, i18n } = useTranslation();
const numberFormatter = new Intl.NumberFormat(i18n.language);
const [qrCode, setQRCode] = useState<string | null>(null);
const [pairingCode, setPairingCode] = useState("");
const [qrDialogOpen, setQrDialogOpen] = useState(false);
const [goQrOpen, setGoQrOpen] = useState(false);
const [goSendOpen, setGoSendOpen] = useState(false);
const token = getToken(TOKEN_ID.TOKEN);
Expand Down Expand Up @@ -84,6 +98,22 @@ function DashboardInstance() {
}
};

// Keep the displayed QR code in sync with the one the server is serving.
useEffect(() => {
if (!qrDialogOpen || !instance) return;

const intervalId = setInterval(() => {
handleConnect(instance.name, false);
}, QRCODE_REFRESH_INTERVAL_MS);

return () => clearInterval(intervalId);
}, [qrDialogOpen, instance?.name]);
Comment on lines +102 to +110

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider including handleConnect in the effect dependencies or stabilizing it with useCallback.

Because this effect calls handleConnect, it should either be part of the dependency array or be memoized with useCallback. Otherwise, its identity (and logic) can change without retriggering the effect, leading to stale closures and failing the hooks lint rule. Please either memoize and include it in the deps, or explicitly justify its exclusion and suppress the lint here.

Suggested implementation:

  // Keep the displayed QR code in sync with the one the server is serving.
  useEffect(() => {
    if (!qrDialogOpen || !instance) return;

    const intervalId = setInterval(() => {
      handleConnect(instance.name, false);
    }, QRCODE_REFRESH_INTERVAL_MS);

    return () => clearInterval(intervalId);
  }, [qrDialogOpen, instance?.name, handleConnect]);

To fully satisfy the hooks lint rule and avoid stale closures, you should also:

  1. Ensure handleConnect is stabilized with useCallback, e.g. const handleConnect = useCallback((name, shouldOpen = true) => { ... }, [/* its dependencies */]);.
  2. If useCallback is not yet imported in this file, add useCallback to the React import (or import { useCallback } from "react"; depending on existing conventions).
  3. If you intentionally do not want handleConnect in the dependency array, instead wrap the effect in an // eslint-disable-next-line react-hooks/exhaustive-deps comment and document why handleConnect is safe to exclude.


// Stop refreshing as soon as the instance is connected.
useEffect(() => {
if (instance?.connectionStatus === "open") setQrDialogOpen(false);
}, [instance?.connectionStatus]);

const closeQRCodePopup = async () => {
setQRCode(null);
setPairingCode("");
Expand Down Expand Up @@ -185,7 +215,7 @@ function DashboardInstance() {
</>
) : (
<div className="flex flex-wrap gap-2">
<Dialog>
<Dialog open={qrDialogOpen} onOpenChange={setQrDialogOpen}>
<DialogTrigger onClick={() => handleConnect(instance.name, false)} asChild>
<Button>
<QrCode className="mr-2 h-4 w-4" />
Expand Down