+
diff --git a/app/javascript/dashboard/components-next/template-preview/WhatsAppTextTemplate.vue b/app/javascript/dashboard/components-next/template-preview/WhatsAppTextTemplate.vue
new file mode 100644
index 000000000..5e1d6f2de
--- /dev/null
+++ b/app/javascript/dashboard/components-next/template-preview/WhatsAppTextTemplate.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+ {{ message.title }}
+
+
+
+
+
+
+
+ {{ message.footer }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/template-preview/index.js b/app/javascript/dashboard/components-next/template-preview/index.js
new file mode 100644
index 000000000..b7f1bd6b0
--- /dev/null
+++ b/app/javascript/dashboard/components-next/template-preview/index.js
@@ -0,0 +1,6 @@
+// Main Template Preview Component
+export { default as TemplatePreview } from './TemplatePreview.vue';
+
+// Core Services
+export { TemplateTypeDetector } from 'dashboard/services/TemplateTypeDetector';
+export { TemplateNormalizer } from 'dashboard/services/TemplateNormalizer';
diff --git a/app/javascript/dashboard/components-next/template-preview/templates/twilio-templates.js b/app/javascript/dashboard/components-next/template-preview/templates/twilio-templates.js
new file mode 100644
index 000000000..a7857b27d
--- /dev/null
+++ b/app/javascript/dashboard/components-next/template-preview/templates/twilio-templates.js
@@ -0,0 +1,209 @@
+export const twilioTemplates = [
+ {
+ body: 'Introducing our latest release the {{1}}! Available now for just {{2}}. Be among the first to own this style. Limited stock available!',
+ types: {
+ 'twilio/media': {
+ body: 'Introducing our latest release the {{1}}! Available now for just {{2}}. Be among the first to own this style. Limited stock available!',
+ media: ['https://vite-five-phi.vercel.app/jordan-shoes.jpg'],
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en',
+ variables: {
+ 1: 'Jordan',
+ 2: '100$',
+ },
+ content_sid: 'HX4b1ff075f097ccdf7f274b6af4d7be02',
+ friendly_name: 'shoe_launch',
+ template_type: 'media',
+ },
+ {
+ body: 'Introducing our latest release the {{1}}! Available now for just {{2}}. Be among the first to own this style. Limited stock available!',
+ types: {
+ 'twilio/media': {
+ body: 'Introducing our latest release the {{1}}! Available now for just {{2}}. Be among the first to own this style. Limited stock available!',
+ media: ['https://vite-five-phi.vercel.app/jordan-shoes.jpg'],
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en',
+ variables: {
+ 1: 'Jordan',
+ 2: '400$',
+ },
+ content_sid: 'HXd5c1f8f8d68976f841c440d5e4b46c2e',
+ friendly_name: 'product_launch_custom_price',
+ template_type: 'media',
+ },
+ {
+ body: 'Introducing our latest release the Nike Air Force! Available now for just $129.99',
+ types: {
+ 'twilio/media': {
+ body: 'Introducing our latest release the Nike Air Force! Available now for just $129.99',
+ media: ['https://vite-five-phi.vercel.app/jordan-shoes.jpg'],
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en',
+ variables: {},
+ content_sid: 'HX25f6e823f2416ca4b34254d98e916fae',
+ friendly_name: 'product_launch',
+ template_type: 'media',
+ },
+ {
+ body: 'Hey {{1}}, how may I help you?',
+ types: {
+ 'twilio/text': {
+ body: 'Hey {{1}}, how may I help you?',
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en',
+ variables: {
+ 1: 'John',
+ },
+ content_sid: 'HXee240fd3a8b5045dba057feda5173e55',
+ friendly_name: 'greet',
+ template_type: 'text',
+ },
+ {
+ body: "Hi {{1}} Thanks for placing an order with us. We'll let you know once your order has been processed and delivered. Your order number is {{3}}. Thanks",
+ types: {
+ 'twilio/text': {
+ body: "Hi {{1}} Thanks for placing an order with us. We'll let you know once your order has been processed and delivered. Your order number is {{3}}. Thanks",
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en',
+ variables: {
+ 1: 'John',
+ 3: '12345',
+ },
+ content_sid: 'HX88291ef8d30d7dcd436cbb9b21c236f4',
+ friendly_name: 'order_status',
+ template_type: 'text',
+ },
+ {
+ body: 'Welcome and congratulations!! This message demonstrates your ability to send a WhatsApp message notification from the Cloud API, hosted by Meta. Thank you for taking the time to test with us.',
+ types: {
+ 'twilio/text': {
+ body: 'Welcome and congratulations!! This message demonstrates your ability to send a WhatsApp message notification from the Cloud API, hosted by Meta. Thank you for taking the time to test with us.',
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en',
+ variables: {},
+ content_sid: 'HXdc6da32d489ee80f67c07d5bb0e7e390',
+ friendly_name: 'hello_world',
+ template_type: 'text',
+ },
+ {
+ body: 'Thanks for reaching out to us. Before we proceed, we would like to get some information from you to assist you better. To whom would you like to connect?',
+ types: {
+ 'twilio/quick-reply': {
+ body: 'Thanks for reaching out to us. Before we proceed, we would like to get some information from you to assist you better. To whom would you like to connect?',
+ actions: [
+ {
+ id: 'Sales_payload',
+ title: 'Sales',
+ },
+ {
+ id: 'Support_payload',
+ title: 'Support',
+ },
+ ],
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en_US',
+ variables: {},
+ content_sid: 'HX778677f5867f96175ab4b7efb9a5bee6',
+ friendly_name: 'welcome_message_new',
+ template_type: 'quick_reply',
+ },
+ {
+ body: 'What type of Chatwoot installation are you using? Select "Chatwoot Cloud" if you are using app.chatwoot.com, otherwise select "Self-hosted Chatwoot".',
+ types: {
+ 'twilio/quick-reply': {
+ body: 'What type of Chatwoot installation are you using? Select "Chatwoot Cloud" if you are using app.chatwoot.com, otherwise select "Self-hosted Chatwoot".',
+ actions: [
+ {
+ id: 'Chatwoot Cloud_payload',
+ title: 'Chatwoot Cloud',
+ },
+ {
+ id: 'Self-hosted Chatwoot_payload',
+ title: 'Self-hosted Chatwoot',
+ },
+ ],
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en_US',
+ variables: {},
+ content_sid: 'HX3a35e5cd76529fd91d19341deb4ef685',
+ friendly_name: 'saas_whatsapp_question',
+ template_type: 'quick_reply',
+ },
+ {
+ body: 'Thanks for reaching out to us. Happy to help. What are you looking for?',
+ types: {
+ 'twilio/quick-reply': {
+ body: 'Thanks for reaching out to us. Happy to help. What are you looking for?',
+ actions: [
+ {
+ id: 'Support_payload',
+ title: 'Support',
+ },
+ {
+ id: 'Sales_payload',
+ title: 'Sales',
+ },
+ {
+ id: 'Demo_payload',
+ title: 'Demo',
+ },
+ ],
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en_US',
+ variables: {},
+ content_sid: 'HX5d8e09f96cee2f7fb7bab223c03cb0a1',
+ friendly_name: 'welcome_message',
+ template_type: 'quick_reply',
+ },
+ {
+ types: {
+ 'twilio/call-to-action': {
+ actions: [
+ {
+ id: null,
+ title: 'Pay now',
+ type: 'URL',
+ url: 'https://payments.example.com/pay',
+ },
+ ],
+ body: 'Hello, this is a gentle reminder regarding your course fee.\n\nThe payment is due on {{date}}.\nKindly complete the payment at your convenience',
+ },
+ },
+ status: 'approved',
+ category: 'utility',
+ language: 'en',
+ variables: {
+ date: '01-Jan-2026',
+ },
+ content_sid: 'HX63e56fc142aad670f320bf400d5bfeb7',
+ friendly_name: 'course_fee_reminder',
+ template_type: 'call_to_action',
+ },
+];
diff --git a/app/javascript/dashboard/components-next/template-preview/templates/whatsapp-templates.js b/app/javascript/dashboard/components-next/template-preview/templates/whatsapp-templates.js
new file mode 100644
index 000000000..fc70e3230
--- /dev/null
+++ b/app/javascript/dashboard/components-next/template-preview/templates/whatsapp-templates.js
@@ -0,0 +1,408 @@
+export const whatsAppTemplates = [
+ {
+ id: '1381151706284063',
+ name: 'event_invitation_static',
+ status: 'APPROVED',
+ category: 'MARKETING',
+ language: 'en',
+ components: [
+ {
+ text: "You're invited to {{event_name}} at {{location}}, Join us for an amazing experience!",
+ type: 'BODY',
+ example: {
+ body_text_named_params: [
+ {
+ example: 'F1',
+ param_name: 'event_name',
+ },
+ {
+ example: 'Dubai',
+ param_name: 'location',
+ },
+ ],
+ },
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ url: 'https://events.example.com/register',
+ text: 'Visit website',
+ type: 'URL',
+ },
+ {
+ url: 'https://maps.app.goo.gl/YoWAzRj1GDuxs6qz8',
+ text: 'Get Directions',
+ type: 'URL',
+ },
+ ],
+ },
+ ],
+ sub_category: 'CUSTOM',
+ parameter_format: 'NAMED',
+ },
+ {
+ id: '767076159336759',
+ name: 'purchase_receipt',
+ status: 'APPROVED',
+ category: 'UTILITY',
+ language: 'en_US',
+ components: [
+ {
+ type: 'HEADER',
+ format: 'DOCUMENT',
+ example: {
+ header_handle: [
+ 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
+ ],
+ },
+ },
+ {
+ text: 'Thank you for using your {{1}} card at {{2}}. Your {{3}} is attached as a PDF.',
+ type: 'BODY',
+ example: {
+ body_text: [['credit', 'CS Mutual', 'receipt']],
+ },
+ },
+ ],
+ parameter_format: 'POSITIONAL',
+ },
+ {
+ id: '1469258364071127',
+ name: 'discount_coupon',
+ status: 'APPROVED',
+ category: 'MARKETING',
+ language: 'en',
+ components: [
+ {
+ text: 'Special offer for you! Get {{discount_percentage}}% off your next purchase. Use the code below at checkout',
+ type: 'BODY',
+ example: {
+ body_text_named_params: [
+ {
+ example: '30',
+ param_name: 'discount_percentage',
+ },
+ ],
+ },
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ text: 'Copy offer code',
+ type: 'COPY_CODE',
+ example: ['SAVE1OFF'],
+ },
+ ],
+ },
+ ],
+ sub_category: 'CUSTOM',
+ parameter_format: 'NAMED',
+ },
+ {
+ id: '1075221534579807',
+ name: 'support_callback',
+ status: 'APPROVED',
+ category: 'UTILITY',
+ language: 'en',
+ components: [
+ {
+ text: 'Hello {{name}}, our support team will call you regarding ticket # {{ticket_id}}.',
+ type: 'BODY',
+ example: {
+ body_text_named_params: [
+ {
+ example: 'muhsin',
+ param_name: 'name',
+ },
+ {
+ example: '232323',
+ param_name: 'ticket_id',
+ },
+ ],
+ },
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ text: 'Call Support',
+ type: 'PHONE_NUMBER',
+ phone_number: '+2112121212',
+ },
+ ],
+ },
+ ],
+ sub_category: 'CUSTOM',
+ parameter_format: 'NAMED',
+ },
+ {
+ id: '1023596726651144',
+ name: 'training_video',
+ status: 'APPROVED',
+ category: 'MARKETING',
+ language: 'en',
+ components: [
+ {
+ type: 'HEADER',
+ format: 'VIDEO',
+ example: {
+ header_handle: [
+ 'https://scontent.whatsapp.net/v/t61.29466-34/521582686_1023596729984477_1872358575355618432_n.mp4',
+ ],
+ },
+ },
+ {
+ text: "Hi {{name}}, here's your training video. Please watch by{{date}}.",
+ type: 'BODY',
+ example: {
+ body_text_named_params: [
+ {
+ example: 'john',
+ param_name: 'name',
+ },
+ {
+ example: 'July 31',
+ param_name: 'date',
+ },
+ ],
+ },
+ },
+ ],
+ sub_category: 'CUSTOM',
+ parameter_format: 'NAMED',
+ },
+ {
+ id: '1106685194739985',
+ name: 'order_confirmation',
+ status: 'APPROVED',
+ category: 'MARKETING',
+ language: 'en',
+ components: [
+ {
+ type: 'HEADER',
+ format: 'IMAGE',
+ example: {
+ header_handle: ['https://vite-five-phi.vercel.app/vaporfly.png'],
+ },
+ },
+ {
+ text: 'Hi your order {{1}} is confirmed. Please wait for further updates',
+ type: 'BODY',
+ example: {
+ body_text: [['blue canvas shoes']],
+ },
+ },
+ ],
+ sub_category: 'CUSTOM',
+ parameter_format: 'POSITIONAL',
+ },
+ {
+ id: '1242180011253003',
+ name: 'product_launch',
+ status: 'APPROVED',
+ category: 'MARKETING',
+ language: 'en',
+ components: [
+ {
+ type: 'HEADER',
+ format: 'IMAGE',
+ example: {
+ header_handle: ['https://vite-five-phi.vercel.app/coat.png'],
+ },
+ },
+ {
+ text: 'New arrival! Our stunning coat now available in {{color}} color.',
+ type: 'BODY',
+ example: {
+ body_text_named_params: [
+ {
+ example: 'blue',
+ param_name: 'color',
+ },
+ ],
+ },
+ },
+ {
+ text: 'Free shipping on orders over $100. Limited time offer.',
+ type: 'FOOTER',
+ },
+ ],
+ sub_category: 'CUSTOM',
+ parameter_format: 'NAMED',
+ },
+ {
+ id: '1449876326175680',
+ name: 'technician_visit',
+ status: 'APPROVED',
+ category: 'UTILITY',
+ language: 'en_US',
+ components: [
+ {
+ text: 'Technician visit',
+ type: 'HEADER',
+ format: 'TEXT',
+ },
+ {
+ text: "Hi {{1}}, we're scheduling a technician visit to {{2}} on {{3}} between {{4}} and {{5}}. Please confirm if this time slot works for you.",
+ type: 'BODY',
+ example: {
+ body_text: [
+ ['John', '123 Maple St', '2025-12-31', '10:00 AM', '2:00 PM'],
+ ],
+ },
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ text: 'Confirm',
+ type: 'QUICK_REPLY',
+ },
+ {
+ text: 'Reschedule',
+ type: 'QUICK_REPLY',
+ },
+ ],
+ },
+ ],
+ parameter_format: 'POSITIONAL',
+ },
+ {
+ id: '997298832221901',
+ name: 'greet',
+ status: 'APPROVED',
+ category: 'MARKETING',
+ language: 'en',
+ components: [
+ {
+ text: 'Hey {{customer_name}} how may I help you?',
+ type: 'BODY',
+ example: {
+ body_text_named_params: [
+ {
+ example: 'John',
+ param_name: 'customer_name',
+ },
+ ],
+ },
+ },
+ ],
+ sub_category: 'CUSTOM',
+ parameter_format: 'NAMED',
+ },
+ {
+ id: '632315222954611',
+ name: 'hello_world',
+ status: 'APPROVED',
+ category: 'UTILITY',
+ language: 'en_US',
+ components: [
+ {
+ text: 'Hello World',
+ type: 'HEADER',
+ format: 'TEXT',
+ },
+ {
+ text: 'Welcome and congratulations!! This message demonstrates your ability to send a WhatsApp message notification from the Cloud API, hosted by Meta. Thank you for taking the time to test with us.',
+ type: 'BODY',
+ },
+ {
+ text: 'WhatsApp Business Platform sample message',
+ type: 'FOOTER',
+ },
+ ],
+ parameter_format: 'POSITIONAL',
+ },
+ {
+ id: '787864066907971',
+ name: 'feedback_request',
+ status: 'APPROVED',
+ category: 'MARKETING',
+ language: 'en',
+ components: [
+ {
+ text: "Hey {{name}}, how was your experience with Puma? We'd love your feedback!",
+ type: 'BODY',
+ example: {
+ body_text_named_params: [
+ {
+ example: 'muhsin',
+ param_name: 'name',
+ },
+ ],
+ },
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ url: 'https://feedback.example.com/survey',
+ text: 'Leave Feedback',
+ type: 'URL',
+ },
+ ],
+ },
+ ],
+ sub_category: 'CUSTOM',
+ parameter_format: 'NAMED',
+ },
+ {
+ id: '1938057163677205',
+ name: 'address_update',
+ status: 'APPROVED',
+ category: 'UTILITY',
+ language: 'en_US',
+ components: [
+ {
+ text: 'Address update',
+ type: 'HEADER',
+ format: 'TEXT',
+ },
+ {
+ text: 'Hi {{1}}, your delivery address has been successfully updated to {{2}}. Contact {{3}} for any inquiries.',
+ type: 'BODY',
+ example: {
+ body_text: [['John', '123 Main St', 'support@telco.com']],
+ },
+ },
+ ],
+ parameter_format: 'POSITIONAL',
+ },
+ {
+ id: '1644094842949394',
+ name: 'delivery_confirmation',
+ status: 'APPROVED',
+ category: 'UTILITY',
+ language: 'en_US',
+ components: [
+ {
+ text: '{{1}}, your order was successfully delivered on {{2}}.\\n\\nThank you for your purchase.\\n',
+ type: 'BODY',
+ example: {
+ body_text: [['John', 'Jan 1, 2024']],
+ },
+ },
+ ],
+ parameter_format: 'POSITIONAL',
+ },
+];
+
+// Helper function to get variable values from examples
+export const getWhatsAppVariables = template => {
+ const variables = {};
+ template.components?.forEach(component => {
+ if (component.example?.body_text_named_params) {
+ component.example.body_text_named_params.forEach(param => {
+ variables[param.param_name] = param.example;
+ });
+ }
+ if (component.example?.body_text) {
+ component.example.body_text[0]?.forEach((value, index) => {
+ variables[(index + 1).toString()] = value;
+ });
+ }
+ });
+ return variables;
+};
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue
index dc247da6f..d955883eb 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue
@@ -11,7 +11,7 @@ import Icon from 'dashboard/components-next/icon/Icon.vue';
import WithLabel from 'v3/components/Form/WithLabel.vue';
import SettingsToggleSection from 'dashboard/components-next/Settings/SettingsToggleSection.vue';
import CSATDisplayTypeSelector from './components/CSATDisplayTypeSelector.vue';
-import CSATTemplate from 'dashboard/components-next/message/bubbles/Template/CSAT.vue';
+import CSATTemplate from 'dashboard/components-next/template-preview/CSATTemplate.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
import FilterSelect from 'dashboard/components-next/filter/inputs/FilterSelect.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
diff --git a/app/javascript/dashboard/services/TemplateConstants.js b/app/javascript/dashboard/services/TemplateConstants.js
new file mode 100644
index 000000000..6474372c4
--- /dev/null
+++ b/app/javascript/dashboard/services/TemplateConstants.js
@@ -0,0 +1,58 @@
+export const PLATFORMS = {
+ WHATSAPP: 'whatsapp',
+ TWILIO: 'twilio',
+};
+
+export const WA_COMPONENT_TYPES = {
+ HEADER: 'HEADER',
+ BODY: 'BODY',
+ FOOTER: 'FOOTER',
+ BUTTONS: 'BUTTONS',
+};
+
+export const WA_HEADER_FORMATS = {
+ TEXT: 'TEXT',
+ IMAGE: 'IMAGE',
+ VIDEO: 'VIDEO',
+ DOCUMENT: 'DOCUMENT',
+};
+
+export const WA_MEDIA_FORMATS = [
+ WA_HEADER_FORMATS.IMAGE,
+ WA_HEADER_FORMATS.VIDEO,
+ WA_HEADER_FORMATS.DOCUMENT,
+];
+
+export const WA_BUTTON_TYPES = {
+ COPY_CODE: 'COPY_CODE',
+};
+
+export const WA_PARAM_FORMATS = {
+ POSITIONAL: 'POSITIONAL',
+ NAMED: 'NAMED',
+};
+
+export const TWILIO_TYPE_PREFIX = 'twilio/';
+
+export const TWILIO_TYPES = {
+ TEXT: 'text',
+ MEDIA: 'media',
+ QUICK_REPLY: 'quick_reply',
+ CALL_TO_ACTION: 'call_to_action',
+ CATALOG: 'catalog',
+};
+
+export const TEMPLATE_TYPES = {
+ WHATSAPP_TEXT: 'whatsapp-text',
+ WHATSAPP_TEXT_HEADER: 'whatsapp-text-header',
+ WHATSAPP_MEDIA_IMAGE: 'whatsapp-media-image',
+ WHATSAPP_MEDIA_VIDEO: 'whatsapp-media-video',
+ WHATSAPP_MEDIA_DOCUMENT: 'whatsapp-media-document',
+ WHATSAPP_INTERACTIVE: 'whatsapp-interactive',
+ WHATSAPP_COPY_CODE: 'whatsapp-copy-code',
+ TWILIO_TEXT: 'twilio-text',
+ TWILIO_MEDIA: 'twilio-media',
+ TWILIO_QUICK_REPLY: 'twilio-quick-reply',
+ TWILIO_CALL_TO_ACTION: 'twilio-call-to-action',
+ TWILIO_CARD: 'twilio-card',
+};
diff --git a/app/javascript/dashboard/services/TemplateNormalizer.js b/app/javascript/dashboard/services/TemplateNormalizer.js
new file mode 100644
index 000000000..588415815
--- /dev/null
+++ b/app/javascript/dashboard/services/TemplateNormalizer.js
@@ -0,0 +1,114 @@
+import { TemplateTypeDetector } from './TemplateTypeDetector';
+import {
+ PLATFORMS,
+ TWILIO_TYPE_PREFIX,
+ WA_COMPONENT_TYPES,
+ WA_PARAM_FORMATS,
+} from './TemplateConstants';
+
+/**
+ * TemplateNormalizer - Convert platform-specific formats to unified structure
+ */
+export class TemplateNormalizer {
+ static normalizeWhatsApp(template) {
+ const components = template.components || [];
+
+ return {
+ id: template.id,
+ name: template.name,
+ platform: PLATFORMS.WHATSAPP,
+ type: TemplateTypeDetector.detectWhatsAppType(template),
+ parameterFormat: template.parameter_format || WA_PARAM_FORMATS.POSITIONAL,
+ header: components.find(c => c.type === WA_COMPONENT_TYPES.HEADER),
+ body: components.find(c => c.type === WA_COMPONENT_TYPES.BODY),
+ footer: components.find(c => c.type === WA_COMPONENT_TYPES.FOOTER),
+ buttons:
+ components.find(c => c.type === WA_COMPONENT_TYPES.BUTTONS)?.buttons ||
+ [],
+ variables: this.extractWhatsAppVariables(template),
+ category: template.category,
+ language: template.language,
+ originalTemplate: template,
+ };
+ }
+
+ static normalizeTwilio(template) {
+ const typeKey =
+ template.template_type?.replace(TWILIO_TYPE_PREFIX, '') ||
+ Object.keys(template.types || {})
+ .map(key => key.replace(TWILIO_TYPE_PREFIX, ''))
+ .find(key => key);
+
+ const hyphenatedKey = `${TWILIO_TYPE_PREFIX}${(typeKey || '').replace(/_/g, '-')}`;
+ const underscoreKey = `${TWILIO_TYPE_PREFIX}${(typeKey || '').replace(/-/g, '_')}`;
+ const typeData =
+ template.types?.[hyphenatedKey] || template.types?.[underscoreKey] || {};
+
+ return {
+ contentSid: template.content_sid,
+ name: template.friendly_name,
+ platform: PLATFORMS.TWILIO,
+ type: TemplateTypeDetector.detectTwilioType(template),
+ body: template.body || typeData.body,
+ media: typeData.media || [],
+ mediaType: template.media_type || null,
+ actions: typeData.actions || [],
+ variables: template.variables || {},
+ category: template.category || 'utility',
+ language: template.language || 'en',
+ originalTemplate: template,
+ };
+ }
+
+ static extractWhatsAppVariables(template) {
+ const variables = {};
+ const components = template.components || [];
+
+ components.forEach(component => {
+ if (component.text) {
+ const matches = component.text.match(/\{\{([^}]+)\}\}/g) || [];
+ matches.forEach(match => {
+ const variable = match.replace(/[{}]/g, '');
+
+ if (template.parameter_format === WA_PARAM_FORMATS.NAMED) {
+ const example =
+ component.example?.body_text_named_params?.find(
+ p => p.param_name === variable
+ )?.example || '';
+ variables[variable] = example;
+ } else {
+ const position = parseInt(variable, 10) - 1;
+ const example = component.example?.body_text?.[0]?.[position] || '';
+ variables[variable] = example;
+ }
+ });
+ }
+
+ if (component.buttons) {
+ component.buttons.forEach(button => {
+ if (button.url) {
+ const matches = button.url.match(/\{\{([^}]+)\}\}/g) || [];
+ matches.forEach(match => {
+ const variable = match.replace(/[{}]/g, '');
+ const example = button.example?.[0] || '';
+ variables[variable] = example;
+ });
+ }
+ });
+ }
+ });
+
+ return variables;
+ }
+
+ static normalize(template, platform) {
+ switch (platform) {
+ case PLATFORMS.WHATSAPP:
+ return this.normalizeWhatsApp(template);
+ case PLATFORMS.TWILIO:
+ return this.normalizeTwilio(template);
+ default:
+ throw new Error(`Unsupported platform: ${platform}`);
+ }
+ }
+}
diff --git a/app/javascript/dashboard/services/TemplateNormalizer.spec.js b/app/javascript/dashboard/services/TemplateNormalizer.spec.js
new file mode 100644
index 000000000..4b9e2a9e0
--- /dev/null
+++ b/app/javascript/dashboard/services/TemplateNormalizer.spec.js
@@ -0,0 +1,143 @@
+// @vitest-environment node
+
+import { describe, it, expect } from 'vitest';
+
+import { TemplateNormalizer } from './TemplateNormalizer';
+
+describe('TemplateNormalizer', () => {
+ it('normalizes Twilio call-to-action templates with hyphenated keys', () => {
+ const template = {
+ types: {
+ 'twilio/call-to-action': {
+ body: 'CTA body for {{date}}',
+ actions: [
+ {
+ id: null,
+ title: 'Pay now',
+ type: 'URL',
+ url: 'https://payments.example.com/pay',
+ },
+ ],
+ },
+ },
+ variables: {
+ date: '01-Jan-2026',
+ },
+ content_sid: 'HX123',
+ friendly_name: 'cta_example',
+ template_type: 'call-to-action',
+ };
+
+ const normalized = TemplateNormalizer.normalizeTwilio(template);
+
+ expect(normalized.type).toBe('twilio-call-to-action');
+ expect(normalized.body).toBe('CTA body for {{date}}');
+ expect(normalized.actions).toHaveLength(1);
+ expect(normalized.variables).toEqual({ date: '01-Jan-2026' });
+ });
+
+ it('normalizes Twilio quick replies', () => {
+ const template = {
+ template_type: 'quick_reply',
+ types: {
+ 'twilio/quick-reply': {
+ body: 'Pick an option',
+ actions: [{ id: 'a', title: 'Option A' }],
+ },
+ },
+ variables: {},
+ };
+
+ const normalized = TemplateNormalizer.normalizeTwilio(template);
+
+ expect(normalized.type).toBe('twilio-quick-reply');
+ expect(normalized.body).toBe('Pick an option');
+ expect(normalized.actions).toEqual([{ id: 'a', title: 'Option A' }]);
+ });
+
+ it('normalizes Twilio media templates', () => {
+ const template = {
+ template_type: 'media',
+ body: 'Media body {{1}}',
+ types: {
+ 'twilio/media': {
+ body: 'Media body {{1}}',
+ media: ['https://example.com/image.jpg'],
+ },
+ },
+ variables: { 1: 'value' },
+ };
+
+ const normalized = TemplateNormalizer.normalizeTwilio(template);
+
+ expect(normalized.type).toBe('twilio-media');
+ expect(normalized.media).toEqual(['https://example.com/image.jpg']);
+ expect(normalized.body).toBe('Media body {{1}}');
+ });
+
+ it('extracts WhatsApp named variables', () => {
+ const template = {
+ parameter_format: 'NAMED',
+ components: [
+ {
+ type: 'BODY',
+ text: 'Hi {{name}}',
+ example: {
+ body_text_named_params: [{ param_name: 'name', example: 'John' }],
+ },
+ },
+ ],
+ };
+
+ const variables = TemplateNormalizer.extractWhatsAppVariables(template);
+
+ expect(variables).toMatchObject({
+ name: 'John',
+ });
+ });
+
+ it('extracts WhatsApp positional variables', () => {
+ const template = {
+ parameter_format: 'POSITIONAL',
+ components: [
+ {
+ type: 'BODY',
+ text: 'Hi {{1}}',
+ example: {
+ body_text: [['positional']],
+ },
+ },
+ ],
+ };
+
+ const variables = TemplateNormalizer.extractWhatsAppVariables(template);
+
+ expect(variables).toMatchObject({
+ 1: 'positional',
+ });
+ });
+
+ it('normalizes WhatsApp media image templates', () => {
+ const template = {
+ id: '1',
+ name: 'order_confirmation',
+ parameter_format: 'POSITIONAL',
+ components: [
+ {
+ type: 'HEADER',
+ format: 'IMAGE',
+ example: { header_handle: ['https://example.com/image.jpg'] },
+ },
+ { type: 'BODY', text: 'Hi {{1}}', example: { body_text: [['John']] } },
+ ],
+ language: 'en',
+ };
+
+ const normalized = TemplateNormalizer.normalizeWhatsApp(template);
+
+ expect(normalized.type).toBe('whatsapp-media-image');
+ expect(normalized.header.format).toBe('IMAGE');
+ expect(normalized.body.text).toBe('Hi {{1}}');
+ expect(normalized.variables).toMatchObject({ 1: 'John' });
+ });
+});
diff --git a/app/javascript/dashboard/services/TemplateTypeDetector.js b/app/javascript/dashboard/services/TemplateTypeDetector.js
new file mode 100644
index 000000000..c7d1859c7
--- /dev/null
+++ b/app/javascript/dashboard/services/TemplateTypeDetector.js
@@ -0,0 +1,63 @@
+import {
+ TEMPLATE_TYPES,
+ TWILIO_TYPE_PREFIX,
+ TWILIO_TYPES,
+ WA_BUTTON_TYPES,
+ WA_COMPONENT_TYPES,
+ WA_HEADER_FORMATS,
+} from './TemplateConstants';
+
+/**
+ * TemplateTypeDetector - Unified service to identify template types across platforms
+ */
+export class TemplateTypeDetector {
+ static detectWhatsAppType(template) {
+ const components = template.components || [];
+ const header = components.find(c => c.type === WA_COMPONENT_TYPES.HEADER);
+ const buttons = components.find(c => c.type === WA_COMPONENT_TYPES.BUTTONS);
+
+ if (header?.format === WA_HEADER_FORMATS.IMAGE)
+ return TEMPLATE_TYPES.WHATSAPP_MEDIA_IMAGE;
+ if (header?.format === WA_HEADER_FORMATS.VIDEO)
+ return TEMPLATE_TYPES.WHATSAPP_MEDIA_VIDEO;
+ if (header?.format === WA_HEADER_FORMATS.DOCUMENT)
+ return TEMPLATE_TYPES.WHATSAPP_MEDIA_DOCUMENT;
+
+ if (buttons) {
+ const hasCopyCode = buttons.buttons?.some(
+ b => b.type === WA_BUTTON_TYPES.COPY_CODE
+ );
+ if (hasCopyCode) return TEMPLATE_TYPES.WHATSAPP_COPY_CODE;
+ return TEMPLATE_TYPES.WHATSAPP_INTERACTIVE;
+ }
+
+ if (header?.format === WA_HEADER_FORMATS.TEXT)
+ return TEMPLATE_TYPES.WHATSAPP_TEXT_HEADER;
+ return TEMPLATE_TYPES.WHATSAPP_TEXT;
+ }
+
+ static detectTwilioType(template) {
+ const typeFromTemplate =
+ template.template_type?.replace(TWILIO_TYPE_PREFIX, '') ||
+ Object.keys(template.types || {})
+ .map(key => key.replace(TWILIO_TYPE_PREFIX, ''))
+ .find(typeKey => typeKey);
+
+ const templateType = (typeFromTemplate || '')
+ .replace(/-/g, '_')
+ .replace(/__/g, '_');
+
+ switch (templateType) {
+ case TWILIO_TYPES.MEDIA:
+ return TEMPLATE_TYPES.TWILIO_MEDIA;
+ case TWILIO_TYPES.QUICK_REPLY:
+ return TEMPLATE_TYPES.TWILIO_QUICK_REPLY;
+ case TWILIO_TYPES.CALL_TO_ACTION:
+ return TEMPLATE_TYPES.TWILIO_CALL_TO_ACTION;
+ case TWILIO_TYPES.CATALOG:
+ return TEMPLATE_TYPES.TWILIO_CARD;
+ default:
+ return TEMPLATE_TYPES.TWILIO_TEXT;
+ }
+ }
+}
diff --git a/app/javascript/dashboard/services/TemplateTypeDetector.spec.js b/app/javascript/dashboard/services/TemplateTypeDetector.spec.js
new file mode 100644
index 000000000..ddf1a7593
--- /dev/null
+++ b/app/javascript/dashboard/services/TemplateTypeDetector.spec.js
@@ -0,0 +1,106 @@
+// @vitest-environment node
+
+import { describe, it, expect } from 'vitest';
+
+import { TemplateTypeDetector } from './TemplateTypeDetector';
+
+describe('TemplateTypeDetector', () => {
+ it('detects Twilio call-to-action from hyphenated template_type', () => {
+ const template = {
+ template_type: 'call-to-action',
+ types: {
+ 'twilio/call-to-action': {},
+ },
+ };
+
+ expect(TemplateTypeDetector.detectTwilioType(template)).toBe(
+ 'twilio-call-to-action'
+ );
+ });
+
+ it('detects Twilio call-to-action when only types key is present', () => {
+ const template = {
+ types: {
+ 'twilio/call_to_action': {},
+ },
+ };
+
+ expect(TemplateTypeDetector.detectTwilioType(template)).toBe(
+ 'twilio-call-to-action'
+ );
+ });
+
+ it('detects Twilio quick reply', () => {
+ const template = {
+ template_type: 'quick_reply',
+ types: {
+ 'twilio/quick-reply': {},
+ },
+ };
+
+ expect(TemplateTypeDetector.detectTwilioType(template)).toBe(
+ 'twilio-quick-reply'
+ );
+ });
+
+ it('detects WhatsApp text-with-header and media', () => {
+ const base = {
+ components: [
+ { type: 'HEADER', format: 'TEXT', text: 'Header' },
+ { type: 'BODY', text: 'Body' },
+ ],
+ };
+
+ expect(TemplateTypeDetector.detectWhatsAppType(base)).toBe(
+ 'whatsapp-text-header'
+ );
+
+ const withImage = {
+ ...base,
+ components: [{ type: 'HEADER', format: 'IMAGE' }, base.components[1]],
+ };
+ expect(TemplateTypeDetector.detectWhatsAppType(withImage)).toBe(
+ 'whatsapp-media-image'
+ );
+ });
+
+ it('detects WhatsApp copy code vs call-to-action buttons', () => {
+ const copyCode = {
+ components: [
+ { type: 'BODY', text: 'Hi' },
+ {
+ type: 'BUTTONS',
+ buttons: [{ type: 'COPY_CODE', text: 'Copy offer code' }],
+ },
+ ],
+ };
+
+ const callToAction = {
+ components: [
+ { type: 'BODY', text: 'Hi' },
+ {
+ type: 'BUTTONS',
+ buttons: [{ type: 'URL', text: 'Visit website' }],
+ },
+ ],
+ };
+
+ expect(TemplateTypeDetector.detectWhatsAppType(copyCode)).toBe(
+ 'whatsapp-copy-code'
+ );
+ expect(TemplateTypeDetector.detectWhatsAppType(callToAction)).toBe(
+ 'whatsapp-interactive'
+ );
+ });
+
+ it('detects Twilio media', () => {
+ const template = {
+ template_type: 'media',
+ types: { 'twilio/media': {} },
+ };
+
+ expect(TemplateTypeDetector.detectTwilioType(template)).toBe(
+ 'twilio-media'
+ );
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js
index c4c6a0b40..093e9ddf5 100644
--- a/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js
+++ b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js
@@ -368,7 +368,7 @@ export const templates = [
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
components: [
{
- text: '🎉 Special offer for you! Get {{discount_percentage}}% off your next purchase. Use the code below at checkout',
+ text: 'Special offer for you! Get {{discount_percentage}}% off your next purchase. Use the code below at checkout',
type: 'BODY',
},
{
@@ -400,7 +400,7 @@ export const templates = [
{
text: 'Call Support',
type: 'PHONE_NUMBER',
- phone_number: '+16506677566',
+ phone_number: '+23232323',
},
],
},
diff --git a/theme/icons.js b/theme/icons.js
index 8a29e2bda..266c7ddfa 100644
--- a/theme/icons.js
+++ b/theme/icons.js
@@ -423,5 +423,10 @@ export const icons = {
width: 16,
height: 16,
},
+ 'quick-reply': {
+ body: `
`,
+ width: 15,
+ height: 15,
+ },
/** Ends */
};