Skip to content

fix(instance): refresh the QR code while the dialog is open - #34

Open
michumichifu wants to merge 1 commit into
evolution-foundation:mainfrom
michumichifu:fix/qrcode-auto-refresh
Open

fix(instance): refresh the QR code while the dialog is open#34
michumichifu wants to merge 1 commit into
evolution-foundation:mainfrom
michumichifu:fix/qrcode-auto-refresh

Conversation

@michumichifu

@michumichifu michumichifu commented Aug 7, 2026

Copy link
Copy Markdown

Problem

The dashboard requests a QR code once, when the dialog opens, and never again:

const handleConnect = async (instanceName, wantPairing) => {
  const data = await connect({ instanceName, token });
  setQRCode(data.code);   // set once, never updated
};

The server, however, keeps rotating the pairing code every few seconds. From the
second rotation onwards, the image on screen is a code the server has already
discarded, and there is nothing on the page telling the user that.

What the user sees when they scan a stale code is not a QR error. The phone shows
"logging in…" for a while and then:

Could not log in. Check your phone's internet connection and scan the QR code again.

So the failure is reported as a network problem on the phone, which sends people
looking in the wrong place. On our own deployment this made pairing feel
unreliable for months, across several instances.

Evidence

Server log for one pairing attempt, with the code counter already at 3 while the
dialog still displayed the first one:

17:24:50  { instance: Mundo Veneco, pairingCode: null, qrcodeCount: 3 }
17:24:57  connection: 'close', statusCode: 515

Fix

Poll GET /instance/connect every 10 seconds while the dialog is open.

This does not create extra connections. Once the instance is in connecting,
the endpoint returns the code currently held in memory and opens nothing:

o == "connecting" ? t.qrCode
: o == "close"    ? (await t.connectToWhatsapp(e), await delay(2000), t.qrCode)

Only the close branch starts a connection, and by the time the interval fires
the first time the instance has already moved to connecting.

The interval is cleared when the dialog closes, and a second effect closes the
dialog as soon as the instance reports open, so polling stops on success
instead of running until the user dismisses the modal.

Notes

  • 10 s is shorter than the server's rotation window, so the displayed code is
    never more than one poll behind.
  • The dialog is now controlled (open / onOpenChange) because the effect needs
    to know whether it is visible.
  • npm run type-check and eslint pass. prettier --check reports this file as
    unformatted both before and after the change, so it was left untouched rather
    than mixing a whole-file reformat into the diff.

Summary by Sourcery

Refresh the instance QR code periodically while the connection dialog is open to prevent users from scanning stale codes.

Bug Fixes:

  • Fix stale QR codes during instance pairing by polling the connect endpoint while the QR dialog is open.

Enhancements:

  • Control the QR dialog visibility via component state so it can automatically close once the instance connection is open.

The server rotates the pairing QR code every few seconds, but the dashboard
called GET /instance/connect only once, when the dialog was opened. From then
on the image on screen never changed, so anyone who did not scan within the
first rotation was scanning a code the server had already discarded.

The pairing then fails in a confusing way: the phone shows "logging in" and
ends with "Could not log in. Check your phone's internet connection and scan
the QR code again", which points the user at their network instead of at the
stale code.

Poll the endpoint every 10 seconds while the dialog is open. This does not open
extra connections: once the instance is in 'connecting', connectToWhatsapp()
returns the QR code already held in memory. The interval is cleared when the
dialog closes, and the dialog closes itself as soon as the instance reports
'open'.
@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds controlled dialog state and polling logic to keep the instance QR code refreshed while the pairing dialog is open, and automatically closes the dialog once the instance connects.

Sequence diagram for refreshed instance QR code polling while dialog is open

