WhatsApp Flow submissions currently reach Chatwoot as `interactive.nfm_reply` messages, but their structured answers are discarded. Agents see an empty message and outbound webhooks cannot access the submitted data. This change preserves the Flow response in message content attributes and renders a readable response card in the conversation. Existing WhatsApp messages and other interactive message types remain unchanged. Fixes https://github.com/chatwoot/chatwoot/issues/13970 <img width="1234" height="1350" alt="CleanShot 2026-08-04 at 00 40 14@2x" src="https://github.com/user-attachments/assets/884b3a5c-bab3-4196-8037-776e6c41ab2a" /> ### How to reproduce 1. Send an approved WhatsApp template containing a Flow button. 2. Complete and submit the Flow from WhatsApp. 3. Open the conversation in Chatwoot. 4. Observe that the incoming message has no visible content and the submitted fields are absent from the message webhook. ### What changed - Parse `interactive.nfm_reply.response_json` for incoming WhatsApp Cloud messages. - Store the Flow name, body, and structured response under `content_attributes.whatsapp_flow_response`. - Add a dedicated conversation bubble that formats submitted field names and values. - Preserve original response keys while converting message attributes to the frontend shape. - Keep `flow_token` available in stored/webhook data while omitting it from the agent-facing card. - Add focused backend, helper, and component regression coverage. ### How to test 1. Send a WhatsApp Flow template to a contact. 2. Submit the Flow with multiple field types, including free text and a selection. 3. Confirm the incoming conversation message displays each submitted field and value. 4. Confirm an empty-answer Flow still renders a visible “Submitted a flow response” message. 5. Inspect the `message_created` webhook and confirm the structured response is present in `content_attributes.whatsapp_flow_response.response_json`. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
107 lines
2.9 KiB
Ruby
107 lines
2.9 KiB
Ruby
module Whatsapp::IncomingMessageServiceHelpers
|
|
def download_attachment_file(attachment_payload)
|
|
Down.download(inbox.channel.media_url(attachment_payload[:id]), headers: inbox.channel.api_headers)
|
|
end
|
|
|
|
def conversation_params
|
|
{
|
|
account_id: @inbox.account_id,
|
|
inbox_id: @inbox.id,
|
|
contact_id: @contact.id,
|
|
contact_inbox_id: @contact_inbox.id
|
|
}
|
|
end
|
|
|
|
def processed_params
|
|
@processed_params ||= params
|
|
end
|
|
|
|
def account
|
|
@account ||= inbox.account
|
|
end
|
|
|
|
def message_type
|
|
messages_data.first[:type]
|
|
end
|
|
|
|
def message_content(message)
|
|
return I18n.t('conversations.messages.whatsapp.flow_response') if message.dig(:interactive, :nfm_reply).present?
|
|
|
|
# TODO: map interactive messages back to button messages in chatwoot
|
|
message.dig(:text, :body) ||
|
|
message.dig(:button, :text) ||
|
|
message.dig(:interactive, :button_reply, :title) ||
|
|
message.dig(:interactive, :list_reply, :title) ||
|
|
message.dig(:name, :formatted_name)
|
|
end
|
|
|
|
def parse_flow_response_json(response_json)
|
|
parsed_response = JSON.parse(response_json)
|
|
parsed_response.is_a?(Hash) ? parsed_response : response_json
|
|
rescue JSON::ParserError, TypeError
|
|
response_json
|
|
end
|
|
|
|
def file_content_type(file_type)
|
|
return :image if %w[image sticker].include?(file_type)
|
|
return :audio if %w[audio voice].include?(file_type)
|
|
return :video if ['video'].include?(file_type)
|
|
return :location if ['location'].include?(file_type)
|
|
return :contact if ['contacts'].include?(file_type)
|
|
|
|
:file
|
|
end
|
|
|
|
def unprocessable_message_type?(message_type)
|
|
%w[reaction ephemeral request_welcome].include?(message_type)
|
|
end
|
|
|
|
def processed_waid(waid)
|
|
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider(waid, :cloud)
|
|
end
|
|
|
|
def whatsapp_phone_number(identifier)
|
|
identifier = identifier.to_s
|
|
return if identifier.blank?
|
|
return unless identifier.match?(/\A\d{1,15}\z/)
|
|
|
|
identifier
|
|
end
|
|
|
|
def error_webhook_event?(message)
|
|
message.key?('errors')
|
|
end
|
|
|
|
def log_error(message)
|
|
Rails.logger.warn "Whatsapp Error: #{message['errors'][0]['title']} - contact: #{message['from']}"
|
|
end
|
|
|
|
def process_in_reply_to(message)
|
|
@in_reply_to_external_id = message['context']&.[]('id')
|
|
return if @in_reply_to_external_id.blank?
|
|
|
|
@in_reply_to_message_id = Whatsapp::InReplyToMessageFinder.new(
|
|
conversation: @conversation,
|
|
source_id: @in_reply_to_external_id
|
|
).perform&.id
|
|
end
|
|
|
|
def referral_attributes(message)
|
|
return {} if outgoing_echo
|
|
|
|
message[:referral]&.to_h&.deep_stringify_keys || {}
|
|
end
|
|
|
|
def find_message_by_source_id(source_id)
|
|
return unless source_id
|
|
|
|
@message = Message.find_by(source_id: source_id)
|
|
end
|
|
|
|
def lock_message_source_id!
|
|
return false if messages_data.blank?
|
|
|
|
Whatsapp::MessageDedupLock.new(messages_data.first[:id]).acquire!
|
|
end
|
|
end
|