Skip to content

fix: interactive buttons not rendering (viewOnceMessage → relayMessage)#2651

Open
n8nfelipe wants to merge 1 commit into
evolution-foundation:mainfrom
n8nfelipe:fix/interactive-buttons
Open

fix: interactive buttons not rendering (viewOnceMessage → relayMessage)#2651
n8nfelipe wants to merge 1 commit into
evolution-foundation:mainfrom
n8nfelipe:fix/interactive-buttons

Conversation

@n8nfelipe

@n8nfelipe n8nfelipe commented Jul 18, 2026

Copy link
Copy Markdown

Problem

Interactive buttons (reply, URL, copy, call, PIX) are not rendering in WhatsApp messages. The message arrives but buttons don't appear.

Root Cause

The buttonMessage() function in whatsapp.baileys.service.ts was wrapping the content in viewOnceMessage, which does not support interactive buttons.

Solution

Replaced viewOnceMessage wrapper with direct interactiveMessage and used relayMessage with proper additionalNodes:

  • biz node with interactive and native_flow children
  • bot node for private chats (biz_bot: '1')
  • generateMessageIDV2 for proper message ID generation

Changes

  • src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Testing

  1. Send a message with interactive buttons (reply, URL, copy)
  2. Verify buttons render correctly on the recipient's phone
  3. Test with both private and group chats

Summary by Sourcery

Fix WhatsApp interactive button messages by sending native interactive messages via relayMessage with proper business and bot metadata.

Bug Fixes:

  • Ensure WhatsApp interactive buttons (including PIX) render correctly by avoiding view-once wrapping that breaks native flow interactions.

Enhancements:

  • Validate WhatsApp numbers before sending interactive messages and handle message relaying with generated IDs and additional biz/bot nodes for correct delivery semantics.

- Replaced viewOnceMessage wrapper with direct interactiveMessage
- Used relayMessage with additionalNodes (biz/interactive/native_flow/bot)
- Added generateMessageIDV2 import from baileys
- Buttons now render correctly on WhatsApp clients

Fixes evolution-foundation#1234
@sourcery-ai

sourcery-ai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors WhatsApp interactive button sending to stop wrapping messages in viewOnceMessage and instead directly build and relay interactiveMessage with correct biz/native_flow metadata and message IDs, adding validation of the target JID and custom relayMessage handling.

Sequence diagram for updated WhatsApp interactiveMessage relay flow

sequenceDiagram
  actor User
  participant Api as ApiServer
  participant BS as BaileysStartupService
  participant WA as WhatsAppClient

  User->>Api: HTTP sendInteractiveButtons
  Api->>BS: buttonMessage(data)
  BS->>BS: whatsappNumber({ numbers })
  BS-->>BS: validate isWA.exists / isJidGroup / broadcast
  alt data.delay
    BS->>WA: presenceSubscribe(sender)
    BS->>WA: sendPresenceUpdate(composing, sender)
    BS->>BS: delay(data.delay)
    BS->>WA: sendPresenceUpdate(paused, sender)
  end
  opt data.thumbnailUrl
    BS->>BS: prepareMediaMessage({ mediatype, media })
  end
  BS->>BS: build interactiveMessage
  BS->>BS: generateMessageIDV2()
  BS->>BS: generateWAMessageFromContent(sender, message, options)
  BS->>BS: build additionalNodes(biz, interactive, native_flow, bot)
  BS->>WA: relayMessage(sender, message, { messageId, additionalNodes })
  WA-->>BS: id
  BS-->>Api: WAMessage (interactive buttons)
  Api-->>User: HTTP 200 (buttons rendered)
Loading

File-Level Changes

