From 99997a701a18cf8db137d735dade1b3c6d5e7f02 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 29 Aug 2025 16:13:25 +0530 Subject: [PATCH] feat: Add twilio content templates (#12277) Implements comprehensive Twilio WhatsApp content template support (Phase 1) enabling text, media, and quick reply templates with proper parameter conversion, sync capabilities, and feature flag protection. ### Features Implemented **Template Types Supported** - Basic Text Templates: Simple text with variables ({{1}}, {{2}}) - Media Templates: Image/Video/Document templates with text variables - Quick Reply Templates: Interactive button templates - Phase 2 (Future): List Picker, Call-to-Action, Catalog, Carousel, Authentication templates **Template Synchronization** - API Endpoint: POST /api/v1/accounts/{account_id}/inboxes/{inbox_id}/sync_templates - Background Job: Channels::Twilio::TemplatesSyncJob - Storage: JSONB format in channel_twilio_sms.content_templates - Auto-categorization: UTILITY, MARKETING, AUTHENTICATION categories ### Template Examples Tested #### Text template ``` { "name": "greet", "language": "en" } ``` #### Template with variables ``` { "name": "order_status", "parameters": [{"type": "body", "parameters": [{"text": "John"}]}] } ``` #### Media template with image ``` { "name": "product_showcase", "parameters": [ {"type": "header", "parameters": [{"image": {"link": "image.jpg"}}]}, {"type": "body", "parameters": [{"text": "iPhone"}, {"text": "$999"}]} ]} ``` #### Preview CleanShot 2025-08-26 at 10 01
51@2x CleanShot 2025-08-26 at 10 02
02@2x #### User guide https://www.chatwoot.com/hc/user-guide/articles/1756195741-twilio-content-templates --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../components/ActionButtons.vue | 34 ++- .../components/ComposeNewConversationForm.vue | 29 +- .../components/ContentTemplateForm.vue | 56 ++++ .../components/ContentTemplateSelector.vue | 124 ++++++++ .../helpers/composeConversationHelper.js | 3 +- .../ContentTemplateParser.vue | 278 ++++++++++++++++++ .../widgets/WootWriter/ReplyBottomPanel.vue | 16 +- .../ContentTemplatesModal.vue | 97 ++++++ .../ContentTemplatesPicker.vue | 169 +++++++++++ .../widgets/conversation/ReplyBox.vue | 29 ++ app/javascript/dashboard/helper/URLHelper.js | 20 ++ .../dashboard/helper/specs/URLHelper.spec.js | 55 ++++ .../dashboard/i18n/locale/en/contact.json | 9 + .../i18n/locale/en/contentTemplates.json | 51 ++++ .../dashboard/i18n/locale/en/index.js | 2 + .../inbox/settingsPage/ConfigurationPage.vue | 13 + app/javascript/shared/constants/messages.js | 6 + app/services/twilio/template_sync_service.rb | 33 ++- config/features.yml | 1 + 19 files changed, 1001 insertions(+), 24 deletions(-) create mode 100644 app/javascript/dashboard/components-next/NewConversation/components/ContentTemplateForm.vue create mode 100644 app/javascript/dashboard/components-next/NewConversation/components/ContentTemplateSelector.vue create mode 100644 app/javascript/dashboard/components-next/content-templates/ContentTemplateParser.vue create mode 100644 app/javascript/dashboard/components/widgets/conversation/ContentTemplates/ContentTemplatesModal.vue create mode 100644 app/javascript/dashboard/components/widgets/conversation/ContentTemplates/ContentTemplatesPicker.vue create mode 100644 app/javascript/dashboard/i18n/locale/en/contentTemplates.json diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index 6f2178408..773ebe315 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -11,12 +11,14 @@ import { extractTextFromMarkdown } from 'dashboard/helper/editorHelper'; import Button from 'dashboard/components-next/button/Button.vue'; import WhatsAppOptions from './WhatsAppOptions.vue'; +import ContentTemplateSelector from './ContentTemplateSelector.vue'; const props = defineProps({ attachedFiles: { type: Array, default: () => [] }, isWhatsappInbox: { type: Boolean, default: false }, isEmailOrWebWidgetInbox: { type: Boolean, default: false }, isTwilioSmsInbox: { type: Boolean, default: false }, + isTwilioWhatsAppInbox: { type: Boolean, default: false }, messageTemplates: { type: Array, default: () => [] }, channelType: { type: String, default: '' }, isLoading: { type: Boolean, default: false }, @@ -32,6 +34,7 @@ const emit = defineEmits([ 'discard', 'sendMessage', 'sendWhatsappMessage', + 'sendTwilioMessage', 'insertEmoji', 'addSignature', 'removeSignature', @@ -63,6 +66,20 @@ const sendWithSignature = computed(() => { return fetchSignatureFlagFromUISettings(props.channelType); }); +const showTwilioContentTemplates = computed(() => { + return props.isTwilioWhatsAppInbox && props.inboxId; +}); + +const shouldShowEmojiButton = computed(() => { + return ( + !props.isWhatsappInbox && !props.isTwilioWhatsAppInbox && !props.hasNoInbox + ); +}); + +const isRegularMessageMode = computed(() => { + return !props.isWhatsappInbox && !props.isTwilioWhatsAppInbox; +}); + const setSignature = () => { if (signatureToApply.value) { if (sendWithSignature.value) { @@ -125,7 +142,7 @@ const keyboardEvents = { action: () => { if ( isEditorHotKeyEnabled('enter') && - !props.isWhatsappInbox && + isRegularMessageMode.value && !props.isDropdownActive ) { emit('sendMessage'); @@ -136,7 +153,7 @@ const keyboardEvents = { action: () => { if ( isEditorHotKeyEnabled('cmd_enter') && - !props.isWhatsappInbox && + isRegularMessageMode.value && !props.isDropdownActive ) { emit('sendMessage'); @@ -158,8 +175,13 @@ useKeyboardEvents(keyboardEvents); :message-templates="messageTemplates" @send-message="emit('sendWhatsappMessage', $event)" /> +
@@ -172,7 +194,7 @@ useKeyboardEvents(keyboardEvents); />
@@ -199,7 +221,7 @@ useKeyboardEvents(keyboardEvents); />