From 79405f76c4c7239bff3caba0e68f2571febb3561 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 12 Aug 2026 13:28:33 +0530 Subject: [PATCH] feat(whatsapp): display click-to-chat ad referrals (#15424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WhatsApp click-to-chat ad referrals are already stored on incoming messages, but agents only see the customer’s text. This displays the originating ad preview directly in the message bubble, including available media, headline, body, and source link. Attribution identifiers remain hidden. ### Things to know - The card renders only when `content_attributes.referral` is present. - Invalid or non-HTTP media and source URLs fall back safely. - Existing messages without referral metadata are unchanged. ### How to test 1. Open a WhatsApp conversation containing an incoming message with `content_attributes.referral`. 2. Confirm the ad preview appears above the customer text. 3. Confirm the source opens in a new tab and missing or invalid media does not break the message bubble. Fixes https://linear.app/chatwoot/issue/CW-6206/add-whatsapp-ad-preview-support-in-chatwoot-inbox --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> --- .../components-next/message/Message.vue | 13 ++ .../message/bubbles/Text/WhatsappReferral.vue | 127 ++++++++++++++++++ .../components-next/message/provider.js | 10 ++ .../i18n/locale/en/conversation.json | 3 + 4 files changed, 153 insertions(+) create mode 100644 app/javascript/dashboard/components-next/message/bubbles/Text/WhatsappReferral.vue diff --git a/app/javascript/dashboard/components-next/message/Message.vue b/app/javascript/dashboard/components-next/message/Message.vue index f6dd35985..14efa4ab8 100644 --- a/app/javascript/dashboard/components-next/message/Message.vue +++ b/app/javascript/dashboard/components-next/message/Message.vue @@ -42,6 +42,7 @@ import CSATBubble from './bubbles/CSAT.vue'; import FormBubble from './bubbles/Form.vue'; import VoiceCallBubble from './bubbles/VoiceCall.vue'; import WhatsappFlowResponseBubble from './bubbles/WhatsappFlowResponse.vue'; +import WhatsappReferral from './bubbles/Text/WhatsappReferral.vue'; import MessageError from './MessageError.vue'; import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue'; @@ -370,6 +371,12 @@ const isMessageDeleted = computed(() => { return props.contentAttributes?.deleted; }); +const shouldShowWhatsappReferral = computed( + () => + variant.value === MESSAGE_VARIANTS.USER && + !!props.contentAttributes?.referral +); + const payloadForContextMenu = computed(() => { return { id: props.id, @@ -426,6 +433,7 @@ const shouldRenderMessage = computed(() => { isUnsupported || isAnIntegrationMessage || hasWhatsappFlowResponse || + shouldShowWhatsappReferral.value || isFailedMessage || hasExternalError ); @@ -580,9 +588,14 @@ provideMessageContext({ :class="{ 'ltr:ml-8 rtl:mr-8 justify-end': orientation === ORIENTATION.RIGHT, 'ltr:mr-8 rtl:ml-8': orientation === ORIENTATION.LEFT, + 'flex-col items-start gap-2': shouldShowWhatsappReferral, }" @contextmenu="openContextMenu($event)" > + +import { computed, ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import Icon from 'next/icon/Icon.vue'; + +const props = defineProps({ + referral: { type: Object, required: true }, +}); + +const { t } = useI18n(); +const hasMediaError = ref(false); + +const safeUrl = value => { + if (!value) return ''; + + try { + const url = new URL(value); + return ['http:', 'https:'].includes(url.protocol) ? url.toString() : ''; + } catch { + return ''; + } +}; + +const sourceUrl = computed(() => safeUrl(props.referral.sourceUrl)); +const mediaUrl = computed(() => + safeUrl( + props.referral.thumbnailUrl || + props.referral.imageUrl || + props.referral.mediaUrl || + props.referral.videoUrl + ) +); +const showMedia = computed(() => mediaUrl.value && !hasMediaError.value); +const isVideo = computed( + () => + props.referral.mediaType === 'video' || + props.referral.mediaContentType?.startsWith('video/') +); +const renderVideo = computed( + () => isVideo.value && !props.referral.thumbnailUrl +); + +const handleMediaError = () => { + hasMediaError.value = true; +}; + + + diff --git a/app/javascript/dashboard/components-next/message/provider.js b/app/javascript/dashboard/components-next/message/provider.js index a8523296b..7c4dc8b3e 100644 --- a/app/javascript/dashboard/components-next/message/provider.js +++ b/app/javascript/dashboard/components-next/message/provider.js @@ -68,6 +68,16 @@ const MessageControl = Symbol('MessageControl'); * @property {string|null} [bccEmail] - BCC email addresses * @property {Object} [whatsappFlowResponse] - WhatsApp Flow response metadata * @property {Record|string} [whatsappFlowResponse.responseJson] - Structured fields or the raw response submitted by the contact + * @property {Object} [referral] - WhatsApp click-to-chat ad metadata + * @property {string} [referral.sourceUrl] - URL of the originating ad + * @property {string} [referral.headline] - Headline of the originating ad + * @property {string} [referral.body] - Body copy of the originating ad + * @property {string} [referral.mediaType] - Media type of the originating ad + * @property {string} [referral.thumbnailUrl] - Thumbnail of the originating ad + * @property {string} [referral.imageUrl] - Image URL of a Cloud API referral + * @property {string} [referral.videoUrl] - Video URL of a Cloud API referral + * @property {string} [referral.mediaUrl] - Media URL of a Twilio referral + * @property {string} [referral.mediaContentType] - Media content type of a Twilio referral */ /** diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index be759c7c4..4620fc18e 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -67,6 +67,9 @@ "UNSUPPORTED_MESSAGE_TIKTOK": "This message is unsupported. You can view this message on the TikTok app.", "UNSUPPORTED_MESSAGE_WHATSAPP": "This message is unsupported. You can view this message on the WhatsApp app.", "WHATSAPP_FLOW_RESPONSE": "Submitted a flow response", + "WHATSAPP_REFERRAL": { + "AD": "Ad" + }, "SUCCESS_DELETE_MESSAGE": "Message deleted successfully", "FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again", "NO_RESPONSE": "No response",