feat: Add support for Whatsapp template messages in the UI (#4711)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Fayaz Ahmed 2022-06-07 17:33:33 +05:30 committed by GitHub
parent 56f668db6b
commit bad24f97ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 733 additions and 54 deletions

View file

@ -73,6 +73,10 @@ class Messages::MessageBuilder
@params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {} @params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {}
end end
def template_params
@params[:template_params].present? ? { additional_attributes: { template_params: JSON.parse(@params[:template_params].to_json) } } : {}
end
def message_sender def message_sender
return if @params[:sender_type] != 'AgentBot' return if @params[:sender_type] != 'AgentBot'
@ -91,6 +95,6 @@ class Messages::MessageBuilder
items: @items, items: @items,
in_reply_to: @in_reply_to, in_reply_to: @in_reply_to,
echo_id: @params[:echo_id] echo_id: @params[:echo_id]
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id) }.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
end end
end end

View file

@ -10,6 +10,7 @@ export const buildCreatePayload = ({
files, files,
ccEmails = '', ccEmails = '',
bccEmails = '', bccEmails = '',
templateParams,
}) => { }) => {
let payload; let payload;
if (files && files.length !== 0) { if (files && files.length !== 0) {
@ -32,6 +33,7 @@ export const buildCreatePayload = ({
content_attributes: contentAttributes, content_attributes: contentAttributes,
cc_emails: ccEmails, cc_emails: ccEmails,
bcc_emails: bccEmails, bcc_emails: bccEmails,
template_params: templateParams,
}; };
} }
return payload; return payload;
@ -51,6 +53,7 @@ class MessageApi extends ApiClient {
files, files,
ccEmails = '', ccEmails = '',
bccEmails = '', bccEmails = '',
templateParams,
}) { }) {
return axios({ return axios({
method: 'post', method: 'post',
@ -63,6 +66,7 @@ class MessageApi extends ApiClient {
files, files,
ccEmails, ccEmails,
bccEmails, bccEmails,
templateParams,
}), }),
}); });
} }

View file

@ -98,4 +98,7 @@ export default {
width: 48rem; width: 48rem;
} }
} }
.modal-big {
width: 60%;
}
</style> </style>

View file

