From e9190e01d32ef99dab3d1efe983a1167aba04d37 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 06:37:40 +0000 Subject: [PATCH 1/2] fix(buttons): dynamic native_flow name and conditional bot node for WhatsApp Web CTA compatibility - Detect button types (CTA vs quick_reply) in nativeFlowMessage to set appropriate native_flow name attribute: '' for CTA-only, 'quick_reply' for quick_reply-only, 'mixed' for combinations - Skip bot node injection for CTA-only buttons (cta_url, cta_copy, cta_call) as the bot node prevents WhatsApp Web from rendering CTA buttons - Keep bot node for quick_reply buttons which need it for response handling https://claude.ai/code/session_01EK9NpViRCtda1WAvFd8ptR --- src/Socket/messages-send.ts | 51 ++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 27456ca5..4a971162 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -1284,6 +1284,33 @@ export const makeMessagesSocket = (config: SocketConfig) => { '[EXPERIMENTAL] Injected biz node for listMessage (legacy format)' ) } else { + // Determine the native_flow name based on actual button types + // WhatsApp Web requires correct name attribute to render buttons: + // - CTA-only buttons (cta_url, cta_copy, cta_call): name should be empty '' + // - quick_reply-only buttons: name should be 'quick_reply' + // - mixed (CTA + quick_reply): name should be 'mixed' + const CTA_BUTTON_NAMES = new Set(['cta_url', 'cta_copy', 'cta_call']) + const buttonNames = nativeFlowButtons.map((b: any) => b?.name).filter(Boolean) + const hasCTA = buttonNames.some((name: string) => CTA_BUTTON_NAMES.has(name)) + const hasQuickReply = buttonNames.some((name: string) => name === 'quick_reply') + + let nativeFlowName: string + if (hasCTA && !hasQuickReply) { + // Pure CTA buttons - WhatsApp Web needs empty name for CTA rendering + nativeFlowName = '' + } else if (hasQuickReply && !hasCTA) { + // Pure quick_reply buttons + nativeFlowName = 'quick_reply' + } else { + // Mixed CTA + quick_reply or other combinations + nativeFlowName = 'mixed' + } + + logger.info( + { msgId, buttonNames, hasCTA, hasQuickReply, nativeFlowName }, + '[EXPERIMENTAL] Determined native_flow name based on button types' + ) + // Use nested structure: biz > interactive > native_flow // For buttons, carousels, and other interactive messages const interactiveType = 'native_flow' @@ -1302,7 +1329,7 @@ export const makeMessagesSocket = (config: SocketConfig) => { tag: interactiveType, attrs: { v: '9', - name: 'mixed' + name: nativeFlowName } } ] @@ -1311,18 +1338,23 @@ export const makeMessagesSocket = (config: SocketConfig) => { }) } - // For private 1:1 chats, add bot node (required for some interactive messages to render) - // Only inject for actual user JIDs, not broadcasts, newsletters, or Meta AI bots - // IMPORTANT: Carousels and catalog messages should NOT have bot node - // as they are regular interactive messages, not bot messages - // NOTE: Only for regular JIDs, NOT hosted JIDs (to avoid interference with interactive messages) + // For private 1:1 chats, conditionally add bot node + // - quick_reply buttons need bot node for response handling + // - CTA-only buttons (cta_url, cta_copy, cta_call) should NOT have bot node + // as they are business actions and the bot node prevents WhatsApp Web rendering + // - Carousels and catalog messages should NOT have bot node const isPrivateUserChat = ( isPnUser(destinationJid) || isLidUser(destinationJid) || destinationJid?.endsWith('@c.us') ) && !isJidBot(destinationJid) - if (isPrivateUserChat && !isCarousel && !isCatalog && buttonType !== 'list') { + // Detect if message contains only CTA buttons (no quick_reply) + const ctaButtonNames = new Set(['cta_url', 'cta_copy', 'cta_call']) + const allButtonNames = nativeFlowButtons.map((b: any) => b?.name).filter(Boolean) + const isCTAOnly = allButtonNames.length > 0 && allButtonNames.every((name: string) => ctaButtonNames.has(name)) + + if (isPrivateUserChat && !isCarousel && !isCatalog && buttonType !== 'list' && !isCTAOnly) { ;(stanza.content as BinaryNode[]).push({ tag: 'bot', attrs: { biz_bot: '1' } @@ -1331,6 +1363,11 @@ export const makeMessagesSocket = (config: SocketConfig) => { { msgId, to: destinationJid }, '[EXPERIMENTAL] Added bot node for private chat interactive message' ) + } else if (isCTAOnly) { + logger.debug( + { msgId, to: destinationJid, buttonNames: allButtonNames }, + '[EXPERIMENTAL] Skipping bot node for CTA-only buttons (Web compatibility)' + ) } else if (isCarousel) { logger.debug( { msgId, to: destinationJid, buttonType }, From a64596202ff60a4771dfcf27067c3efc4b263bfb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 11:42:06 +0000 Subject: [PATCH 2/2] fix(buttons): use empty native_flow name for all regular buttons + fix scope issue - Changed native_flow name from 'mixed' to '' (empty) for all regular buttons (both CTA and quick_reply), matching WhatsApp client traffic analysis - Only special flows (payment_info, mpm, order_details) get specific names - Fixed variable scope: moved hasCTA/hasQuickReply/isCTAOnly before if/else block to ensure they're accessible for bot node conditional logic - Removed duplicate CTA_BUTTON_NAMES/allButtonNames declarations https://claude.ai/code/session_01EK9NpViRCtda1WAvFd8ptR --- src/Socket/messages-send.ts | 47 +++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 4a971162..49548462 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -1263,6 +1263,13 @@ export const makeMessagesSocket = (config: SocketConfig) => { metrics.interactiveMessagesSent.inc({ type: buttonType }) try { + // Classify button types for native_flow name and bot node decisions + const CTA_BUTTON_NAMES = new Set(['cta_url', 'cta_copy', 'cta_call']) + const allButtonNames = nativeFlowButtons.map((b: any) => b?.name).filter(Boolean) + const hasCTA = allButtonNames.some((name: string) => CTA_BUTTON_NAMES.has(name)) + const hasQuickReply = allButtonNames.some((name: string) => name === 'quick_reply') + const isCTAOnly = hasCTA && !hasQuickReply + // For listMessage (legacy format), use direct tag // This matches pastorini's working implementation if (buttonType === 'list') { @@ -1285,29 +1292,24 @@ export const makeMessagesSocket = (config: SocketConfig) => { ) } else { // Determine the native_flow name based on actual button types - // WhatsApp Web requires correct name attribute to render buttons: - // - CTA-only buttons (cta_url, cta_copy, cta_call): name should be empty '' - // - quick_reply-only buttons: name should be 'quick_reply' - // - mixed (CTA + quick_reply): name should be 'mixed' - const CTA_BUTTON_NAMES = new Set(['cta_url', 'cta_copy', 'cta_call']) - const buttonNames = nativeFlowButtons.map((b: any) => b?.name).filter(Boolean) - const hasCTA = buttonNames.some((name: string) => CTA_BUTTON_NAMES.has(name)) - const hasQuickReply = buttonNames.some((name: string) => name === 'quick_reply') - - let nativeFlowName: string - if (hasCTA && !hasQuickReply) { - // Pure CTA buttons - WhatsApp Web needs empty name for CTA rendering - nativeFlowName = '' - } else if (hasQuickReply && !hasCTA) { - // Pure quick_reply buttons - nativeFlowName = 'quick_reply' - } else { - // Mixed CTA + quick_reply or other combinations - nativeFlowName = 'mixed' + // Based on WhatsApp client traffic analysis (see getButtonArgs), + // the default name for regular native_flow buttons should be '' (empty) + // Only special flows (payment, mpm, order) need specific names + // Using '' for all regular buttons (CTA and quick_reply) ensures + // maximum compatibility with WhatsApp Web + const SPECIAL_FLOW_NAMES: Record = { + 'review_and_pay': 'payment_info', + 'payment_info': 'payment_info', + 'mpm': 'mpm', + 'review_order': 'order_details' } + const firstButtonName = allButtonNames[0] || '' + + // Check if this is a special flow type, otherwise use '' for Web compatibility + const nativeFlowName = SPECIAL_FLOW_NAMES[firstButtonName] || '' logger.info( - { msgId, buttonNames, hasCTA, hasQuickReply, nativeFlowName }, + { msgId, buttonNames: allButtonNames, hasCTA, hasQuickReply, isCTAOnly, nativeFlowName, firstButtonName }, '[EXPERIMENTAL] Determined native_flow name based on button types' ) @@ -1349,11 +1351,6 @@ export const makeMessagesSocket = (config: SocketConfig) => { destinationJid?.endsWith('@c.us') ) && !isJidBot(destinationJid) - // Detect if message contains only CTA buttons (no quick_reply) - const ctaButtonNames = new Set(['cta_url', 'cta_copy', 'cta_call']) - const allButtonNames = nativeFlowButtons.map((b: any) => b?.name).filter(Boolean) - const isCTAOnly = allButtonNames.length > 0 && allButtonNames.every((name: string) => ctaButtonNames.has(name)) - if (isPrivateUserChat && !isCarousel && !isCatalog && buttonType !== 'list' && !isCTAOnly) { ;(stanza.content as BinaryNode[]).push({ tag: 'bot',