feat(whatsapp): display click-to-chat ad referrals (#15424)
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>
This commit is contained in:
@@ -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)"
|
||||
>
|
||||
<WhatsappReferral
|
||||
v-if="shouldShowWhatsappReferral"
|
||||
:referral="contentAttributes.referral"
|
||||
/>
|
||||
<Component :is="componentToRender" />
|
||||
</div>
|
||||
<MessageError
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup>
|
||||
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;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full max-w-lg overflow-hidden border rounded-lg border-n-weak bg-n-alpha-1"
|
||||
data-testid="whatsapp-referral"
|
||||
>
|
||||
<a
|
||||
v-if="sourceUrl"
|
||||
:href="sourceUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="block text-current no-underline hover:no-underline"
|
||||
>
|
||||
<div
|
||||
v-if="showMedia"
|
||||
class="relative overflow-hidden bg-n-alpha-2 aspect-video"
|
||||
>
|
||||
<video
|
||||
v-if="renderVideo"
|
||||
:src="mediaUrl"
|
||||
muted
|
||||
preload="metadata"
|
||||
class="object-cover w-full h-full skip-context-menu"
|
||||
@error="handleMediaError"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
:src="mediaUrl"
|
||||
:alt="referral.headline || t('CONVERSATION.WHATSAPP_REFERRAL.AD')"
|
||||
class="object-cover w-full h-full skip-context-menu"
|
||||
@error="handleMediaError"
|
||||
/>
|
||||
<div
|
||||
v-if="isVideo"
|
||||
class="absolute inset-0 flex items-center justify-center pointer-events-none bg-n-alpha-black2"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center rounded-full shadow-md size-10 bg-n-solid-1"
|
||||
>
|
||||
<Icon icon="i-lucide-play" class="size-4 ms-0.5 text-n-slate-12" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="flex items-center gap-1 text-xs text-n-slate-11">
|
||||
<span>{{ t('CONVERSATION.WHATSAPP_REFERRAL.AD') }}</span>
|
||||
<Icon icon="i-lucide-external-link" class="size-3" />
|
||||
</div>
|
||||
<p
|
||||
v-if="referral.headline"
|
||||
class="mt-1 mb-0 font-medium break-words text-n-slate-12"
|
||||
>
|
||||
{{ referral.headline }}
|
||||
</p>
|
||||
<p
|
||||
v-if="referral.body"
|
||||
class="mt-1 mb-0 whitespace-pre-line break-words line-clamp-3 text-n-slate-11"
|
||||
>
|
||||
{{ referral.body }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<div v-else class="p-3">
|
||||
<div class="text-xs text-n-slate-11">
|
||||
{{ t('CONVERSATION.WHATSAPP_REFERRAL.AD') }}
|
||||
</div>
|
||||
<p
|
||||
v-if="referral.headline"
|
||||
class="mt-1 mb-0 font-medium break-words text-n-slate-12"
|
||||
>
|
||||
{{ referral.headline }}
|
||||
</p>
|
||||
<p
|
||||
v-if="referral.body"
|
||||
class="mt-1 mb-0 whitespace-pre-line break-words line-clamp-3 text-n-slate-11"
|
||||
>
|
||||
{{ referral.body }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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, unknown>|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
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user