sequenceDiagram
    actor User
    participant DashboardInstance
    participant Dialog
    participant InstanceAPI

    User->>Dialog: click DialogTrigger
    Dialog-->>DashboardInstance: onOpenChange setQrDialogOpen(true)
    DashboardInstance->>InstanceAPI: GET_instance_connect via handleConnect(instanceName, false)
    InstanceAPI-->>DashboardInstance: qrCode
    DashboardInstance->>DashboardInstance: setQRCode(qrCode)
    DashboardInstance-->>Dialog: render with qrDialogOpen(true)

    loop every QRCODE_REFRESH_INTERVAL_MS while qrDialogOpen and instance
        DashboardInstance->>InstanceAPI: GET_instance_connect via handleConnect(instanceName, false)
        InstanceAPI-->>DashboardInstance: qrCode
        DashboardInstance->>DashboardInstance: setQRCode(qrCode)
    end

    InstanceAPI-->>DashboardInstance: instance.connectionStatus becomes open
    DashboardInstance->>DashboardInstance: setQrDialogOpen(false)
    DashboardInstance-->>Dialog: render with qrDialogOpen(false)
    Dialog-->>User: close QR modal and stop polling
Loading

File-Level Changes

Change Details Files
Introduce a periodic refresh interval for the QR code while the pairing dialog is open.
  • Define QRCODE_REFRESH_INTERVAL_MS (10 seconds) with documentation on server QR rotation behavior.
  • Add a useEffect that, when the QR dialog is open and an instance exists, sets up a setInterval to call handleConnect periodically, and clears the interval on cleanup.
  • Scope the polling effect to dialog visibility and instance identity via the qrDialogOpen and instance?.name dependencies.
src/pages/instance/DashboardInstance/index.tsx
Control the QR dialog's open state and stop refreshing once the instance is connected.
  • Introduce qrDialogOpen state and setter to track the QR dialog visibility.
  • Wire Dialog to use controlled open/onOpenChange props instead of being uncontrolled.
  • Add a useEffect that closes the QR dialog when instance.connectionStatus becomes "open" so polling stops on successful connection.
src/pages/instance/DashboardInstance/index.tsx

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The QR polling effect depends on handleConnect but doesn't include it in the dependency array; consider wrapping handleConnect in useCallback and adding it as a dependency to avoid stale closures if its implementation changes.
  • closeQRCodePopup resets qrCode and pairingCode but does not update qrDialogOpen, which can desynchronize the controlled dialog state when the popup is closed programmatically; consider calling setQrDialogOpen(false) there as well.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The QR polling effect depends on `handleConnect` but doesn't include it in the dependency array; consider wrapping `handleConnect` in `useCallback` and adding it as a dependency to avoid stale closures if its implementation changes.
- `closeQRCodePopup` resets `qrCode` and `pairingCode` but does not update `qrDialogOpen`, which can desynchronize the controlled dialog state when the popup is closed programmatically; consider calling `setQrDialogOpen(false)` there as well.

## Individual Comments

### Comment 1
<location path="src/pages/instance/DashboardInstance/index.tsx" line_range="102-110" />
<code_context>
   };

+  // 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]);
+
+  // Stop refreshing as soon as the instance is connected.
</code_context>
<issue_to_address>
**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:

```typescript
  // 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.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +102 to +110
useEffect(() => {
if (!qrDialogOpen || !instance) return;

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

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

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.

@michumichifu

Copy link
Copy Markdown
Author

Update: this is now running in production on our own deployment, built on top of main (95d27b4) and mounted over the Manager shipped in the evoapicloud/evolution-api:2.4.0-rc2 image.

Two things worth reporting from the real run.

It works. The QR image now changes on its own while the dialog stays open, and pairing succeeded on the first scan of an instance that had been failing all afternoon.

The stale-code problem was worse than the description suggests. While debugging an instance that would not pair, the server was already on its third code while the dialog still showed the first one:

{ instance: Mundo Veneco, pairingCode: null, qrcodeCount: 3 }

Every scan of that dialog was a scan of a code the server had discarded two rotations earlier, and the phone reported it as "Could not log in. Check your phone's internet connection" — which is why this went unnoticed for months on our side: the error message points at the phone, not at the code.

The polling does not open extra connections, measured rather than assumed. On a throwaway instance sitting in connecting, we counted the Baileys version: log lines (one per socket opened), fired five consecutive GET /instance/connect calls, and counted again:

sockets before the 5 calls: 1
  call 1: HTTP 200
  call 2: HTTP 200
  call 3: HTTP 200
  call 4: HTTP 200
  call 5: HTTP 200
sockets after the 5 calls:  1

Same socket throughout, which matches the connecting branch of connectToWhatsapp() returning the code already held in memory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants