mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	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:
		| @@ -73,6 +73,10 @@ class Messages::MessageBuilder | ||||
|     @params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {} | ||||
|   end | ||||
|  | ||||
|   def template_params | ||||
|     @params[:template_params].present? ? { additional_attributes: { template_params: JSON.parse(@params[:template_params].to_json) } } : {} | ||||
|   end | ||||
|  | ||||
|   def message_sender | ||||
|     return if @params[:sender_type] != 'AgentBot' | ||||
|  | ||||
| @@ -91,6 +95,6 @@ class Messages::MessageBuilder | ||||
|       items: @items, | ||||
|       in_reply_to: @in_reply_to, | ||||
|       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 | ||||
|   | ||||
| @@ -10,6 +10,7 @@ export const buildCreatePayload = ({ | ||||
|   files, | ||||
|   ccEmails = '', | ||||
|   bccEmails = '', | ||||
|   templateParams, | ||||
| }) => { | ||||
|   let payload; | ||||
|   if (files && files.length !== 0) { | ||||
| @@ -32,6 +33,7 @@ export const buildCreatePayload = ({ | ||||
|       content_attributes: contentAttributes, | ||||
|       cc_emails: ccEmails, | ||||
|       bcc_emails: bccEmails, | ||||
|       template_params: templateParams, | ||||
|     }; | ||||
|   } | ||||
|   return payload; | ||||
| @@ -51,6 +53,7 @@ class MessageApi extends ApiClient { | ||||
|     files, | ||||
|     ccEmails = '', | ||||
|     bccEmails = '', | ||||
|     templateParams, | ||||
|   }) { | ||||
|     return axios({ | ||||
|       method: 'post', | ||||
| @@ -63,6 +66,7 @@ class MessageApi extends ApiClient { | ||||
|         files, | ||||
|         ccEmails, | ||||
|         bccEmails, | ||||
|         templateParams, | ||||
|       }), | ||||
|     }); | ||||
|   } | ||||
|   | ||||
| @@ -98,4 +98,7 @@ export default { | ||||
|     width: 48rem; | ||||
|   } | ||||
| } | ||||
| .modal-big { | ||||
|   width: 60%; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -79,6 +79,16 @@ | ||||
|         :title="signatureToggleTooltip" | ||||
|         @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"> | ||||
|         <div | ||||
|           v-show="$refs.upload && $refs.upload.dropActive" | ||||
| @@ -218,6 +228,10 @@ export default { | ||||
|       type: Boolean, | ||||
|       default: true, | ||||
|     }, | ||||
|     hasWhatsappTemplates: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     isNote() { | ||||
| @@ -261,6 +275,9 @@ export default { | ||||
|     showMessageSignatureButton() { | ||||
|       return !this.isPrivate && this.isAnEmailChannel; | ||||
|     }, | ||||
|     showWhatsappTemplatesButton() { | ||||
|       return !this.isOnPrivateNote && this.hasWhatsappTemplates; | ||||
|     }, | ||||
|     sendWithSignature() { | ||||
|       const { send_with_signature: isEnabled } = this.uiSettings; | ||||
|       return isEnabled; | ||||
|   | ||||
| @@ -101,7 +101,7 @@ | ||||
|       :toggle-audio-recorder="toggleAudioRecorder" | ||||
|       :toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause" | ||||
|       :show-emoji-picker="showEmojiPicker" | ||||
|       :on-send="sendMessage" | ||||
|       :on-send="onSendReply" | ||||
|       :is-send-disabled="isReplyButtonDisabled" | ||||
|       :recording-audio-duration-text="recordingAudioDurationText" | ||||
|       :recording-audio-state="recordingAudioState" | ||||
| @@ -112,7 +112,16 @@ | ||||
|       :enable-rich-editor="isRichEditorEnabled" | ||||
|       :enter-to-send-enabled="enterToSendEnabled" | ||||
|       :enable-multiple-file-upload="enableMultipleFileUpload" | ||||
|       :has-whatsapp-templates="hasWhatsappTemplates" | ||||
|       @toggleEnterToSend="toggleEnterToSend" | ||||
|       @selectWhatsappTemplate="openWhatsappTemplateModal" | ||||
|     /> | ||||
|     <whatsapp-templates | ||||
|       :inbox-id="inbox.id" | ||||
|       :show="showWhatsAppTemplatesModal" | ||||
|       @close="hideWhatsappTemplatesModal" | ||||
|       @on-send="onSendWhatsAppReply" | ||||
|       @cancel="hideWhatsappTemplatesModal" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -137,7 +146,7 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; | ||||
| import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; | ||||
| import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages'; | ||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||
|  | ||||
| import WhatsappTemplates from './WhatsappTemplates/Modal.vue'; | ||||
| import { | ||||
|   isEscape, | ||||
|   isEnter, | ||||
| @@ -162,6 +171,7 @@ export default { | ||||
|     WootMessageEditor, | ||||
|     WootAudioRecorder, | ||||
|     Banner, | ||||
|     WhatsappTemplates, | ||||
|   }, | ||||
|   mixins: [ | ||||
|     clickaway, | ||||
| @@ -201,6 +211,7 @@ export default { | ||||
|       hasSlashCommand: false, | ||||
|       bccEmails: '', | ||||
|       ccEmails: '', | ||||
|       showWhatsAppTemplatesModal: false, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
| @@ -212,7 +223,6 @@ export default { | ||||
|       globalConfig: 'globalConfig/get', | ||||
|       accountId: 'getCurrentAccountId', | ||||
|     }), | ||||
|  | ||||
|     showRichContentEditor() { | ||||
|       if (this.isOnPrivateNote) { | ||||
|         return true; | ||||
| @@ -256,7 +266,9 @@ export default { | ||||
|  | ||||
|       return false; | ||||
|     }, | ||||
|  | ||||
|     hasWhatsappTemplates() { | ||||
|       return !!this.inbox.message_templates; | ||||
|     }, | ||||
|     enterToSendEnabled() { | ||||
|       return !!this.uiSettings.enter_to_send_enabled; | ||||
|     }, | ||||
| @@ -484,7 +496,7 @@ export default { | ||||
|           hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused; | ||||
|         if (shouldSendMessage) { | ||||
|           e.preventDefault(); | ||||
|           this.sendMessage(); | ||||
|           this.onSendReply(); | ||||
|         } | ||||
|       } else if (hasPressedCommandPlusKKey(e)) { | ||||
|         this.openCommandBar(); | ||||
| @@ -497,6 +509,12 @@ export default { | ||||
|     toggleEnterToSend(enterToSendEnabled) { | ||||
|       this.updateUISettings({ enter_to_send_enabled: enterToSendEnabled }); | ||||
|     }, | ||||
|     openWhatsappTemplateModal() { | ||||
|       this.showWhatsAppTemplatesModal = true; | ||||
|     }, | ||||
|     hideWhatsappTemplatesModal() { | ||||
|       this.showWhatsAppTemplatesModal = false; | ||||
|     }, | ||||
|     onClickSelfAssign() { | ||||
|       const { | ||||
|         account_id, | ||||
| @@ -520,7 +538,7 @@ export default { | ||||
|       }; | ||||
|       this.assignedAgent = selfAssign; | ||||
|     }, | ||||
|     async sendMessage() { | ||||
|     async onSendReply() { | ||||
|       if (this.isReplyButtonDisabled) { | ||||
|         return; | ||||
|       } | ||||
| @@ -531,22 +549,31 @@ export default { | ||||
|         } | ||||
|         const messagePayload = this.getMessagePayload(newMessage); | ||||
|         this.clearMessage(); | ||||
|         try { | ||||
|           await this.$store.dispatch( | ||||
|             'createPendingMessageAndSend', | ||||
|             messagePayload | ||||
|           ); | ||||
|           bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE); | ||||
|         } catch (error) { | ||||
|           const errorMessage = | ||||
|             error?.response?.data?.error || | ||||
|             this.$t('CONVERSATION.MESSAGE_ERROR'); | ||||
|           this.showAlert(errorMessage); | ||||
|         } | ||||
|         this.sendMessage(messagePayload); | ||||
|         this.hideEmojiPicker(); | ||||
|         this.$emit('update:popoutReplyBox', false); | ||||
|       } | ||||
|     }, | ||||
|     async sendMessage(messagePayload) { | ||||
|       try { | ||||
|         await this.$store.dispatch( | ||||
|           'createPendingMessageAndSend', | ||||
|           messagePayload | ||||
|         ); | ||||
|         bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE); | ||||
|       } catch (error) { | ||||
|         const errorMessage = | ||||
|           error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR'); | ||||
|         this.showAlert(errorMessage); | ||||
|       } | ||||
|     }, | ||||
|     async onSendWhatsAppReply(messagePayload) { | ||||
|       this.sendMessage({ | ||||
|         conversationId: this.currentChat.id, | ||||
|         ...messagePayload, | ||||
|       }); | ||||
|       this.hideWhatsappTemplatesModal(); | ||||
|     }, | ||||
|     replaceText(message) { | ||||
|       setTimeout(() => { | ||||
|         this.message = message; | ||||
|   | ||||
| @@ -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> | ||||
| @@ -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> | ||||
| @@ -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> | ||||
| @@ -7,7 +7,7 @@ | ||||
|       <fluent-icon | ||||
|         v-tooltip.top-start="$t('CHAT_LIST.SENT')" | ||||
|         icon="checkmark" | ||||
|         size="16" | ||||
|         size="14" | ||||
|       /> | ||||
|     </span> | ||||
|     <fluent-icon | ||||
| @@ -165,7 +165,11 @@ export default { | ||||
|       return `https://www.instagram.com/stories/${storySender}/${storyId}`; | ||||
|     }, | ||||
|     showSentIndicator() { | ||||
|       return this.isOutgoing && this.sourceId && this.isAnEmailChannel; | ||||
|       return ( | ||||
|         this.isOutgoing && | ||||
|         this.sourceId && | ||||
|         (this.isAnEmailChannel || this.isAWhatsappChannel) | ||||
|       ); | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
|       :type="type" | ||||
|       :placeholder="placeholder" | ||||
|       :readonly="readonly" | ||||
|       :style="styles" | ||||
|       @input="onChange" | ||||
|       @blur="onBlur" | ||||
|     /> | ||||
| @@ -47,6 +48,10 @@ export default { | ||||
|       type: Boolean, | ||||
|       deafaut: false, | ||||
|     }, | ||||
|     styles: { | ||||
|       type: Object, | ||||
|       default: () => {}, | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     onChange(e) { | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import { default as _setNewPassword } from './setNewPassword.json'; | ||||
| import { default as _settings } from './settings.json'; | ||||
| import { default as _signup } from './signup.json'; | ||||
| import { default as _teamsSettings } from './teamsSettings.json'; | ||||
| import { default as _whatsappTemplates } from './whatsappTemplates.json'; | ||||
| import { default as _bulkActions } from './bulkActions.json'; | ||||
|  | ||||
| export default { | ||||
| @@ -47,5 +48,6 @@ export default { | ||||
|   ..._settings, | ||||
|   ..._signup, | ||||
|   ..._teamsSettings, | ||||
|   ..._whatsappTemplates, | ||||
|   ..._bulkActions, | ||||
| }; | ||||
|   | ||||
| @@ -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" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,12 +1,12 @@ | ||||
| <template> | ||||
|   <form class="conversation--form" @submit.prevent="handleSubmit"> | ||||
|   <form class="conversation--form" @submit.prevent="onFormSubmit"> | ||||
|     <div v-if="showNoInboxAlert" class="callout warning"> | ||||
|       <p> | ||||
|         {{ $t('NEW_CONVERSATION.NO_INBOX') }} | ||||
|       </p> | ||||
|     </div> | ||||
|     <div v-else> | ||||
|       <div class="row"> | ||||
|       <div class="row gutter-small"> | ||||
|         <div class="columns"> | ||||
|           <label :class="{ error: $v.targetInbox.$error }"> | ||||
|             {{ $t('NEW_CONVERSATION.FORM.INBOX.LABEL') }} | ||||
| @@ -88,6 +88,12 @@ | ||||
|               </label> | ||||
|             </label> | ||||
|           </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 }"> | ||||
|             {{ $t('NEW_CONVERSATION.FORM.MESSAGE.LABEL') }} | ||||
|             <textarea | ||||
| @@ -104,7 +110,7 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|     <div v-if="!hasWhatsappTemplates" class="modal-footer"> | ||||
|       <button class="button clear" @click.prevent="onCancel"> | ||||
|         {{ $t('NEW_CONVERSATION.FORM.CANCEL') }} | ||||
|       </button> | ||||
| @@ -121,7 +127,7 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail'; | ||||
| import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor'; | ||||
| import ReplyEmailHead from 'dashboard/components/widgets/conversation/ReplyEmailHead'; | ||||
| import CannedResponse from 'dashboard/components/widgets/conversation/CannedResponse.vue'; | ||||
|  | ||||
| import WhatsappTemplates from './WhatsappTemplates.vue'; | ||||
| import alertMixin from 'shared/mixins/alertMixin'; | ||||
| import { INBOX_TYPES } from 'shared/mixins/inboxMixin'; | ||||
| import { ExceptionWithMessage } from 'shared/helpers/CustomErrors'; | ||||
| @@ -133,6 +139,7 @@ export default { | ||||
|     WootMessageEditor, | ||||
|     ReplyEmailHead, | ||||
|     CannedResponse, | ||||
|     WhatsappTemplates, | ||||
|   }, | ||||
|   mixins: [alertMixin], | ||||
|   props: { | ||||
| @@ -155,6 +162,7 @@ export default { | ||||
|       selectedInbox: '', | ||||
|       bccEmails: '', | ||||
|       ccEmails: '', | ||||
|       whatsappTemplateSelected: false, | ||||
|     }; | ||||
|   }, | ||||
|   validations: { | ||||
| @@ -174,7 +182,7 @@ export default { | ||||
|       conversationsUiFlags: 'contactConversations/getUIFlags', | ||||
|       currentUser: 'getCurrentUser', | ||||
|     }), | ||||
|     getNewConversation() { | ||||
|     emailMessagePayload() { | ||||
|       const payload = { | ||||
|         inboxId: this.targetInbox.inbox.id, | ||||
|         sourceId: this.targetInbox.source_id, | ||||
| @@ -194,7 +202,7 @@ export default { | ||||
|     }, | ||||
|     targetInbox: { | ||||
|       get() { | ||||
|         return this.selectedInbox || ''; | ||||
|         return this.selectedInbox || {}; | ||||
|       }, | ||||
|       set(value) { | ||||
|         this.selectedInbox = value; | ||||
| @@ -221,6 +229,9 @@ export default { | ||||
|         this.selectedInbox.inbox.channel_type === INBOX_TYPES.WEB | ||||
|       ); | ||||
|     }, | ||||
|     hasWhatsappTemplates() { | ||||
|       return !!this.selectedInbox.inbox?.message_templates; | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     message(value) { | ||||
| @@ -243,13 +254,30 @@ export default { | ||||
|     onSuccess() { | ||||
|       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(); | ||||
|       if (this.$v.$invalid) { | ||||
|         return; | ||||
|       } | ||||
|       this.createConversation(this.emailMessagePayload); | ||||
|     }, | ||||
|     async createConversation(payload) { | ||||
|       try { | ||||
|         const payload = this.getNewConversation; | ||||
|         const data = await this.onSubmit(payload); | ||||
|         const action = { | ||||
|           type: 'link', | ||||
| @@ -269,10 +297,13 @@ export default { | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     replaceTextWithCannedResponse(message) { | ||||
|       setTimeout(() => { | ||||
|         this.message = message; | ||||
|       }, 50); | ||||
|  | ||||
|     toggleWaTemplate(val) { | ||||
|       this.whatsappTemplateSelected = val; | ||||
|     }, | ||||
|     async onSendWhatsAppReply(messagePayload) { | ||||
|       const payload = this.prepareWhatsAppMessagePayload(messagePayload); | ||||
|       await this.createConversation(payload); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| @@ -321,4 +352,7 @@ export default { | ||||
|   display: flex; | ||||
|   justify-content: flex-end; | ||||
| } | ||||
| .row.gutter-small { | ||||
|   gap: var(--space-small); | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -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> | ||||
| @@ -426,7 +426,7 @@ export default { | ||||
|       return this.$store.getters['inboxes/getInbox'](this.currentInboxId); | ||||
|     }, | ||||
|     inboxName() { | ||||
|       if (this.isATwilioSMSChannel || this.isATwilioWhatsappChannel) { | ||||
|       if (this.isATwilioSMSChannel || this.isAWhatsappChannel) { | ||||
|         return `${this.inbox.name} (${this.inbox.phone_number})`; | ||||
|       } | ||||
|       if (this.isAnEmailChannel) { | ||||
|   | ||||
| @@ -47,6 +47,20 @@ export const getters = { | ||||
|   getInboxes($state) { | ||||
|     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) { | ||||
|     return $state.records.filter(inbox => { | ||||
|       const { | ||||
|   | ||||
| @@ -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", | ||||
|   "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", | ||||
|   "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-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", | ||||
|   | ||||
| @@ -61,9 +61,13 @@ class Channel::Whatsapp < ApplicationRecord | ||||
|     true | ||||
|   end | ||||
|  | ||||
|   def message_templates | ||||
|     sync_templates | ||||
|     super | ||||
|   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 | ||||
|  | ||||
|   private | ||||
| @@ -79,7 +83,7 @@ class Channel::Whatsapp < ApplicationRecord | ||||
|       }.to_json | ||||
|     ) | ||||
|  | ||||
|     response.success? ? response['messages'].first['id'] : nil | ||||
|     process_response(response) | ||||
|   end | ||||
|  | ||||
|   def send_attachment_message(phone_number, message) | ||||
| @@ -99,7 +103,7 @@ class Channel::Whatsapp < ApplicationRecord | ||||
|       }.to_json | ||||
|     ) | ||||
|  | ||||
|     response.success? ? response['messages'].first['id'] : nil | ||||
|     process_response(response) | ||||
|   end | ||||
|  | ||||
|   def send_template_message(phone_number, template_info) | ||||
| @@ -113,7 +117,16 @@ class Channel::Whatsapp < ApplicationRecord | ||||
|       }.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 | ||||
|  | ||||
|   def template_body_parameters(template_info) | ||||
| @@ -131,15 +144,6 @@ class Channel::Whatsapp < ApplicationRecord | ||||
|     } | ||||
|   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 | ||||
|   def validate_provider_config | ||||
|     response = HTTParty.post( | ||||
|   | ||||
| @@ -92,6 +92,10 @@ class Inbox < ApplicationRecord | ||||
|     channel_type == 'Channel::TwitterProfile' | ||||
|   end | ||||
|  | ||||
|   def whatsapp? | ||||
|     channel_type == 'Channel::Whatsapp' | ||||
|   end | ||||
|  | ||||
|   def inbox_type | ||||
|     channel.name | ||||
|   end | ||||
|   | ||||
| @@ -6,16 +6,18 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService | ||||
|   end | ||||
|  | ||||
|   def perform_reply | ||||
|     # can reply checks if 24 hour limit has passed. | ||||
|     if message.conversation.can_reply? | ||||
|       send_on_whatsapp | ||||
|     else | ||||
|     should_send_template_message = template_params.present? || !message.conversation.can_reply? | ||||
|     if should_send_template_message | ||||
|       send_template_message | ||||
|     else | ||||
|       send_session_message | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def send_template_message | ||||
|     channel.sync_templates | ||||
|     name, namespace, lang_code, processed_parameters = processable_channel_message_template | ||||
|  | ||||
|     return if name.blank? | ||||
|  | ||||
|     message_id = channel.send_template(message.conversation.contact_inbox.source_id, { | ||||
| @@ -28,6 +30,16 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService | ||||
|   end | ||||
|  | ||||
|   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 | ||||
|     # 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 | ||||
| @@ -78,8 +90,12 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService | ||||
|     template['components'].find { |obj| obj['type'] == 'BODY' && obj.key?('text') } | ||||
|   end | ||||
|  | ||||
|   def send_on_whatsapp | ||||
|   def send_session_message | ||||
|     message_id = channel.send_message(message.conversation.contact_inbox.source_id, message) | ||||
|     message.update!(source_id: message_id) if message_id.present? | ||||
|   end | ||||
|  | ||||
|   def template_params | ||||
|     message.additional_attributes && message.additional_attributes['template_params'] | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -78,3 +78,6 @@ if resource.api? | ||||
|   json.webhook_url resource.channel.try(:webhook_url) | ||||
|   json.inbox_identifier resource.channel.try(:identifier) | ||||
| end | ||||
|  | ||||
| ### WhatsApp Channel | ||||
| json.message_templates resource.channel.try(:message_templates) if resource.whatsapp? | ||||
|   | ||||
| @@ -1,6 +1,13 @@ | ||||
| require 'rails_helper' | ||||
|  | ||||
| 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 | ||||
|     before do | ||||
|       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') | ||||
|       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 | ||||
|         message = create( | ||||
|           :message, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Fayaz Ahmed
					Fayaz Ahmed