chore: add more templates
This commit is contained in:
+364
-32
@@ -53,6 +53,21 @@ const buttonComponents = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const interactiveComponents = computed(() => {
|
||||
return templateComponents.value.filter(
|
||||
component => ['LIST', 'PRODUCT', 'CATALOG'].includes(component.type)
|
||||
);
|
||||
});
|
||||
|
||||
const isInteractiveTemplate = computed(() => {
|
||||
const hasInteractiveButtons = buttonComponents.value.some(component =>
|
||||
component.buttons?.some(button =>
|
||||
['quick_reply', 'url', 'phone_number', 'catalog_browse'].includes(button.type)
|
||||
)
|
||||
);
|
||||
return hasInteractiveButtons || interactiveComponents.value.length > 0;
|
||||
});
|
||||
|
||||
const legacyParams = computed(() => {
|
||||
const params = {};
|
||||
Object.keys(processedParams.value).forEach(key => {
|
||||
@@ -90,7 +105,17 @@ const processedStringWithVariableHighlight = computed(() => {
|
||||
const rules = computed(() => {
|
||||
const paramRules = {};
|
||||
Object.keys(processedParams.value).forEach(key => {
|
||||
paramRules[key] = { required: requiredIf(true) };
|
||||
if (key === 'header' && processedParams.value.header.location_type === 'location') {
|
||||
// Add specific validation for location parameters
|
||||
paramRules[key] = {
|
||||
location: {
|
||||
latitude: { required: requiredIf(true) },
|
||||
longitude: { required: requiredIf(true) }
|
||||
}
|
||||
};
|
||||
} else {
|
||||
paramRules[key] = { required: requiredIf(true) };
|
||||
}
|
||||
});
|
||||
return {
|
||||
processedParams: paramRules,
|
||||
@@ -106,45 +131,89 @@ const getFieldErrorType = key => {
|
||||
|
||||
const generateVariables = () => {
|
||||
const allVariables = {};
|
||||
|
||||
// Debug: Log template structure
|
||||
console.log('Template components:', templateComponents.value);
|
||||
console.log('Header component:', headerComponent.value);
|
||||
|
||||
// Process body variables
|
||||
const bodyVars = templateString.value.match(/{{([^}]+)}}/g) || [];
|
||||
bodyVars.forEach(variable => {
|
||||
const key = processVariable(variable);
|
||||
if (!allVariables.body) allVariables.body = {};
|
||||
allVariables.body[key] = '';
|
||||
});
|
||||
if (bodyVars.length > 0) {
|
||||
allVariables.body = {};
|
||||
bodyVars.forEach(variable => {
|
||||
const key = processVariable(variable);
|
||||
// Special handling for authentication templates
|
||||
if (props.template?.category === 'AUTHENTICATION') {
|
||||
if (
|
||||
key === '1' ||
|
||||
key.toLowerCase().includes('otp') ||
|
||||
key.toLowerCase().includes('code')
|
||||
) {
|
||||
allVariables.body.otp_code = '';
|
||||
} else if (
|
||||
key === '2' ||
|
||||
key.toLowerCase().includes('expiry') ||
|
||||
key.toLowerCase().includes('minute')
|
||||
) {
|
||||
allVariables.body.expiry_minutes = '';
|
||||
} else {
|
||||
allVariables.body[key] = '';
|
||||
}
|
||||
} else {
|
||||
allVariables.body[key] = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process header variables
|
||||
if (headerComponent.value) {
|
||||
if (headerComponent.value.text) {
|
||||
// Text headers with variables
|
||||
const headerVars = headerComponent.value.text.match(/{{([^}]+)}}/g) || [];
|
||||
headerVars.forEach(variable => {
|
||||
const key = processVariable(variable);
|
||||
if (!allVariables.header) allVariables.header = {};
|
||||
allVariables.header[key] = '';
|
||||
});
|
||||
if (headerVars.length > 0) {
|
||||
allVariables.header = {};
|
||||
headerVars.forEach(variable => {
|
||||
const key = processVariable(variable);
|
||||
allVariables.header[key] = '';
|
||||
});
|
||||
}
|
||||
} else if (
|
||||
headerComponent.value.format &&
|
||||
['IMAGE', 'VIDEO', 'DOCUMENT'].includes(headerComponent.value.format)
|
||||
) {
|
||||
// Media headers need URL input
|
||||
if (!allVariables.header) allVariables.header = {};
|
||||
allVariables.header.media_url = '';
|
||||
allVariables.header.media_type =
|
||||
headerComponent.value.format.toLowerCase();
|
||||
allVariables.header = {
|
||||
media_url: '',
|
||||
media_type: headerComponent.value.format.toLowerCase(),
|
||||
};
|
||||
} else if (
|
||||
headerComponent.value.format &&
|
||||
headerComponent.value.format === 'LOCATION'
|
||||
) {
|
||||
// Location headers need location data
|
||||
console.log('Detected LOCATION header template:', headerComponent.value);
|
||||
allVariables.header = {
|
||||
location: {
|
||||
latitude: '',
|
||||
longitude: '',
|
||||
name: '',
|
||||
address: '',
|
||||
},
|
||||
location_type: 'location',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Process footer variables
|
||||
if (footerComponent.value?.text) {
|
||||
const footerVars = footerComponent.value.text.match(/{{([^}]+)}}/g) || [];
|
||||
footerVars.forEach(variable => {
|
||||
const key = processVariable(variable);
|
||||
if (!allVariables.footer) allVariables.footer = {};
|
||||
allVariables.footer[key] = '';
|
||||
});
|
||||
if (footerVars.length > 0) {
|
||||
allVariables.footer = {};
|
||||
footerVars.forEach(variable => {
|
||||
const key = processVariable(variable);
|
||||
allVariables.footer[key] = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process button variables
|
||||
@@ -154,25 +223,62 @@ const generateVariables = () => {
|
||||
// Handle URL buttons with variables
|
||||
if (button.url && button.url.includes('{{')) {
|
||||
const buttonVars = button.url.match(/{{([^}]+)}}/g) || [];
|
||||
buttonVars.forEach(() => {
|
||||
if (buttonVars.length > 0) {
|
||||
if (!allVariables.buttons) allVariables.buttons = [];
|
||||
if (!allVariables.buttons[index]) allVariables.buttons[index] = {};
|
||||
allVariables.buttons[index].type = 'url';
|
||||
allVariables.buttons[index].parameter = '';
|
||||
});
|
||||
allVariables.buttons[index] = {
|
||||
type: 'url',
|
||||
parameter: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle copy code buttons
|
||||
if (button.type === 'COPY_CODE') {
|
||||
if (!allVariables.buttons) allVariables.buttons = [];
|
||||
if (!allVariables.buttons[index]) allVariables.buttons[index] = {};
|
||||
allVariables.buttons[index].type = 'copy_code';
|
||||
allVariables.buttons[index].parameter = '';
|
||||
allVariables.buttons[index] = {
|
||||
type: 'copy_code',
|
||||
parameter: '',
|
||||
};
|
||||
}
|
||||
|
||||
// Handle interactive buttons with dynamic text
|
||||
if (['quick_reply', 'url', 'phone_number'].includes(button.type)) {
|
||||
if (button.text && button.text.includes('{{')) {
|
||||
if (!allVariables.buttons) allVariables.buttons = [];
|
||||
allVariables.buttons[index] = {
|
||||
type: button.type,
|
||||
parameter: '',
|
||||
text: button.text,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Process interactive components (LIST, PRODUCT, CATALOG)
|
||||
interactiveComponents.value.forEach(component => {
|
||||
if (component.type === 'LIST') {
|
||||
allVariables.interactive = {
|
||||
type: 'list',
|
||||
button_text: 'Select Option',
|
||||
sections: component.sections || []
|
||||
};
|
||||
} else if (component.type === 'PRODUCT') {
|
||||
allVariables.interactive = {
|
||||
type: 'product',
|
||||
catalog_id: component.catalog_id || '',
|
||||
product_id: component.product_id || ''
|
||||
};
|
||||
} else if (component.type === 'CATALOG') {
|
||||
allVariables.interactive = {
|
||||
type: 'catalog',
|
||||
catalog_id: component.catalog_id || '',
|
||||
title: component.title || 'Browse Products'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
processedParams.value = allVariables;
|
||||
};
|
||||
|
||||
@@ -227,6 +333,70 @@ const getHeaderFieldPlaceholder = key => {
|
||||
return `Enter ${key} value`;
|
||||
};
|
||||
|
||||
const getBodyParameterLabel = key => {
|
||||
if (props.template?.category === 'AUTHENTICATION') {
|
||||
switch (key) {
|
||||
case 'otp_code':
|
||||
return t('WHATSAPP_TEMPLATES.PARSER.OTP_CODE') || 'OTP Code';
|
||||
case 'expiry_minutes':
|
||||
return (
|
||||
t('WHATSAPP_TEMPLATES.PARSER.EXPIRY_MINUTES') || 'Expiry (minutes)'
|
||||
);
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
const getBodyParameterPlaceholder = key => {
|
||||
if (props.template?.category === 'AUTHENTICATION') {
|
||||
switch (key) {
|
||||
case 'otp_code':
|
||||
return (
|
||||
t('WHATSAPP_TEMPLATES.PARSER.OTP_CODE_PLACEHOLDER') ||
|
||||
'Enter 4-8 digit OTP code'
|
||||
);
|
||||
case 'expiry_minutes':
|
||||
return (
|
||||
t('WHATSAPP_TEMPLATES.PARSER.EXPIRY_MINUTES_PLACEHOLDER') ||
|
||||
'Enter expiry time in minutes'
|
||||
);
|
||||
default:
|
||||
return `Enter ${key} value`;
|
||||
}
|
||||
}
|
||||
return `Enter ${key} value`;
|
||||
};
|
||||
|
||||
const getBodyParameterType = key => {
|
||||
if (props.template?.category === 'AUTHENTICATION') {
|
||||
switch (key) {
|
||||
case 'otp_code':
|
||||
return 'tel';
|
||||
case 'expiry_minutes':
|
||||
return 'number';
|
||||
default:
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
return 'text';
|
||||
};
|
||||
|
||||
const getBodyParameterMaxLength = key => {
|
||||
if (props.template?.category === 'AUTHENTICATION') {
|
||||
switch (key) {
|
||||
case 'otp_code':
|
||||
return 8;
|
||||
case 'expiry_minutes':
|
||||
return 3;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const sendMessage = async () => {
|
||||
const isValid = await v$.value.$validate();
|
||||
if (!isValid) return;
|
||||
@@ -261,6 +431,7 @@ onMounted(() => {
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
|
||||
<p
|
||||
v-dompurify-html="processedStringWithVariableHighlight"
|
||||
class="mb-0 text-sm text-n-slate-11"
|
||||
@@ -285,11 +456,93 @@ onMounted(() => {
|
||||
'Header Parameters'
|
||||
}}
|
||||
</h4>
|
||||
<!-- Location Parameters -->
|
||||
<div
|
||||
v-for="(variable, key) in processedParams.header"
|
||||
:key="`header-${key}`"
|
||||
class="flex items-center w-full gap-2 mb-2"
|
||||
v-if="processedParams.header.location_type === 'location'"
|
||||
class="w-full space-y-3 mb-4 p-3 bg-n-solid-1 rounded-lg border border-n-weak"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center">
|
||||
<div class="w-2 h-2 bg-white rounded-full"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-n-slate-12">Location Details</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="text-xs font-medium text-n-slate-10 mb-1 block">
|
||||
📍 Latitude <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
v-model="processedParams.header.location.latitude"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="37.7749 (San Francisco)"
|
||||
:message-type="getFieldErrorType('header.location.latitude')"
|
||||
/>
|
||||
<span class="text-xs text-n-slate-9">Range: -90.0 to 90.0</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-n-slate-10 mb-1 block">
|
||||
📍 Longitude <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
v-model="processedParams.header.location.longitude"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
type="number"
|
||||
step="any"
|
||||
placeholder="-122.4194 (San Francisco)"
|
||||
:message-type="getFieldErrorType('header.location.longitude')"
|
||||
/>
|
||||
<span class="text-xs text-n-slate-9">Range: -180.0 to 180.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-xs font-medium text-n-slate-10 mb-1 block">
|
||||
🏢 Location Name
|
||||
</label>
|
||||
<Input
|
||||
v-model="processedParams.header.location.name"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
placeholder="Your Business Name"
|
||||
:message-type="getFieldErrorType('header.location.name')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-xs font-medium text-n-slate-10 mb-1 block">
|
||||
📮 Full Address
|
||||
</label>
|
||||
<Input
|
||||
v-model="processedParams.header.location.address"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
placeholder="123 Main Street, City, State 12345"
|
||||
:message-type="getFieldErrorType('header.location.address')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-n-slate-9 bg-blue-50 p-2 rounded border-l-4 border-blue-400">
|
||||
💡 <strong>Tip:</strong> You can get coordinates by searching your location on Google Maps,
|
||||
right-clicking the pin, and copying the latitude/longitude values.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regular Header Parameters -->
|
||||
<div
|
||||
v-if="processedParams.header.location_type !== 'location'"
|
||||
>
|
||||
<div
|
||||
v-for="(variable, key) in processedParams.header"
|
||||
v-if="!['location', 'location_type'].includes(key)"
|
||||
:key="`header-${key}`"
|
||||
class="flex items-center w-full gap-2 mb-2"
|
||||
>
|
||||
<span
|
||||
class="flex items-center h-8 text-sm min-w-6 ltr:text-left rtl:text-right text-n-slate-10"
|
||||
>
|
||||
@@ -303,6 +556,7 @@ onMounted(() => {
|
||||
:placeholder="getHeaderFieldPlaceholder(key)"
|
||||
:type="key === 'media_url' ? 'url' : 'text'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -321,13 +575,16 @@ onMounted(() => {
|
||||
<span
|
||||
class="flex items-center h-8 text-sm min-w-6 ltr:text-left rtl:text-right text-n-slate-10"
|
||||
>
|
||||
{{ key }}
|
||||
{{ getBodyParameterLabel(key) }}
|
||||
</span>
|
||||
<Input
|
||||
v-model="processedParams.body[key]"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
:message-type="getFieldErrorType(`body.${key}`)"
|
||||
:placeholder="getBodyParameterPlaceholder(key)"
|
||||
:type="getBodyParameterType(key)"
|
||||
:maxlength="getBodyParameterMaxLength(key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -359,6 +616,81 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Components -->
|
||||
<div v-if="processedParams.interactive" class="w-full">
|
||||
<h4 class="text-sm font-medium text-n-slate-12 mb-2">
|
||||
{{
|
||||
t('WHATSAPP_TEMPLATES.PARSER.INTERACTIVE_PARAMETERS') ||
|
||||
'Interactive Parameters'
|
||||
}}
|
||||
</h4>
|
||||
|
||||
<!-- Product Template -->
|
||||
<div v-if="processedParams.interactive.type === 'product'" class="space-y-2 mb-4">
|
||||
<div>
|
||||
<label class="text-xs font-medium text-n-slate-10 mb-1 block">Catalog ID</label>
|
||||
<Input
|
||||
v-model="processedParams.interactive.catalog_id"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
placeholder="Enter catalog ID"
|
||||
:message-type="getFieldErrorType('interactive.catalog_id')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-n-slate-10 mb-1 block">Product ID</label>
|
||||
<Input
|
||||
v-model="processedParams.interactive.product_id"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
placeholder="Enter product retailer ID"
|
||||
:message-type="getFieldErrorType('interactive.product_id')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Catalog Template -->
|
||||
<div v-else-if="processedParams.interactive.type === 'catalog'" class="space-y-2 mb-4">
|
||||
<div>
|
||||
<label class="text-xs font-medium text-n-slate-10 mb-1 block">Catalog ID</label>
|
||||
<Input
|
||||
v-model="processedParams.interactive.catalog_id"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
placeholder="Enter catalog ID"
|
||||
:message-type="getFieldErrorType('interactive.catalog_id')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-n-slate-10 mb-1 block">Browse Title</label>
|
||||
<Input
|
||||
v-model="processedParams.interactive.title"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
placeholder="Browse Products"
|
||||
:message-type="getFieldErrorType('interactive.title')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List Template -->
|
||||
<div v-else-if="processedParams.interactive.type === 'list'" class="space-y-2 mb-4">
|
||||
<div>
|
||||
<label class="text-xs font-medium text-n-slate-10 mb-1 block">Button Text</label>
|
||||
<Input
|
||||
v-model="processedParams.interactive.button_text"
|
||||
custom-input-class="!h-8 w-full !bg-transparent"
|
||||
class="w-full"
|
||||
placeholder="Select an Option"
|
||||
:message-type="getFieldErrorType('interactive.button_text')"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-n-slate-10">
|
||||
List sections are configured in the template and cannot be modified here.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button Variables -->
|
||||
<div v-if="processedParams.buttons" class="w-full">
|
||||
<h4 class="text-sm font-medium text-n-slate-12 mb-2">
|
||||
|
||||
+106
-27
@@ -43,6 +43,20 @@ export default {
|
||||
).text;
|
||||
});
|
||||
|
||||
const headerComponent = computed(() => {
|
||||
return props.template.components.find(
|
||||
component => component.type === 'HEADER'
|
||||
);
|
||||
});
|
||||
|
||||
const hasMediaHeader = computed(() => {
|
||||
return (
|
||||
headerComponent.value &&
|
||||
headerComponent.value.format &&
|
||||
['IMAGE', 'VIDEO', 'DOCUMENT'].includes(headerComponent.value.format)
|
||||
);
|
||||
});
|
||||
|
||||
const variables = computed(() => {
|
||||
return templateString.value.match(/{{([^}]+)}}/g);
|
||||
});
|
||||
@@ -65,14 +79,23 @@ export default {
|
||||
);
|
||||
|
||||
const generateVariables = () => {
|
||||
const matchedVariables = templateString.value.match(/{{([^}]+)}}/g);
|
||||
if (!matchedVariables) return;
|
||||
const params = {};
|
||||
|
||||
const finalVars = matchedVariables.map(i => processVariable(i));
|
||||
processedParams.value = finalVars.reduce((acc, variable) => {
|
||||
acc[variable] = '';
|
||||
return acc;
|
||||
}, {});
|
||||
// Add media URL field if template has media header
|
||||
if (hasMediaHeader.value) {
|
||||
params.media_url = '';
|
||||
}
|
||||
|
||||
// Add body variables
|
||||
const matchedVariables = templateString.value.match(/{{([^}]+)}}/g);
|
||||
if (matchedVariables) {
|
||||
const finalVars = matchedVariables.map(i => processVariable(i));
|
||||
finalVars.forEach(variable => {
|
||||
params[variable] = '';
|
||||
});
|
||||
}
|
||||
|
||||
processedParams.value = params;
|
||||
};
|
||||
|
||||
const resetTemplate = () => {
|
||||
@@ -83,6 +106,29 @@ export default {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
// Prepare enhanced template parameters for media support
|
||||
const enhancedParams = {};
|
||||
|
||||
// Handle media header
|
||||
if (hasMediaHeader.value && processedParams.value.media_url) {
|
||||
enhancedParams.header = {
|
||||
media_url: processedParams.value.media_url,
|
||||
media_type: headerComponent.value.format.toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle body variables
|
||||
const bodyParams = {};
|
||||
Object.keys(processedParams.value).forEach(key => {
|
||||
if (key !== 'media_url') {
|
||||
bodyParams[key] = processedParams.value[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (Object.keys(bodyParams).length > 0) {
|
||||
enhancedParams.body = bodyParams;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
message: processedString.value,
|
||||
templateParams: {
|
||||
@@ -90,7 +136,7 @@ export default {
|
||||
category: props.template.category,
|
||||
language: props.template.language,
|
||||
namespace: props.template.namespace,
|
||||
processed_params: processedParams.value,
|
||||
processed_params: enhancedParams,
|
||||
},
|
||||
};
|
||||
emit('sendMessage', payload);
|
||||
@@ -103,6 +149,8 @@ export default {
|
||||
variables,
|
||||
templateString,
|
||||
processedString,
|
||||
headerComponent,
|
||||
hasMediaHeader,
|
||||
v$,
|
||||
resetTemplate,
|
||||
sendMessage,
|
||||
@@ -119,26 +167,57 @@ export default {
|
||||
readonly
|
||||
class="template-input"
|
||||
/>
|
||||
<div v-if="variables" class="p-2.5">
|
||||
<p class="text-sm font-semibold mb-2.5">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(variable, key) in processedParams"
|
||||
:key="key"
|
||||
class="items-center flex mb-2.5"
|
||||
>
|
||||
<span
|
||||
class="bg-n-alpha-black2 text-n-slate-12 inline-block rounded-md text-xs py-2.5 px-6"
|
||||
<div v-if="variables || hasMediaHeader" class="p-2.5">
|
||||
<!-- Media Header Section -->
|
||||
<div v-if="hasMediaHeader" class="mb-4">
|
||||
<p class="text-sm font-semibold mb-2.5">
|
||||
{{
|
||||
headerComponent.format.charAt(0) +
|
||||
headerComponent.format.slice(1).toLowerCase()
|
||||
}}
|
||||
Header
|
||||
</p>
|
||||
<div class="items-center flex mb-2.5">
|
||||
<span
|
||||
class="bg-n-alpha-black2 text-n-slate-12 inline-block rounded-md text-xs py-2.5 px-6"
|
||||
>
|
||||
{{ headerComponent.format.toLowerCase() }} URL
|
||||
</span>
|
||||
<woot-input
|
||||
v-model="processedParams.media_url"
|
||||
type="url"
|
||||
class="flex-1 text-sm ml-2.5"
|
||||
:placeholder="`Enter ${headerComponent.format.toLowerCase()} URL`"
|
||||
:styles="{ marginBottom: 0 }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Variables Section -->
|
||||
<div v-if="variables">
|
||||
<p class="text-sm font-semibold mb-2.5">
|
||||
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
|
||||
</p>
|
||||
<div
|
||||
v-for="(variable, key) in processedParams"
|
||||
:key="key"
|
||||
class="items-center flex mb-2.5"
|
||||
>
|
||||
{{ key }}
|
||||
</span>
|
||||
<woot-input
|
||||
v-model="processedParams[key]"
|
||||
type="text"
|
||||
class="flex-1 text-sm ml-2.5"
|
||||
:styles="{ marginBottom: 0 }"
|
||||
/>
|
||||
<!-- Skip media_url as it's handled above -->
|
||||
<template v-if="key !== 'media_url'">
|
||||
<span
|
||||
class="bg-n-alpha-black2 text-n-slate-12 inline-block rounded-md text-xs py-2.5 px-6"
|
||||
>
|
||||
{{ key }}
|
||||
</span>
|
||||
<woot-input
|
||||
v-model="processedParams[key]"
|
||||
type="text"
|
||||
class="flex-1 text-sm ml-2.5"
|
||||
:styles="{ marginBottom: 0 }"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="v$.$dirty && v$.$invalid"
|
||||
|
||||
@@ -260,4 +260,25 @@ export const templates = [
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'order_confirmation',
|
||||
status: 'approved',
|
||||
category: 'TICKET_UPDATE',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
type: 'HEADER',
|
||||
format: 'IMAGE',
|
||||
example: {
|
||||
header_handle: ['https://example.com/shoes.jpg'],
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Hi your order {{1}} is confirmed. Please wait for further updates',
|
||||
type: 'BODY',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -12,15 +12,55 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
end
|
||||
|
||||
def send_template(phone_number, template_info)
|
||||
# Check if this is an interactive template
|
||||
if template_info[:parameters].is_a?(Hash) && template_info[:parameters][:type] == 'interactive'
|
||||
send_interactive_template(phone_number, template_info)
|
||||
else
|
||||
send_regular_template(phone_number, template_info)
|
||||
end
|
||||
end
|
||||
|
||||
def send_regular_template(phone_number, template_info)
|
||||
template_body = template_body_parameters(template_info)
|
||||
|
||||
request_body = {
|
||||
messaging_product: 'whatsapp',
|
||||
recipient_type: 'individual',
|
||||
to: phone_number,
|
||||
type: 'template',
|
||||
template: template_body
|
||||
}
|
||||
|
||||
response = HTTParty.post(
|
||||
"#{phone_id_path}/messages",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
messaging_product: 'whatsapp',
|
||||
to: phone_number,
|
||||
template: template_body_parameters(template_info),
|
||||
type: 'template'
|
||||
}.to_json
|
||||
body: request_body.to_json
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
def send_interactive_template(phone_number, template_info)
|
||||
interactive_data = template_info[:parameters][:interactive_data]
|
||||
|
||||
request_body = {
|
||||
messaging_product: 'whatsapp',
|
||||
recipient_type: 'individual',
|
||||
to: phone_number,
|
||||
type: 'interactive',
|
||||
interactive: {
|
||||
type: interactive_data[:type],
|
||||
body: {
|
||||
text: build_template_body_text(template_info)
|
||||
},
|
||||
action: interactive_data[:action]
|
||||
}
|
||||
}
|
||||
|
||||
response = HTTParty.post(
|
||||
"#{phone_id_path}/messages",
|
||||
headers: api_headers,
|
||||
body: request_body.to_json
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
@@ -137,16 +177,21 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
}
|
||||
|
||||
# Handle enhanced template parameters structure
|
||||
if template_info[:parameters].is_a?(Array) && template_info[:parameters].first.is_a?(Hash) && template_info[:parameters].first.key?(:type) && template_info[:parameters].first[:type] != 'text'
|
||||
# New enhanced format with component structure
|
||||
template_body[:components] = template_info[:parameters]
|
||||
else
|
||||
# Legacy format - maintain backward compatibility
|
||||
template_body[:components] = [{
|
||||
type: 'body',
|
||||
parameters: template_info[:parameters]
|
||||
}]
|
||||
end
|
||||
template_body[:components] = if template_info[:parameters].is_a?(Array) &&
|
||||
template_info[:parameters].first.is_a?(Hash) &&
|
||||
template_info[:parameters].first.key?(:type)
|
||||
# New enhanced format with component structure
|
||||
template_info[:parameters]
|
||||
elsif template_info[:parameters].is_a?(Array)
|
||||
# Legacy format with parameter array
|
||||
[{
|
||||
type: 'body',
|
||||
parameters: template_info[:parameters]
|
||||
}]
|
||||
else
|
||||
# Invalid parameters - this should not happen
|
||||
[]
|
||||
end
|
||||
|
||||
template_body
|
||||
end
|
||||
@@ -176,4 +221,13 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_template_body_text(template_info)
|
||||
# Build the template body text with parameter substitution
|
||||
# This is a simplified version - you might want to enhance this
|
||||
# to properly substitute template variables
|
||||
template_info[:name] || 'Interactive Template'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,32 +75,123 @@ class Whatsapp::TemplateProcessorService
|
||||
template = find_template
|
||||
return if template.blank?
|
||||
|
||||
# Check for interactive templates first
|
||||
if has_interactive_components?(template)
|
||||
process_interactive_template_params(template)
|
||||
# Handle enhanced template parameters structure
|
||||
if template_params['processed_params'].is_a?(Hash) && template_params['processed_params'].key?('body')
|
||||
elsif template_params['processed_params'].is_a?(Hash) && template_params['processed_params'].key?('body')
|
||||
process_enhanced_template_params(template)
|
||||
else
|
||||
# Legacy processing for backward compatibility
|
||||
process_legacy_template_params(template)
|
||||
# Check if we have special header types that need processing
|
||||
header_component = template['components'].find { |c| c['type'] == 'HEADER' }
|
||||
if header_component&.dig('format')&.in?(%w[IMAGE VIDEO DOCUMENT])
|
||||
process_media_template_params(template, header_component)
|
||||
elsif header_component&.dig('format') == 'LOCATION'
|
||||
process_location_template_params(template, header_component)
|
||||
elsif template['category']&.downcase == 'authentication'
|
||||
process_authentication_template_params(template)
|
||||
else
|
||||
process_legacy_template_params(template)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def process_media_template_params(_template, header_component)
|
||||
# For templates with media headers, we need to create proper component parameters
|
||||
components = []
|
||||
|
||||
# Add header component with media parameter
|
||||
media_url = if header_component['example'] && header_component['example']['header_handle']
|
||||
# Template has example media URL, use it as a placeholder
|
||||
header_component['example']['header_handle'].first
|
||||
else
|
||||
# No example, need to provide a media URL parameter
|
||||
# Since we don't have user input, we'll create an empty media parameter
|
||||
'https://example.com/placeholder.jpg' # Placeholder URL
|
||||
end
|
||||
|
||||
components << {
|
||||
type: 'header',
|
||||
parameters: [{
|
||||
:type => header_component['format'].downcase,
|
||||
header_component['format'].downcase => {
|
||||
link: media_url
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
# Add body parameters if any
|
||||
body_params = template_params['processed_params'].map { |_, value| { type: 'text', text: value } }
|
||||
components << { type: 'body', parameters: body_params } if body_params.present?
|
||||
|
||||
@template_params = components
|
||||
end
|
||||
|
||||
def process_location_template_params(_template, header_component)
|
||||
# For templates with location headers
|
||||
components = []
|
||||
|
||||
# Add location header component
|
||||
# Location headers typically include latitude, longitude, name, and address
|
||||
location_params = if template_params['processed_params'].is_a?(Hash) && template_params['processed_params']['header']
|
||||
build_location_parameter(template_params['processed_params']['header'])
|
||||
else
|
||||
# Use example location if available, otherwise create placeholder
|
||||
build_default_location_parameter(header_component)
|
||||
end
|
||||
|
||||
if location_params
|
||||
components << {
|
||||
type: 'header',
|
||||
parameters: [location_params]
|
||||
}
|
||||
end
|
||||
|
||||
# Add body parameters if any
|
||||
if template_params['processed_params'].present? && !template_params['processed_params'].is_a?(Hash)
|
||||
body_params = template_params['processed_params'].map { |_, value| { type: 'text', text: value } }
|
||||
components << { type: 'body', parameters: body_params } if body_params.present?
|
||||
end
|
||||
|
||||
@template_params = components
|
||||
end
|
||||
|
||||
def process_authentication_template_params(_template)
|
||||
# Authentication templates typically have OTP codes and expiration times
|
||||
components = []
|
||||
|
||||
# Process body parameters for authentication templates
|
||||
if template_params['processed_params'].present?
|
||||
body_params = if template_params['processed_params'].is_a?(Hash)
|
||||
process_authentication_body_params(template_params['processed_params'])
|
||||
else
|
||||
template_params['processed_params'].map { |_, value| { type: 'text', text: value } }
|
||||
end
|
||||
components << { type: 'body', parameters: body_params } if body_params.present?
|
||||
end
|
||||
|
||||
@template_params = components
|
||||
end
|
||||
|
||||
def process_enhanced_template_params(_template)
|
||||
processed_params = template_params['processed_params']
|
||||
components = []
|
||||
|
||||
# Process body parameters
|
||||
if processed_params['body'].present?
|
||||
body_params = processed_params['body'].map { |_, value| build_parameter(value) }
|
||||
components << { type: 'body', parameters: body_params }
|
||||
end
|
||||
|
||||
# Process header parameters
|
||||
# Process header parameters first (important for WhatsApp API order)
|
||||
if processed_params['header'].present?
|
||||
header_params = processed_params['header'].filter_map do |key, value|
|
||||
header_params = []
|
||||
|
||||
processed_params['header'].each do |key, value|
|
||||
next if value.blank?
|
||||
|
||||
if key == 'media_url' && processed_params['header']['media_type'].present?
|
||||
build_media_parameter(value, processed_params['header']['media_type'])
|
||||
else
|
||||
build_parameter(value)
|
||||
media_param = build_media_parameter(value, processed_params['header']['media_type'])
|
||||
header_params << media_param if media_param
|
||||
elsif key == 'location' && processed_params['header']['location_type'] == 'location'
|
||||
location_param = build_location_parameter(value)
|
||||
header_params << location_param if location_param
|
||||
elsif !%w[media_type location_type].include?(key)
|
||||
header_params << build_parameter(value)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -108,15 +199,38 @@ class Whatsapp::TemplateProcessorService
|
||||
components << { type: 'header', parameters: header_params } if header_params.present?
|
||||
end
|
||||
|
||||
# Process body parameters
|
||||
if processed_params['body'].present?
|
||||
body_params = processed_params['body'].filter_map do |key, value|
|
||||
next if value.blank?
|
||||
|
||||
# Handle special authentication parameters
|
||||
if key == 'otp_code'
|
||||
build_authentication_parameter(value, 'otp')
|
||||
elsif key == 'expiry_minutes'
|
||||
build_authentication_parameter(value, 'expiry')
|
||||
else
|
||||
build_parameter(value)
|
||||
end
|
||||
end
|
||||
components << { type: 'body', parameters: body_params } if body_params.present?
|
||||
end
|
||||
|
||||
# Process footer parameters (rarely used but supported)
|
||||
if processed_params['footer'].present?
|
||||
footer_params = processed_params['footer'].map { |_, value| build_parameter(value) }
|
||||
components << { type: 'footer', parameters: footer_params }
|
||||
footer_params = processed_params['footer'].filter_map do |_, value|
|
||||
next if value.blank?
|
||||
|
||||
build_parameter(value)
|
||||
end
|
||||
components << { type: 'footer', parameters: footer_params } if footer_params.present?
|
||||
end
|
||||
|
||||
# Process button parameters
|
||||
if processed_params['buttons'].present?
|
||||
button_params = processed_params['buttons'].map.with_index do |button, index|
|
||||
button_params = processed_params['buttons'].filter_map.with_index do |button, index|
|
||||
next if button.blank? || button['parameter'].blank?
|
||||
|
||||
{
|
||||
type: 'button',
|
||||
sub_type: button['type'] || 'url',
|
||||
@@ -124,32 +238,33 @@ class Whatsapp::TemplateProcessorService
|
||||
parameters: [build_button_parameter(button)]
|
||||
}
|
||||
end
|
||||
components.concat(button_params)
|
||||
components.concat(button_params) if button_params.present?
|
||||
end
|
||||
|
||||
components
|
||||
@template_params = components
|
||||
end
|
||||
|
||||
def process_legacy_template_params(template)
|
||||
parameter_format = template['parameter_format']
|
||||
|
||||
if parameter_format == 'NAMED'
|
||||
template_params['processed_params']&.map { |key, value| { type: 'text', parameter_name: key, text: value } }
|
||||
else
|
||||
template_params['processed_params']&.map { |_, value| { type: 'text', text: value } }
|
||||
end
|
||||
@template_params = if parameter_format == 'NAMED'
|
||||
template_params['processed_params']&.map { |key, value| { type: 'text', parameter_name: key, text: value } }
|
||||
else
|
||||
template_params['processed_params']&.map { |_, value| { type: 'text', text: value } }
|
||||
end
|
||||
@template_params
|
||||
end
|
||||
|
||||
def build_parameter(value)
|
||||
case value
|
||||
when String
|
||||
sanitized_value = sanitize_parameter(value)
|
||||
if sanitized_value.match?(%r{^https?://})
|
||||
validate_url(sanitized_value)
|
||||
# URL parameter (for media or documents)
|
||||
{ type: 'image', image: { link: sanitized_value } }
|
||||
# Check if this is rich text formatting
|
||||
if has_rich_formatting?(sanitized_value)
|
||||
build_rich_text_parameter(sanitized_value)
|
||||
else
|
||||
# Text parameter
|
||||
# For regular template parameters, always treat as text
|
||||
# Media parameters are handled separately via build_media_parameter
|
||||
{ type: 'text', text: sanitized_value }
|
||||
end
|
||||
when Hash
|
||||
@@ -194,6 +309,8 @@ class Whatsapp::TemplateProcessorService
|
||||
end
|
||||
|
||||
def build_button_parameter(button)
|
||||
return { type: 'text', text: '' } if button.blank? || button['parameter'].blank?
|
||||
|
||||
case button['type']
|
||||
when 'copy_code'
|
||||
coupon_code = button['parameter'].to_s.strip
|
||||
@@ -205,7 +322,8 @@ class Whatsapp::TemplateProcessorService
|
||||
coupon_code: coupon_code
|
||||
}
|
||||
else
|
||||
build_parameter(button['parameter'])
|
||||
# For URL buttons and other button types, treat parameter as text
|
||||
{ type: 'text', text: button['parameter'].to_s.strip }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -217,11 +335,13 @@ class Whatsapp::TemplateProcessorService
|
||||
end
|
||||
|
||||
def validate_url(url)
|
||||
return if url.blank?
|
||||
|
||||
uri = URI.parse(url)
|
||||
raise ArgumentError, 'Invalid URL scheme' unless %w[http https].include?(uri.scheme)
|
||||
raise ArgumentError, 'URL too long' if url.length > 2000
|
||||
rescue URI::InvalidURIError
|
||||
raise ArgumentError, 'Invalid URL format'
|
||||
raise ArgumentError, "Invalid URL scheme: #{uri.scheme}. Only http and https are allowed" unless %w[http https].include?(uri.scheme)
|
||||
raise ArgumentError, 'URL too long (max 2000 characters)' if url.length > 2000
|
||||
rescue URI::InvalidURIError => e
|
||||
raise ArgumentError, "Invalid URL format: #{e.message}. Please enter a valid image URL like https://example.com/image.jpg"
|
||||
end
|
||||
|
||||
def build_media_parameter(url, media_type)
|
||||
@@ -257,6 +377,114 @@ class Whatsapp::TemplateProcessorService
|
||||
end
|
||||
end
|
||||
|
||||
def build_location_parameter(location_data)
|
||||
# Location parameter for header components
|
||||
# Can be a hash with lat/lng or a string address
|
||||
case location_data
|
||||
when Hash
|
||||
# Structured location data
|
||||
validate_location_data(location_data)
|
||||
{
|
||||
type: 'location',
|
||||
location: {
|
||||
latitude: location_data['latitude'].to_f,
|
||||
longitude: location_data['longitude'].to_f,
|
||||
name: location_data['name'].to_s.strip,
|
||||
address: location_data['address'].to_s.strip
|
||||
}
|
||||
}
|
||||
when String
|
||||
# Address string - parse or use as name
|
||||
address = sanitize_parameter(location_data)
|
||||
{
|
||||
type: 'location',
|
||||
location: {
|
||||
latitude: 0.0,
|
||||
longitude: 0.0,
|
||||
name: address,
|
||||
address: address
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def build_default_location_parameter(header_component)
|
||||
# Build default location from template example or placeholder
|
||||
if header_component['example'] && header_component['example']['header_handle']
|
||||
example_location = header_component['example']['header_handle'].first
|
||||
{
|
||||
type: 'location',
|
||||
location: {
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
name: example_location || 'Business Location',
|
||||
address: example_location || 'San Francisco, CA'
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
type: 'location',
|
||||
location: {
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
name: 'Business Location',
|
||||
address: 'San Francisco, CA'
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def build_authentication_parameter(value, param_type)
|
||||
# Authentication-specific parameters
|
||||
sanitized_value = sanitize_parameter(value)
|
||||
|
||||
case param_type
|
||||
when 'otp'
|
||||
# OTP code - typically 4-8 digits
|
||||
raise ArgumentError, 'OTP code must be numeric' unless sanitized_value.match?(/\A\d+\z/)
|
||||
raise ArgumentError, 'OTP code must be 4-8 digits' unless sanitized_value.length.between?(4, 8)
|
||||
|
||||
{ type: 'text', text: sanitized_value }
|
||||
when 'expiry'
|
||||
# Expiry time in minutes
|
||||
expiry_minutes = sanitized_value.to_i
|
||||
raise ArgumentError, 'Expiry minutes must be a positive number' unless expiry_minutes > 0
|
||||
|
||||
{ type: 'text', text: expiry_minutes.to_s }
|
||||
else
|
||||
{ type: 'text', text: sanitized_value }
|
||||
end
|
||||
end
|
||||
|
||||
def process_authentication_body_params(processed_params)
|
||||
# Process authentication-specific body parameters
|
||||
processed_params.filter_map do |key, value|
|
||||
next if value.blank?
|
||||
|
||||
case key
|
||||
when 'otp_code'
|
||||
build_authentication_parameter(value, 'otp')
|
||||
when 'expiry_minutes'
|
||||
build_authentication_parameter(value, 'expiry')
|
||||
else
|
||||
build_parameter(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate_location_data(location_data)
|
||||
required_fields = %w[latitude longitude]
|
||||
missing_fields = required_fields.reject { |field| location_data.key?(field) }
|
||||
|
||||
raise ArgumentError, "Missing required location fields: #{missing_fields.join(', ')}" if missing_fields.any?
|
||||
|
||||
lat = location_data['latitude'].to_f
|
||||
lng = location_data['longitude'].to_f
|
||||
|
||||
raise ArgumentError, 'Latitude must be between -90 and 90' unless lat.between?(-90, 90)
|
||||
raise ArgumentError, 'Longitude must be between -180 and 180' unless lng.between?(-180, 180)
|
||||
end
|
||||
|
||||
def validated_body_object(template)
|
||||
# we don't care if its not approved template
|
||||
return if template['status'] != 'approved'
|
||||
@@ -265,4 +493,175 @@ class Whatsapp::TemplateProcessorService
|
||||
# we don't support other forms of templates
|
||||
template['components'].find { |obj| obj['type'] == 'BODY' && obj.key?('text') }
|
||||
end
|
||||
|
||||
def has_interactive_components?(template)
|
||||
# Check if template has interactive buttons like quick replies, call-to-actions, etc.
|
||||
interactive_types = %w[quick_reply url phone_number copy_code list catalog_browse]
|
||||
template['components']&.any? do |component|
|
||||
(component['type'] == 'BUTTONS' && component['buttons']&.any? { |button| interactive_types.include?(button['type']) }) ||
|
||||
(component['type'] == 'LIST' && component['sections'].present?) ||
|
||||
(component['type'] == 'PRODUCT' && component['product_id'].present?) ||
|
||||
(component['type'] == 'CATALOG' && component['catalog_id'].present?)
|
||||
end
|
||||
end
|
||||
|
||||
def process_interactive_template_params(template)
|
||||
components = []
|
||||
|
||||
# Process body parameters
|
||||
if template_params['processed_params'].present?
|
||||
body_params = template_params['processed_params'].map { |_, value| { type: 'text', text: value } }
|
||||
components << { type: 'body', parameters: body_params } if body_params.present?
|
||||
end
|
||||
|
||||
@template_params = {
|
||||
type: 'interactive',
|
||||
components: components,
|
||||
interactive_data: extract_interactive_data(template)
|
||||
}
|
||||
end
|
||||
|
||||
def extract_interactive_data(template)
|
||||
# Check if this is a catalog browse template
|
||||
catalog_component = template['components'].find { |c| c['type'] == 'CATALOG' }
|
||||
return extract_catalog_data(catalog_component) if catalog_component&.dig('catalog_id')
|
||||
|
||||
# Check if this is a product template
|
||||
product_component = template['components'].find { |c| c['type'] == 'PRODUCT' }
|
||||
return extract_product_data(product_component) if product_component&.dig('product_id')
|
||||
|
||||
# Check if this is a list template
|
||||
list_component = template['components'].find { |c| c['type'] == 'LIST' }
|
||||
return extract_list_data(list_component) if list_component&.dig('sections')
|
||||
|
||||
# Default to button template
|
||||
interactive_data = { type: 'button', action: { buttons: [] } }
|
||||
|
||||
button_component = template['components'].find { |c| c['type'] == 'BUTTONS' }
|
||||
return interactive_data unless button_component&.dig('buttons')
|
||||
|
||||
buttons = button_component['buttons'].map.with_index do |button, index|
|
||||
# Process dynamic button text if parameters are provided
|
||||
button_text = process_dynamic_button_text(button['text'] || '', index)
|
||||
|
||||
case button['type']
|
||||
when 'quick_reply'
|
||||
{
|
||||
type: 'reply',
|
||||
reply: {
|
||||
id: "reply_#{index}",
|
||||
title: button_text || "Reply #{index + 1}"
|
||||
}
|
||||
}
|
||||
when 'url'
|
||||
{
|
||||
type: 'reply',
|
||||
reply: {
|
||||
id: "url_#{index}",
|
||||
title: button_text || 'Visit Link'
|
||||
}
|
||||
}
|
||||
when 'phone_number'
|
||||
{
|
||||
type: 'reply',
|
||||
reply: {
|
||||
id: "call_#{index}",
|
||||
title: button_text || 'Call Now'
|
||||
}
|
||||
}
|
||||
end
|
||||
end.compact.first(3) # WhatsApp allows max 3 reply buttons
|
||||
|
||||
interactive_data[:action][:buttons] = buttons
|
||||
interactive_data
|
||||
end
|
||||
|
||||
def extract_list_data(list_component)
|
||||
interactive_data = {
|
||||
type: 'list',
|
||||
action: {
|
||||
button: 'Select an Option',
|
||||
sections: []
|
||||
}
|
||||
}
|
||||
|
||||
sections = list_component['sections'].map.with_index do |section, section_index|
|
||||
rows = section['rows']&.map&.with_index do |row, row_index|
|
||||
{
|
||||
id: "row_#{section_index}_#{row_index}",
|
||||
title: row['title'] || "Option #{row_index + 1}",
|
||||
description: row['description'] || nil
|
||||
}
|
||||
end&.first(10) || [] # WhatsApp allows max 10 rows per section
|
||||
|
||||
{
|
||||
title: section['title'] || "Section #{section_index + 1}",
|
||||
rows: rows
|
||||
}
|
||||
end.first(10) # WhatsApp allows max 10 sections
|
||||
|
||||
interactive_data[:action][:sections] = sections
|
||||
interactive_data
|
||||
end
|
||||
|
||||
def extract_product_data(product_component)
|
||||
{
|
||||
type: 'product',
|
||||
action: {
|
||||
catalog_id: product_component['catalog_id'] || '',
|
||||
product_retailer_id: product_component['product_id']
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def extract_catalog_data(catalog_component)
|
||||
{
|
||||
type: 'product_list',
|
||||
action: {
|
||||
catalog_id: catalog_component['catalog_id'],
|
||||
sections: [
|
||||
{
|
||||
title: catalog_component['title'] || 'Browse Products',
|
||||
product_items: catalog_component['products']&.map do |product|
|
||||
{
|
||||
product_retailer_id: product['id'] || product['product_id']
|
||||
}
|
||||
end || []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def process_dynamic_button_text(button_text, button_index)
|
||||
return button_text unless button_text.include?('{{')
|
||||
|
||||
# Replace button text variables with provided parameters
|
||||
if template_params['processed_params'].present?
|
||||
button_params = template_params['processed_params']['buttons']
|
||||
if button_params.is_a?(Array) && button_params[button_index].present?
|
||||
button_param = button_params[button_index]['parameter'] || button_params[button_index]['text']
|
||||
if button_param.present?
|
||||
# Replace {{1}}, {{variable}}, etc. with the provided parameter
|
||||
button_text = button_text.gsub(/\{\{[^}]+\}\}/, button_param.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
button_text
|
||||
end
|
||||
|
||||
def has_rich_formatting?(text)
|
||||
# Check if text contains WhatsApp rich formatting markers
|
||||
text.match?(/\*[^*]+\*/) || # Bold: *text*
|
||||
text.match?(/_[^_]+_/) || # Italic: _text_
|
||||
text.match?(/~[^~]+~/) || # Strikethrough: ~text~
|
||||
text.match?(/```[^`]+```/) # Monospace: ```text```
|
||||
end
|
||||
|
||||
def build_rich_text_parameter(text)
|
||||
# WhatsApp supports rich text formatting in templates
|
||||
# This preserves the formatting markers for the API
|
||||
{ type: 'text', text: text }
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user