@ -79,6 +79,16 @@
:title="signatureToggleTooltip" :title="signatureToggleTooltip"
@click="toggleMessageSignature" @click="toggleMessageSignature"
/> />
<woot-button
v-if="showWhatsappTemplatesButton"
v-tooltip.top-end="'Whatsapp Templates'"
icon="whatsapp"
color-scheme="secondary"
variant="smooth"
size="small"
:title="'Whatsapp Templates'"
@click="$emit('selectWhatsappTemplate')"
/>
<transition name="modal-fade"> <transition name="modal-fade">
<div <div
v-show="$refs.upload && $refs.upload.dropActive" v-show="$refs.upload && $refs.upload.dropActive"
@ -218,6 +228,10 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
hasWhatsappTemplates: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
isNote() { isNote() {
@ -261,6 +275,9 @@ export default {
showMessageSignatureButton() { showMessageSignatureButton() {
return !this.isPrivate && this.isAnEmailChannel; return !this.isPrivate && this.isAnEmailChannel;
}, },
showWhatsappTemplatesButton() {
return !this.isOnPrivateNote && this.hasWhatsappTemplates;
},
sendWithSignature() { sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings; const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled; return isEnabled;

View file

@ -101,7 +101,7 @@
:toggle-audio-recorder="toggleAudioRecorder" :toggle-audio-recorder="toggleAudioRecorder"
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause" :toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
:show-emoji-picker="showEmojiPicker" :show-emoji-picker="showEmojiPicker"
:on-send="sendMessage" :on-send="onSendReply"
:is-send-disabled="isReplyButtonDisabled" :is-send-disabled="isReplyButtonDisabled"
:recording-audio-duration-text="recordingAudioDurationText" :recording-audio-duration-text="recordingAudioDurationText"
:recording-audio-state="recordingAudioState" :recording-audio-state="recordingAudioState"
@ -112,7 +112,16 @@
:enable-rich-editor="isRichEditorEnabled" :enable-rich-editor="isRichEditorEnabled"
:enter-to-send-enabled="enterToSendEnabled" :enter-to-send-enabled="enterToSendEnabled"
:enable-multiple-file-upload="enableMultipleFileUpload" :enable-multiple-file-upload="enableMultipleFileUpload"
:has-whatsapp-templates="hasWhatsappTemplates"
@toggleEnterToSend="toggleEnterToSend" @toggleEnterToSend="toggleEnterToSend"
@selectWhatsappTemplate="openWhatsappTemplateModal"
/>
<whatsapp-templates
:inbox-id="inbox.id"
:show="showWhatsAppTemplatesModal"
@close="hideWhatsappTemplatesModal"
@on-send="onSendWhatsAppReply"
@cancel="hideWhatsappTemplatesModal"
/> />
</div> </div>
</template> </template>
@ -137,7 +146,7 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages'; import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import { import {
isEscape, isEscape,
isEnter, isEnter,
@ -162,6 +171,7 @@ export default {
WootMessageEditor, WootMessageEditor,
WootAudioRecorder, WootAudioRecorder,
Banner, Banner,
WhatsappTemplates,
}, },
mixins: [ mixins: [
clickaway, clickaway,
@ -201,6 +211,7 @@ export default {
hasSlashCommand: false, hasSlashCommand: false,
bccEmails: '', bccEmails: '',
ccEmails: '', ccEmails: '',
showWhatsAppTemplatesModal: false,
}; };
}, },
computed: { computed: {
@ -212,7 +223,6 @@ export default {
globalConfig: 'globalConfig/get', globalConfig: 'globalConfig/get',
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
}), }),
showRichContentEditor() { showRichContentEditor() {
if (this.isOnPrivateNote) { if (this.isOnPrivateNote) {
return true; return true;
@ -256,7 +266,9 @@ export default {
return false; return false;
}, },
hasWhatsappTemplates() {
return !!this.inbox.message_templates;
},
enterToSendEnabled() { enterToSendEnabled() {
return !!this.uiSettings.enter_to_send_enabled; return !!this.uiSettings.enter_to_send_enabled;
}, },
@ -484,7 +496,7 @@ export default {
hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused; hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused;
if (shouldSendMessage) { if (shouldSendMessage) {
e.preventDefault(); e.preventDefault();
this.sendMessage(); this.onSendReply();
} }
} else if (hasPressedCommandPlusKKey(e)) { } else if (hasPressedCommandPlusKKey(e)) {
this.openCommandBar(); this.openCommandBar();
@ -497,6 +509,12 @@ export default {
toggleEnterToSend(enterToSendEnabled) { toggleEnterToSend(enterToSendEnabled) {
this.updateUISettings({ enter_to_send_enabled: enterToSendEnabled }); this.updateUISettings({ enter_to_send_enabled: enterToSendEnabled });
}, },
openWhatsappTemplateModal() {
this.showWhatsAppTemplatesModal = true;
},
hideWhatsappTemplatesModal() {
this.showWhatsAppTemplatesModal = false;
},
onClickSelfAssign() { onClickSelfAssign() {
const { const {
account_id, account_id,
@ -520,7 +538,7 @@ export default {
}; };
this.assignedAgent = selfAssign; this.assignedAgent = selfAssign;
}, },
async sendMessage() { async onSendReply() {
if (this.isReplyButtonDisabled) { if (this.isReplyButtonDisabled) {
return; return;
} }
@ -531,6 +549,12 @@ export default {
} }
const messagePayload = this.getMessagePayload(newMessage); const messagePayload = this.getMessagePayload(newMessage);
this.clearMessage(); this.clearMessage();
this.sendMessage(messagePayload);
this.hideEmojiPicker();
this.$emit('update:popoutReplyBox', false);
}
},
async sendMessage(messagePayload) {
try { try {
await this.$store.dispatch( await this.$store.dispatch(
'createPendingMessageAndSend', 'createPendingMessageAndSend',
@ -539,13 +563,16 @@ export default {
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE); bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
} catch (error) { } catch (error) {
const errorMessage = const errorMessage =
error?.response?.data?.error || error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
this.$t('CONVERSATION.MESSAGE_ERROR');
this.showAlert(errorMessage); this.showAlert(errorMessage);
} }
this.hideEmojiPicker(); },
this.$emit('update:popoutReplyBox', false); async onSendWhatsAppReply(messagePayload) {
} this.sendMessage({
conversationId: this.currentChat.id,
...messagePayload,
});
this.hideWhatsappTemplatesModal();
}, },
replaceText(message) { replaceText(message) {
setTimeout(() => { setTimeout(() => {

View file

@ -0,0 +1,76 @@
<template>
<woot-modal :show.sync="show" :on-close="onClose" size="modal-big">
<woot-modal-header
:header-title="$t('WHATSAPP_TEMPLATES.MODAL.TITLE')"
:header-content="modalHeaderContent"
/>
<div class="row modal-content">
<templates-picker
v-if="!selectedWaTemplate"
:inbox-id="inboxId"
@onSelect="pickTemplate"
/>
<template-parser
v-else
:template="selectedWaTemplate"
@resetTemplate="onResetTemplate"
@sendMessage="onSendMessage"
/>
</div>
</woot-modal>
</template>
<script>
import TemplatesPicker from './TemplatesPicker.vue';
import TemplateParser from './TemplateParser.vue';
export default {
components: {
TemplatesPicker,
TemplateParser,
},
props: {
inboxId: {
type: Number,
default: undefined,
},
show: {
type: Boolean,
default: true,
},
},
data() {
return {
selectedWaTemplate: null,
};
},
computed: {
modalHeaderContent() {
return this.selectedWaTemplate
? this.$t('WHATSAPP_TEMPLATES.MODAL.TEMPLATE_SELECTED_SUBTITLE', {
templateName: this.selectedWaTemplate.name,
})
: this.$t('WHATSAPP_TEMPLATES.MODAL.SUBTITLE');
},
},
methods: {
pickTemplate(template) {
this.selectedWaTemplate = template;
},
onResetTemplate() {
this.selectedWaTemplate = null;
},
onSendMessage(message) {
this.$emit('on-send', message);
},
onClose() {
this.$emit('cancel');
},
},
};
</script>
<style scoped>
.modal-content {
padding: 2.5rem 3.2rem;
}
</style>

View file

@ -0,0 +1,183 @@
<template>
<div class="medium-12 columns">
<textarea
v-model="processedString"
rows="4"
readonly
class="template-input"
></textarea>
<div>
<div class="template__variables-container">
<p class="variables-label">
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
</p>
<div
v-for="(variable, key) in processedParams"
:key="key"
class="template__variable-item"
>
<span class="variable-label">
{{ key }}
</span>
<woot-input
v-model="processedParams[key]"
type="text"
class="variable-input"
:styles="{ marginBottom: 0 }"
/>
</div>
<p v-if="showRequiredMessage" class="error">
{{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }}
</p>
</div>
</div>
<footer>
<woot-button variant="smooth" @click="$emit('resetTemplate')">
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
</woot-button>
<woot-button @click="sendMessage">
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
</woot-button>
</footer>
</div>
</template>
<script>
import { required } from 'vuelidate/lib/validators';
const allKeysRequired = value => {
const keys = Object.keys(value);
return keys.every(key => value[key]);
};
export default {
props: {
template: {
type: Object,
default: () => {},
},
},
validations: {
processedParams: {
required,
allKeysRequired,
},
},
data() {
return {
message: this.template.message,
processedParams: {},
showRequiredMessage: false,
};
},
computed: {
variables() {
const variables = this.templateString.match(/{{([^}]+)}}/g);
return variables;
},
templateString() {
return this.template.components.find(
component => component.type === 'BODY'
).text;
},
processedString() {
return this.templateString.replace(/{{([^}]+)}}/g, (match, variable) => {
const variableKey = this.processVariable(variable);
return this.processedParams[variableKey] || `{{${variable}}}`;
});
},
},
mounted() {
this.generateVariables();
},
methods: {
sendMessage() {
this.$v.$touch();
if (this.$v.$invalid) {
this.showRequiredMessage = true;
return;
}
const message = {
message: this.processedString,
templateParams: {
name: this.template.name,
category: this.template.category,
language: this.template.language,
namespace: this.template.namespace,
processed_params: this.processedParams,
},
};
this.$emit('sendMessage', message);
},
processVariable(str) {
return str.replace(/{{|}}/g, '');
},
generateVariables() {
const templateString = this.template.components.find(
component => component.type === 'BODY'
).text;
const variables = templateString.match(/{{([^}]+)}}/g).map(variable => {
return this.processVariable(variable);
});
this.processedParams = variables.reduce((acc, variable) => {
acc[variable] = '';
return acc;
}, {});
},
},
};
</script>
<style scoped lang="scss">
.template__variables-container {
padding: var(--space-one);
}
.variables-label {
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
margin-bottom: var(--space-one);
}
.template__variable-item {
align-items: center;
display: flex;
margin-bottom: var(--space-one);
.label {
font-size: var(--font-size-mini);
}
.variable-input {
flex: 1;
font-size: var(--font-size-small);
margin-left: var(--space-one);
}
.variable-label {
background-color: var(--s-75);
border-radius: var(--border-radius-normal);
display: inline-block;
font-size: var(--font-size-mini);
padding: var(--space-one) var(--space-medium);
}
}
footer {
display: flex;
justify-content: flex-end;
button {
margin-left: var(--space-one);
}
}
.error {
background-color: var(--r-100);
border-radius: var(--border-radius-normal);
color: var(--r-800);
padding: var(--space-one);
text-align: center;
}
.template-input {
background-color: var(--s-25);
}
</style>

View file

@ -0,0 +1,163 @@
<template>
<div class="medium-12 columns">
<div class="templates__list-search">
<fluent-icon icon="search" class="search-icon" size="16" />
<input
ref="search"
v-model="query"
type="search"
:placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
class="templates__search-input"
/>
</div>
<div class="template__list-container">
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
<button
class="template__list-item"
@click="$emit('onSelect', template)"
>
<div>
<div class="flex-between">
<p class="label-title">
{{ template.name }}
</p>
<span class="label-lang label">
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} :
{{ template.language }}
</span>
</div>
<div>
<p class="strong">
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }}
</p>
<p class="label-body">{{ getTemplatebody(template) }}</p>
</div>
<div class="label-category">
<p class="strong">
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }}
</p>
<p>{{ template.category }}</p>
</div>
</div>
</button>
<hr v-if="i != filteredTemplateMessages.length - 1" :key="`hr-${i}`" />
</div>
<div v-if="!filteredTemplateMessages.length">
<p>
{{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
<strong>{{ query }}</strong>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
inboxId: {
type: Number,
default: undefined,
},
},
data() {
return {
query: '',
};
},
computed: {
whatsAppTemplateMessages() {
return this.$store.getters['inboxes/getWhatsAppTemplates'](this.inboxId);
},
filteredTemplateMessages() {
return this.whatsAppTemplateMessages.filter(template =>
template.name.toLowerCase().includes(this.query.toLowerCase())
);
},
},
methods: {
getTemplatebody(template) {
return template.components.find(component => component.type === 'BODY')
.text;
},
},
};
</script>
<style scoped lang="scss">
.flex-between {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-one);
}
.templates__list-search {
align-items: center;
background-color: var(--s-25);
border-radius: var(--border-radius-medium);
border: 1px solid var(--s-100);
display: flex;
margin-bottom: var(--space-one);
padding: 0 var(--space-one);
.search-icon {
color: var(--s-400);
}
.templates__search-input {
background-color: transparent;
border: var(--space-large);
font-size: var(--font-size-mini);
height: unset;
margin: var(--space-zero);
}
}
.template__list-container {
background-color: var(--s-25);
border-radius: var(--border-radius-medium);
max-height: 30rem;
overflow-y: auto;
padding: var(--space-one);
.template__list-item {
border-radius: var(--border-radius-medium);
cursor: pointer;
display: block;
padding: var(--space-one);
text-align: left;
width: 100%;
&:hover {
background-color: var(--w-50);
}
.label-title {
font-size: var(--font-size-small);
}
.label-category {
margin-top: var(--space-two);
span {
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
}
}
.label-body {
font-family: monospace;
}
}
}
.strong {
font-size: var(--font-size-mini);
font-weight: var(--font-weight-bold);
}
hr {
border-bottom: 1px solid var(--s-100);
margin: var(--space-one) auto;
max-width: 95%;
}
</style>

View file

@ -7,7 +7,7 @@
<fluent-icon <fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.SENT')" v-tooltip.top-start="$t('CHAT_LIST.SENT')"
icon="checkmark" icon="checkmark"
size="16" size="14"
/> />
</span> </span>
<fluent-icon <fluent-icon
@ -165,7 +165,11 @@ export default {
return `https://www.instagram.com/stories/${storySender}/${storyId}`; return `https://www.instagram.com/stories/${storySender}/${storyId}`;
}, },
showSentIndicator() { showSentIndicator() {
return this.isOutgoing && this.sourceId && this.isAnEmailChannel; return (
this.isOutgoing &&
this.sourceId &&
(this.isAnEmailChannel || this.isAWhatsappChannel)
);
}, },
}, },
methods: { methods: {

View file

@ -6,6 +6,7 @@
:type="type" :type="type"
:placeholder="placeholder" :placeholder="placeholder"
:readonly="readonly" :readonly="readonly"
:style="styles"
@input="onChange" @input="onChange"
@blur="onBlur" @blur="onBlur"
/> />
@ -47,6 +48,10 @@ export default {
type: Boolean, type: Boolean,
deafaut: false, deafaut: false,
}, },
styles: {
type: Object,
default: () => {},
},
}, },
methods: { methods: {
onChange(e) { onChange(e) {

View file

@ -21,6 +21,7 @@ import { default as _setNewPassword } from './setNewPassword.json';
import { default as _settings } from './settings.json'; import { default as _settings } from './settings.json';
import { default as _signup } from './signup.json'; import { default as _signup } from './signup.json';
import { default as _teamsSettings } from './teamsSettings.json'; import { default as _teamsSettings } from './teamsSettings.json';
import { default as _whatsappTemplates } from './whatsappTemplates.json';
import { default as _bulkActions } from './bulkActions.json'; import { default as _bulkActions } from './bulkActions.json';
export default { export default {
@ -47,5 +48,6 @@ export default {
..._settings, ..._settings,
..._signup, ..._signup,
..._teamsSettings, ..._teamsSettings,
..._whatsappTemplates,
..._bulkActions, ..._bulkActions,
}; };

View file

@ -0,0 +1,25 @@
{
"WHATSAPP_TEMPLATES": {
"MODAL": {
"TITLE": "Whatsapp Templates",
"SUBTITLE": "Select the whatsapp template you want to send",
"TEMPLATE_SELECTED_SUBTITLE": "Process %{templateName}"
},
"PICKER": {
"SEARCH_PLACEHOLDER": "Search Templates",
"NO_TEMPLATES_FOUND": "No templates found for",
"LABELS": {
"LANGUAGE": "Language",
"TEMPLATE_BODY": "Template Body",
"CATEGORY": "Category"
}
},
"PARSER": {
"VARIABLES_LABEL": "Variables",
"VARIABLE_PLACEHOLDER": "Enter %{variable} value",
"GO_BACK_LABEL": "Go Back",
"SEND_MESSAGE_LABEL": "Send Message",
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
}
}
}

View file

@ -1,12 +1,12 @@
<template> <template>
<form class="conversation--form" @submit.prevent="handleSubmit"> <form class="conversation--form" @submit.prevent="onFormSubmit">
<div v-if="showNoInboxAlert" class="callout warning"> <div v-if="showNoInboxAlert" class="callout warning">
<p> <p>
{{ $t('NEW_CONVERSATION.NO_INBOX') }} {{ $t('NEW_CONVERSATION.NO_INBOX') }}
</p> </p>
</div> </div>
<div v-else> <div v-else>
<div class="row"> <div class="row gutter-small">
<div class="columns"> <div class="columns">
<label :class="{ error: $v.targetInbox.$error }"> <label :class="{ error: $v.targetInbox.$error }">
{{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }} {{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }}
@ -88,6 +88,12 @@
</label> </label>
</label> </label>
</div> </div>
<whatsapp-templates
v-else-if="hasWhatsappTemplates"
:inbox-id="selectedInbox.inbox.id"
@on-select-template="toggleWaTemplate"
@on-send="onSendWhatsAppReply"
/>
<label v-else :class="{ error: $v.message.$error }"> <label v-else :class="{ error: $v.message.$error }">
{{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }} {{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }}
<textarea <textarea
@ -104,7 +110,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div v-if="!hasWhatsappTemplates" class="modal-footer">
<button class="button clear" @click.prevent="onCancel"> <button class="button clear" @click.prevent="onCancel">
{{ $t('NEW_CONVERSATION.FORM.CANCEL') }} {{ $t('NEW_CONVERSATION.FORM.CANCEL') }}
</button> </button>
@ -121,7 +127,7 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor'; import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead'; import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead';
import CannedResponse from 'dashboard/components/widgets/conversation/CannedResponse.vue'; import CannedResponse from 'dashboard/components/widgets/conversation/CannedResponse.vue';
import WhatsappTemplates from './WhatsappTemplates.vue';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import { INBOX_TYPES } from 'shared/mixins/inboxMixin'; import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors'; import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
@ -133,6 +139,7 @@ export default {
WootMessageEditor, WootMessageEditor,
ReplyEmailHead, ReplyEmailHead,
CannedResponse, CannedResponse,
WhatsappTemplates,
}, },
mixins: [alertMixin], mixins: [alertMixin],
props: { props: {
@ -155,6 +162,7 @@ export default {
selectedInbox: '', selectedInbox: '',
bccEmails: '', bccEmails: '',
ccEmails: '', ccEmails: '',
whatsappTemplateSelected: false,
}; };
}, },
validations: { validations: {
@ -174,7 +182,7 @@ export default {
conversationsUiFlags: 'contactConversations/getUIFlags', conversationsUiFlags: 'contactConversations/getUIFlags',
currentUser: 'getCurrentUser', currentUser: 'getCurrentUser',
}), }),
getNewConversation() { emailMessagePayload() {
const payload = { const payload = {
inboxId: this.targetInbox.inbox.id, inboxId: this.targetInbox.inbox.id,
sourceId: this.targetInbox.source_id, sourceId: this.targetInbox.source_id,
@ -194,7 +202,7 @@ export default {
}, },
targetInbox: { targetInbox: {
get() { get() {
return this.selectedInbox || ''; return this.selectedInbox || {};
}, },
set(value) { set(value) {
this.selectedInbox = value; this.selectedInbox = value;
@ -221,6 +229,9 @@ export default {
this.selectedInbox.inbox.channel_type === INBOX_TYPES.WEB this.selectedInbox.inbox.channel_type === INBOX_TYPES.WEB
); );
}, },
hasWhatsappTemplates() {
return !!this.selectedInbox.inbox?.message_templates;
},
}, },
watch: { watch: {
message(value) { message(value) {
@ -243,13 +254,30 @@ export default {
onSuccess() { onSuccess() {
this.$emit('success'); this.$emit('success');
}, },
async handleSubmit() { replaceTextWithCannedResponse(message) {
setTimeout(() => {
this.message = message;
}, 50);
},
prepareWhatsAppMessagePayload({ message: content, templateParams }) {
const payload = {
inboxId: this.targetInbox.inbox.id,
sourceId: this.targetInbox.source_id,
contactId: this.contact.id,
message: { content, templateParams },
assigneeId: this.currentUser.id,
};
return payload;
},
onFormSubmit() {
this.$v.$touch(); this.$v.$touch();
if (this.$v.$invalid) { if (this.$v.$invalid) {
return; return;
} }
this.createConversation(this.emailMessagePayload);
},
async createConversation(payload) {
try { try {
const payload = this.getNewConversation;
const data = await this.onSubmit(payload); const data = await this.onSubmit(payload);
const action = { const action = {
type: 'link', type: 'link',
@ -269,10 +297,13 @@ export default {
} }
} }
}, },
replaceTextWithCannedResponse(message) {
setTimeout(() => { toggleWaTemplate(val) {
this.message = message; this.whatsappTemplateSelected = val;
}, 50); },
async onSendWhatsAppReply(messagePayload) {
const payload = this.prepareWhatsAppMessagePayload(messagePayload);
await this.createConversation(payload);
}, },
}, },
}; };
@ -321,4 +352,7 @@ export default {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} }
.row.gutter-small {
gap: var(--space-small);
}
</style> </style>

View file

@ -0,0 +1,59 @@
<template>
<div class="row">
<templates-picker
v-if="!selectedWaTemplate"
:inbox-id="inboxId"
@onSelect="pickTemplate"
/>
<template-parser
v-else
:template="selectedWaTemplate"
@resetTemplate="onResetTemplate"
@sendMessage="onSendMessage"
/>
</div>
</template>
<script>
import TemplatesPicker from 'dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue';
import TemplateParser from 'dashboard/components/widgets/conversation/WhatsappTemplates/TemplateParser.vue';
export default {
components: {
TemplatesPicker,
TemplateParser,
},
props: {
inboxId: {
type: Number,
default: undefined,
},
show: {
type: Boolean,
default: true,
},
},
data() {
return {
selectedWaTemplate: null,
};
},
methods: {
pickTemplate(template) {
this.$emit('pickTemplate', true);
this.selectedWaTemplate = template;
},
onResetTemplate() {
this.$emit('pickTemplate', false);
this.selectedWaTemplate = null;
},
onSendMessage(message) {
this.$emit('on-send', message);
},
onClose() {
this.$emit('cancel');
},
},
};
</script>
<style></style>

View file

@ -426,7 +426,7 @@ export default {
return this.$store.getters['inboxes/getInbox'](this.currentInboxId); return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
}, },
inboxName() { inboxName() {
if (this.isATwilioSMSChannel || this.isATwilioWhatsappChannel) { if (this.isATwilioSMSChannel || this.isAWhatsappChannel) {
return `${this.inbox.name} (${this.inbox.phone_number})`; return `${this.inbox.name} (${this.inbox.phone_number})`;
} }
if (this.isAnEmailChannel) { if (this.isAnEmailChannel) {

View file

@ -47,6 +47,20 @@ export const getters = {
getInboxes($state) { getInboxes($state) {
return $state.records; return $state.records;
}, },
getWhatsAppTemplates: $state => inboxId => {
const [inbox] = $state.records.filter(
record => record.id === Number(inboxId)
);
// filtering out the whatsapp templates with media
if (inbox.message_templates) {
return inbox.message_templates.filter(template => {
return !template.components.some(
i => i.format === 'IMAGE' || i.format === 'VIDEO'
);
});
}
return [];
},
getNewConversationInboxes($state) { getNewConversationInboxes($state) {
return $state.records.filter(inbox => { return $state.records.filter(inbox => {
const { const {

View file

@ -121,6 +121,7 @@
"video-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-8.5A3.25 3.25 0 0 1 2 16.25v-8.5A3.25 3.25 0 0 1 5.25 4.5h8.5Zm0 1.5h-8.5A1.75 1.75 0 0 0 3.5 7.75v8.5c0 .966.784 1.75 1.75 1.75h8.5a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6Zm6.75 1.573L17 9.674v4.651l3.5 2.1V7.573Z", "video-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-8.5A3.25 3.25 0 0 1 2 16.25v-8.5A3.25 3.25 0 0 1 5.25 4.5h8.5Zm0 1.5h-8.5A1.75 1.75 0 0 0 3.5 7.75v8.5c0 .966.784 1.75 1.75 1.75h8.5a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6Zm6.75 1.573L17 9.674v4.651l3.5 2.1V7.573Z",
"warning-outline": "M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z", "warning-outline": "M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z",
"wifi-off-outline": "m12.858 14.273 7.434 7.434a1 1 0 0 0 1.414-1.414l-17.999-18a1 1 0 1 0-1.414 1.414L5.39 6.804c-.643.429-1.254.927-1.821 1.495a12.382 12.382 0 0 0-1.39 1.683 1 1 0 0 0 1.644 1.14c.363-.524.761-1.01 1.16-1.41a9.94 9.94 0 0 1 1.855-1.46L7.99 9.405a8.14 8.14 0 0 0-3.203 3.377 1 1 0 0 0 1.784.903 6.08 6.08 0 0 1 1.133-1.563 6.116 6.116 0 0 1 1.77-1.234l1.407 1.407A5.208 5.208 0 0 0 8.336 13.7a5.25 5.25 0 0 0-1.09 1.612 1 1 0 0 0 1.832.802c.167-.381.394-.722.672-1a3.23 3.23 0 0 1 3.108-.841Zm-1.332-5.93 2.228 2.229a6.1 6.1 0 0 1 2.616 1.55c.444.444.837.995 1.137 1.582a1 1 0 1 0 1.78-.911 8.353 8.353 0 0 0-1.503-2.085 8.108 8.108 0 0 0-6.258-2.365ZM8.51 5.327l1.651 1.651a9.904 9.904 0 0 1 10.016 4.148 1 1 0 1 0 1.646-1.136A11.912 11.912 0 0 0 8.51 5.327Zm4.552 11.114a1.501 1.501 0 1 1-2.123 2.123 1.501 1.501 0 0 1 2.123-2.123Z", "wifi-off-outline": "m12.858 14.273 7.434 7.434a1 1 0 0 0 1.414-1.414l-17.999-18a1 1 0 1 0-1.414 1.414L5.39 6.804c-.643.429-1.254.927-1.821 1.495a12.382 12.382 0 0 0-1.39 1.683 1 1 0 0 0 1.644 1.14c.363-.524.761-1.01 1.16-1.41a9.94 9.94 0 0 1 1.855-1.46L7.99 9.405a8.14 8.14 0 0 0-3.203 3.377 1 1 0 0 0 1.784.903 6.08 6.08 0 0 1 1.133-1.563 6.116 6.116 0 0 1 1.77-1.234l1.407 1.407A5.208 5.208 0 0 0 8.336 13.7a5.25 5.25 0 0 0-1.09 1.612 1 1 0 0 0 1.832.802c.167-.381.394-.722.672-1a3.23 3.23 0 0 1 3.108-.841Zm-1.332-5.93 2.228 2.229a6.1 6.1 0 0 1 2.616 1.55c.444.444.837.995 1.137 1.582a1 1 0 1 0 1.78-.911 8.353 8.353 0 0 0-1.503-2.085 8.108 8.108 0 0 0-6.258-2.365ZM8.51 5.327l1.651 1.651a9.904 9.904 0 0 1 10.016 4.148 1 1 0 1 0 1.646-1.136A11.912 11.912 0 0 0 8.51 5.327Zm4.552 11.114a1.501 1.501 0 1 1-2.123 2.123 1.501 1.501 0 0 1 2.123-2.123Z",
"whatsapp-outline": "M19.05 4.91A9.816 9.816 0 0 0 12.04 2c-5.46 0-9.91 4.45-9.91 9.91c0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21c5.46 0 9.91-4.45 9.91-9.91c0-2.65-1.03-5.14-2.9-7.01zm-7.01 15.24c-1.48 0-2.93-.4-4.2-1.15l-.3-.18l-3.12.82l.83-3.04l-.2-.31a8.264 8.264 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.24-8.24c2.2 0 4.27.86 5.82 2.42a8.183 8.183 0 0 1 2.41 5.83c.02 4.54-3.68 8.23-8.22 8.23zm4.52-6.16c-.25-.12-1.47-.72-1.69-.81c-.23-.08-.39-.12-.56.12c-.17.25-.64.81-.78.97c-.14.17-.29.19-.54.06c-.25-.12-1.05-.39-1.99-1.23c-.74-.66-1.23-1.47-1.38-1.72c-.14-.25-.02-.38.11-.51c.11-.11.25-.29.37-.43s.17-.25.25-.41c.08-.17.04-.31-.02-.43s-.56-1.34-.76-1.84c-.2-.48-.41-.42-.56-.43h-.48c-.17 0-.43.06-.66.31c-.22.25-.86.85-.86 2.07c0 1.22.89 2.4 1.01 2.56c.12.17 1.75 2.67 4.23 3.74c.59.26 1.05.41 1.41.52c.59.19 1.13.16 1.56.1c.48-.07 1.47-.6 1.67-1.18c.21-.58.21-1.07.14-1.18s-.22-.16-.47-.28z",
"brand-facebook-outline": "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z", "brand-facebook-outline": "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z",
"brand-line-outline": "M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314", "brand-line-outline": "M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314",
"brand-linkedin-outline": "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z", "brand-linkedin-outline": "M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z",

View file

@ -61,9 +61,13 @@ class Channel::Whatsapp < ApplicationRecord
true true
end end
def message_templates def sync_templates
sync_templates # to prevent too many api calls
super last_updated = message_templates_last_updated || 1.day.ago
return if Time.current < (last_updated + 12.hours)
response = HTTParty.get("#{api_base_path}/configs/templates", headers: api_headers)
update(message_templates: response['waba_templates'], message_templates_last_updated: Time.now.utc) if response.success?
end end
private private
@ -79,7 +83,7 @@ class Channel::Whatsapp < ApplicationRecord
}.to_json }.to_json
) )
response.success? ? response['messages'].first['id'] : nil process_response(response)
end end
def send_attachment_message(phone_number, message) def send_attachment_message(phone_number, message)
@ -99,7 +103,7 @@ class Channel::Whatsapp < ApplicationRecord
}.to_json }.to_json
) )
response.success? ? response['messages'].first['id'] : nil process_response(response)
end end
def send_template_message(phone_number, template_info) def send_template_message(phone_number, template_info)
@ -113,7 +117,16 @@ class Channel::Whatsapp < ApplicationRecord
}.to_json }.to_json
) )
response.success? ? response['messages'].first['id'] : nil process_response(response)
end
def process_response(response)
if response.success?
response['messages'].first['id']
else
Rails.logger.error response.body
nil
end
end end
def template_body_parameters(template_info) def template_body_parameters(template_info)
@ -131,15 +144,6 @@ class Channel::Whatsapp < ApplicationRecord
} }
end end
def sync_templates
# to prevent too many api calls
last_updated = message_templates_last_updated || 1.day.ago
return if Time.current < (last_updated + 12.hours)
response = HTTParty.get("#{api_base_path}/configs/templates", headers: api_headers)
update(message_templates: response['waba_templates'], message_templates_last_updated: Time.now.utc) if response.success?
end
# Extract later into provider Service # Extract later into provider Service
def validate_provider_config def validate_provider_config
response = HTTParty.post( response = HTTParty.post(

View file

@ -92,6 +92,10 @@ class Inbox < ApplicationRecord
channel_type == 'Channel::TwitterProfile' channel_type == 'Channel::TwitterProfile'
end end
def whatsapp?
channel_type == 'Channel::Whatsapp'
end
def inbox_type def inbox_type
channel.name channel.name
end end

View file

@ -6,16 +6,18 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
end end
def perform_reply def perform_reply
# can reply checks if 24 hour limit has passed. should_send_template_message = template_params.present? || !message.conversation.can_reply?
if message.conversation.can_reply? if should_send_template_message
send_on_whatsapp
else
send_template_message send_template_message
else
send_session_message
end end
end end
def send_template_message def send_template_message
channel.sync_templates
name, namespace, lang_code, processed_parameters = processable_channel_message_template name, namespace, lang_code, processed_parameters = processable_channel_message_template
return if name.blank? return if name.blank?
message_id = channel.send_template(message.conversation.contact_inbox.source_id, { message_id = channel.send_template(message.conversation.contact_inbox.source_id, {
@ -28,6 +30,16 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
end end
def processable_channel_message_template def processable_channel_message_template
if template_params.present?
return [
template_params['name'],
template_params['namespace'],
template_params['language'],
template_params['processed_params'].map { |_, value| { type: 'text', text: value } }
]
end
# Delete the following logic once the update for template_params is stable
# see if we can match the message content to a template # see if we can match the message content to a template
# An example template may look like "Your package has been shipped. It will be delivered in {{1}} business days. # An example template may look like "Your package has been shipped. It will be delivered in {{1}} business days.
# We want to iterate over these templates with our message body and see if we can fit it to any of the templates # We want to iterate over these templates with our message body and see if we can fit it to any of the templates
@ -78,8 +90,12 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
template['components'].find { |obj| obj['type'] == 'BODY' && obj.key?('text') } template['components'].find { |obj| obj['type'] == 'BODY' && obj.key?('text') }
end end
def send_on_whatsapp def send_session_message
message_id = channel.send_message(message.conversation.contact_inbox.source_id, message) message_id = channel.send_message(message.conversation.contact_inbox.source_id, message)
message.update!(source_id: message_id) if message_id.present? message.update!(source_id: message_id) if message_id.present?
end end
def template_params
message.additional_attributes && message.additional_attributes['template_params']
end
end end

View file

@ -78,3 +78,6 @@ if resource.api?
json.webhook_url resource.channel.try(:webhook_url) json.webhook_url resource.channel.try(:webhook_url)
json.inbox_identifier resource.channel.try(:identifier) json.inbox_identifier resource.channel.try(:identifier)
end end
### WhatsApp Channel
json.message_templates resource.channel.try(:message_templates) if resource.whatsapp?

View file

@ -1,6 +1,13 @@
require 'rails_helper' require 'rails_helper'
describe Whatsapp::SendOnWhatsappService do describe Whatsapp::SendOnWhatsappService do
template_params = {
name: 'sample_shipping_confirmation',
namespace: '23423423_2342423_324234234_2343224',
language: 'en_US',
processed_params: { '1' => '3' }
}
describe '#perform' do describe '#perform' do
before do before do
stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook') stub_request(:post, 'https://waba.360dialog.io/v1/configs/webhook')
@ -54,6 +61,30 @@ describe Whatsapp::SendOnWhatsappService do
expect(message.reload.source_id).to eq('123456789') expect(message.reload.source_id).to eq('123456789')
end end
it 'calls channel.send_template if template_params are present' do
message = create(:message, additional_attributes: { template_params: template_params },
content: 'Your package will be delivered in 3 business days.', conversation: conversation, message_type: :outgoing)
allow(HTTParty).to receive(:post).and_return(whatsapp_request)
allow(whatsapp_request).to receive(:success?).and_return(true)
allow(whatsapp_request).to receive(:[]).with('messages').and_return([{ 'id' => '123456789' }])
expect(HTTParty).to receive(:post).with(
'https://waba.360dialog.io/v1/messages',
headers: { 'D360-API-KEY' => 'test_key', 'Content-Type' => 'application/json' },
body: {
to: '123456789',
template: {
name: 'sample_shipping_confirmation',
namespace: '23423423_2342423_324234234_2343224',
language: { 'policy': 'deterministic', 'code': 'en_US' },
components: [{ 'type': 'body', 'parameters': [{ 'type': 'text', 'text': '3' }] }]
},
type: 'template'
}.to_json
)
described_class.new(message: message).perform
expect(message.reload.source_id).to eq('123456789')
end
it 'calls channel.send_template when template has regexp characters' do it 'calls channel.send_template when template has regexp characters' do
message = create( message = create(
:message, :message,