Change Details Files
Send WhatsApp interactive buttons using direct interactiveMessage + relayMessage with proper biz/native_flow nodes instead of viewOnceMessage wrapper.
  • Remove viewOnceMessage wrapper around interactiveMessage and construct a standalone interactiveMessage payload for button messages
  • Reuse prepared media (imageMessage) to build interactiveMessage header with media attachment when present
  • Generate nativeFlow buttons array and messageParamsJson with UUID-based templateId
  • Create proto.IMessage with top-level interactiveMessage rather than nested inside viewOnceMessage
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Add JID existence validation and custom relayMessage flow including typing simulation, quoting, and additional nodes for business/native_flow and bot metadata.
  • Validate WhatsApp number via whatsappNumber and reject non-existing, non-group, non-broadcast JIDs with BadRequestException
  • Simulate typing (presence composing/paused) manually when delay is provided instead of using sendMessageWithTyping helper
  • Resolve quoted messages via getMessage when a quoted key is provided and attach to generated message
  • Generate message IDs using generateMessageIDV2 and send via client.relayMessage with additional biz/interactive/native_flow nodes and bot node for 1:1 chats
  • Normalize the generated message key (id, remoteJid, participant, fromMe) and strip empty properties before returning, with error logging and wrapping in InternalServerErrorException
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Possibly linked issues

  • #: PR changes buttonMessage to use interactiveMessage with relayMessage, directly addressing broken interactive buttons reported in the issue.
  • #(unknown): PR changes message construction and delivery so WhatsApp interactive buttons render, directly addressing the issue’s question.

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
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The (isArray(value) && value.length) === 0 check in the cleanup loop is likely incorrect due to operator precedence and will never behave as intended; it should be split or rewritten as isArray(value) && value.length === 0.
  • The new const isWA = (await this.whatsappNumber({ numbers: [data.number] }))?.shift(); line can return undefined when the number lookup fails, causing isWA.exists and isWA.jid to throw; add a guard for !isWA before accessing its properties.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `(isArray(value) && value.length) === 0` check in the cleanup loop is likely incorrect due to operator precedence and will never behave as intended; it should be split or rewritten as `isArray(value) && value.length === 0`.
- The new `const isWA = (await this.whatsappNumber({ numbers: [data.number] }))?.shift();` line can return `undefined` when the number lookup fails, causing `isWA.exists` and `isWA.jid` to throw; add a guard for `!isWA` before accessing its properties.

## Individual Comments

### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="3343-3344" />
<code_context>
-        mentionsEveryOne: data?.mentionsEveryOne,
-        mentioned: data?.mentioned,
-      });
+    const isWA = (await this.whatsappNumber({ numbers: [data.number] }))?.shift();
+    if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) {
+      throw new BadRequestException(isWA);
     }
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against `isWA` being undefined before accessing its properties.

Because `whatsappNumber` is optional-chained, `isWA` can be `undefined` (e.g. empty result), and `isWA.exists` / `isWA.jid` will then throw. Add a guard before using its properties, for example:

```ts
const isWA = (await this.whatsappNumber({ numbers: [data.number] }))?.shift();
if (!isWA) {
  // handle missing lookup
}
if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) {
  throw new BadRequestException(isWA);
}
```
</issue_to_address>

### Comment 2
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="3458-3459" />
<code_context>
+        fromMe: true,
+      };
+
+      for (const [key, value] of Object.entries(m)) {
+        if (!value || (isArray(value) && value.length) === 0) {
+          delete m[key];
+        }
</code_context>
<issue_to_address>
**issue (bug_risk):** The cleanup condition will incorrectly delete most non-array properties from `m`.

`(isArray(value) && value.length) === 0` is evaluated as `((isArray(value) && value.length) === 0)`. For non-arrays, `isArray(value)` is `false`, so the left side is `false`, and `false === 0` is `true`, causing deletion even when `value` is truthy. You probably want `isArray(value) && value.length === 0`, and ideally a more explicit condition that only deletes empty arrays rather than all falsy values.
</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 +3343 to +3344
const isWA = (await this.whatsappNumber({ numbers: [data.number] }))?.shift();
if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Guard against isWA being undefined before accessing its properties.

Because whatsappNumber is optional-chained, isWA can be undefined (e.g. empty result), and isWA.exists / isWA.jid will then throw. Add a guard before using its properties, for example:

const isWA = (await this.whatsappNumber({ numbers: [data.number] }))?.shift();
if (!isWA) {
  // handle missing lookup
}
if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) {
  throw new BadRequestException(isWA);
}

Comment on lines +3458 to +3459
for (const [key, value] of Object.entries(m)) {
if (!value || (isArray(value) && value.length) === 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): The cleanup condition will incorrectly delete most non-array properties from m.

(isArray(value) && value.length) === 0 is evaluated as ((isArray(value) && value.length) === 0). For non-arrays, isArray(value) is false, so the left side is false, and false === 0 is true, causing deletion even when value is truthy. You probably want isArray(value) && value.length === 0, and ideally a more explicit condition that only deletes empty arrays rather than all falsy values.

@dpaes

dpaes commented Jul 20, 2026

Copy link
Copy Markdown

send to develop (we dont accept anything to main) @n8nfelipe

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