From 4fc80ba4ee13f9e78a5105b8b998a1b3c68c6155 Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Wed, 15 Oct 2025 16:23:04 -0300 Subject: [PATCH] feat(zapi): Z-API integration (#115) * feat(zapi): connect flow and UI updates * fix(zapi): re-add manage connection section * feat(zapi): reply * feat: send message * fix: qrcode job logic * test: qr code job specs * chore: concurrent index * chore: whatsapp model minor * test: message window service specs * chore: service refactor * test: zapi service * chore: zapi beta * chore: minor fixes * test: incoming message specs * chore: minor fixes * feat: handle status transitions * test: refactor spec * test: refactor spec * chore(z-api): use feature flag * chore: fix migration name --- .../components/ActionButtons.vue | 4 +- .../components/ComposeNewConversationForm.vue | 8 +- app/javascript/dashboard/components/Code.vue | 32 +- .../widgets/conversation/MessagesView.vue | 36 +- .../dashboard/composables/useInbox.js | 8 + app/javascript/dashboard/featureFlags.js | 1 + .../i18n/locale/en/conversation.json | 2 +- .../dashboard/i18n/locale/en/inboxMgmt.json | 37 +- .../i18n/locale/pt_BR/conversation.json | 2 +- .../i18n/locale/pt_BR/inboxMgmt.json | 37 +- .../dashboard/settings/inbox/FinishSetup.vue | 36 +- .../routes/dashboard/settings/inbox/Index.vue | 1 + .../dashboard/settings/inbox/Settings.vue | 6 +- .../settings/inbox/channels/Whatsapp.vue | 59 +- .../settings/inbox/channels/ZapiWhatsapp.vue | 162 ++++ .../settings/inbox/components/ChannelName.vue | 17 + ...eModal.vue => WhatsappLinkDeviceModal.vue} | 18 +- .../inbox/settingsPage/ConfigurationPage.vue | 215 ++++- app/javascript/shared/mixins/inboxMixin.js | 6 + .../shared/mixins/specs/inboxMixin.spec.js | 8 + .../channels/whatsapp/zapi_qr_code_job.rb | 32 + app/jobs/webhooks/whatsapp_events_job.rb | 2 + app/models/channel/whatsapp.rb | 10 +- app/models/message.rb | 3 +- .../conversations/message_window_service.rb | 2 +- .../whatsapp/incoming_message_zapi_service.rb | 19 + .../providers/whatsapp_zapi_service.rb | 273 ++++++ .../zapi_handlers/connected_callback.rb | 20 + .../zapi_handlers/delivery_callback.rb | 17 + .../zapi_handlers/disconnected_callback.rb | 9 + .../whatsapp/zapi_handlers/helpers.rb | 28 + .../zapi_handlers/message_status_callback.rb | 40 + .../zapi_handlers/received_callback.rb | 208 +++++ config/features.yml | 3 + ...tsapp_channel_provider_connection_index.rb | 23 + db/schema.rb | 4 +- .../whatsapp/zapi_qr_code_job_spec.rb | 98 +++ spec/models/channel/whatsapp_spec.rb | 4 +- .../message_window_service_spec.rb | 18 + .../incoming_message_zapi_service_spec.rb | 794 ++++++++++++++++++ .../providers/whatsapp_zapi_service_spec.rb | 645 ++++++++++++++ theme/icons.js | 12 +- 42 files changed, 2845 insertions(+), 114 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ZapiWhatsapp.vue rename app/javascript/dashboard/routes/dashboard/settings/inbox/components/{WhatsappBaileysLinkDeviceModal.vue => WhatsappLinkDeviceModal.vue} (85%) create mode 100644 app/jobs/channels/whatsapp/zapi_qr_code_job.rb create mode 100644 app/services/whatsapp/incoming_message_zapi_service.rb create mode 100644 app/services/whatsapp/providers/whatsapp_zapi_service.rb create mode 100644 app/services/whatsapp/zapi_handlers/connected_callback.rb create mode 100644 app/services/whatsapp/zapi_handlers/delivery_callback.rb create mode 100644 app/services/whatsapp/zapi_handlers/disconnected_callback.rb create mode 100644 app/services/whatsapp/zapi_handlers/helpers.rb create mode 100644 app/services/whatsapp/zapi_handlers/message_status_callback.rb create mode 100644 app/services/whatsapp/zapi_handlers/received_callback.rb create mode 100644 db/migrate/20250928173414_recreate_whatsapp_channel_provider_connection_index.rb create mode 100644 spec/jobs/channels/whatsapp/zapi_qr_code_job_spec.rb create mode 100644 spec/services/whatsapp/incoming_message_zapi_service_spec.rb create mode 100644 spec/services/whatsapp/providers/whatsapp_zapi_service_spec.rb diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue index 0b8913742..396fe1ada 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ActionButtons.vue @@ -17,6 +17,7 @@ const props = defineProps({ attachedFiles: { type: Array, default: () => [] }, isWhatsappInbox: { type: Boolean, default: false }, isWhatsappBaileysInbox: { type: Boolean, default: false }, + isWhatsAppZapiInbox: { type: Boolean, default: false }, isEmailOrWebWidgetInbox: { type: Boolean, default: false }, isTwilioSmsInbox: { type: Boolean, default: false }, isTwilioWhatsAppInbox: { type: Boolean, default: false }, @@ -80,7 +81,8 @@ const shouldShowEmojiButton = computed(() => { const isRegularMessageMode = computed(() => { return ( (!props.isWhatsappInbox && !props.isTwilioWhatsAppInbox) || - props.isWhatsappBaileysInbox + props.isWhatsappBaileysInbox || + props.isWhatsAppZapiInbox ); }); diff --git a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue index 4cad3cf22..1d4bd7986 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/ComposeNewConversationForm.vue @@ -69,6 +69,9 @@ const inboxTypes = computed(() => ({ isWhatsappBaileys: props.targetInbox?.channelType === INBOX_TYPES.WHATSAPP && props.targetInbox?.provider === 'baileys', + isWhatsappZapi: + props.targetInbox?.channelType === INBOX_TYPES.WHATSAPP && + props.targetInbox?.provider === 'zapi', isWebWidget: props.targetInbox?.channelType === INBOX_TYPES.WEB, isApi: props.targetInbox?.channelType === INBOX_TYPES.API, isEmailOrWebWidget: @@ -284,7 +287,9 @@ const handleSendTwilioMessage = async ({ message, templateParams }) => { const shouldShowMessageEditor = computed(() => { return ( - (!inboxTypes.value.isWhatsapp || inboxTypes.value.isWhatsappBaileys) && + (!inboxTypes.value.isWhatsapp || + inboxTypes.value.isWhatsappBaileys || + inboxTypes.value.isWhatsappZapi) && !showNoInboxAlert.value && !inboxTypes.value.isTwilioWhatsapp ); @@ -358,6 +363,7 @@ const shouldShowMessageEditor = computed(() => { :attached-files="state.attachedFiles" :is-whatsapp-inbox="inboxTypes.isWhatsapp" :is-whatsapp-baileys-inbox="inboxTypes.isWhatsappBaileys" + :is-whatsapp-zapi-inbox="inboxTypes.isWhatsappZapi" :is-email-or-web-widget-inbox="inboxTypes.isEmailOrWebWidget" :is-twilio-sms-inbox="inboxTypes.isTwilioSMS" :is-twilio-whats-app-inbox="inboxTypes.isTwilioWhatsapp" diff --git a/app/javascript/dashboard/components/Code.vue b/app/javascript/dashboard/components/Code.vue index e45cacae8..f7946100d 100644 --- a/app/javascript/dashboard/components/Code.vue +++ b/app/javascript/dashboard/components/Code.vue @@ -1,5 +1,5 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue index 4ff8b1ab1..31e1dc9da 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue @@ -12,6 +12,10 @@ const props = defineProps({ type: String, default: '', }, + provider: { + type: String, + default: '', + }, }); const getters = useStoreGetters(); const { t } = useI18n(); @@ -39,6 +43,16 @@ const twilioChannelName = () => { return t(`INBOX_MGMT.CHANNELS.TWILIO_SMS`); }; +const whatsappChannelName = () => { + if (props.provider === 'baileys') { + return t(`INBOX_MGMT.CHANNELS.WHATSAPP_BAILEYS`); + } + if (props.provider === 'zapi') { + return t(`INBOX_MGMT.CHANNELS.WHATSAPP_ZAPI`); + } + return t(`INBOX_MGMT.CHANNELS.WHATSAPP`); +}; + const readableChannelName = computed(() => { if (props.channelType === 'Channel::Api') { return globalConfig.value.apiChannelName || t('INBOX_MGMT.CHANNELS.API'); @@ -46,6 +60,9 @@ const readableChannelName = computed(() => { if (props.channelType === 'Channel::TwilioSms') { return twilioChannelName(); } + if (props.channelType === 'Channel::Whatsapp') { + return whatsappChannelName(); + } return t(`INBOX_MGMT.CHANNELS.${i18nMap[props.channelType]}`); }); diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WhatsappBaileysLinkDeviceModal.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WhatsappLinkDeviceModal.vue similarity index 85% rename from app/javascript/dashboard/routes/dashboard/settings/inbox/components/WhatsappBaileysLinkDeviceModal.vue rename to app/javascript/dashboard/routes/dashboard/settings/inbox/components/WhatsappLinkDeviceModal.vue index a9b5bfd25..461bc4b68 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WhatsappBaileysLinkDeviceModal.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/WhatsappLinkDeviceModal.vue @@ -67,10 +67,14 @@ watchEffect(() => {
@@ -90,7 +94,7 @@ watchEffect(() => { @@ -101,7 +105,7 @@ watchEffect(() => {

{{ $t( - 'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.LOADING_QRCODE' + 'INBOX_MGMT.ADD.WHATSAPP.EXTERNAL_PROVIDER.LINK_DEVICE_MODAL.LOADING_QRCODE' ) }}

@@ -119,7 +123,7 @@ watchEffect(() => {

{{ $t( - 'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.RECONNECTING' + 'INBOX_MGMT.ADD.WHATSAPP.EXTERNAL_PROVIDER.LINK_DEVICE_MODAL.RECONNECTING' ) }}

@@ -130,7 +134,7 @@ watchEffect(() => {

{{ $t( - 'INBOX_MGMT.ADD.WHATSAPP.BAILEYS.LINK_DEVICE_MODAL.CONNECTED' + 'INBOX_MGMT.ADD.WHATSAPP.EXTERNAL_PROVIDER.LINK_DEVICE_MODAL.CONNECTED' ) }}

@@ -138,7 +142,7 @@ watchEffect(() => { diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue index c67fa410b..f883f4142 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue @@ -9,7 +9,7 @@ import NextButton from 'dashboard/components-next/button/Button.vue'; import WhatsappReauthorize from '../channels/whatsapp/Reauthorize.vue'; import { requiredIf } from '@vuelidate/validators'; import { isValidURL } from '../../../../../helper/URLHelper'; -import WhatsappBaileysLinkDeviceModal from '../components/WhatsappBaileysLinkDeviceModal.vue'; +import WhatsappLinkDeviceModal from '../components/WhatsappLinkDeviceModal.vue'; import InboxName from '../../../../../components/widgets/InboxName.vue'; import Switch from 'dashboard/components-next/switch/Switch.vue'; @@ -20,7 +20,7 @@ export default { SmtpSettings, NextButton, WhatsappReauthorize, - WhatsappBaileysLinkDeviceModal, + WhatsappLinkDeviceModal, InboxName, // eslint-disable-next-line vue/no-reserved-component-names Switch, @@ -41,17 +41,26 @@ export default { whatsAppInboxAPIKey: '', isRequestingReauthorization: false, isSyncingTemplates: false, - whatsAppProviderUrl: '', - showBaileysLinkDeviceModal: false, + baileysProviderUrl: '', + showLinkDeviceModal: false, markAsRead: true, + zapiInstanceId: '', + zapiToken: '', + zapiClientToken: '', + zapiTokenUpdate: '', + zapiClientTokenUpdate: '', }; }, validations() { return { whatsAppInboxAPIKey: { - requiredIf: requiredIf(!this.isAWhatsAppBaileysChannel), + requiredIf: requiredIf( + !this.isAWhatsAppBaileysChannel && !this.isAWhatsAppZapiChannel + ), }, - whatsAppProviderUrl: { isValidURL: value => !value || isValidURL(value) }, + baileysProviderUrl: { isValidURL: value => !value || isValidURL(value) }, + zapiTokenUpdate: {}, + zapiClientTokenUpdate: {}, }; }, computed: { @@ -73,8 +82,11 @@ export default { methods: { setDefaults() { this.hmacMandatory = this.inbox.hmac_mandatory || false; - this.whatsAppProviderUrl = this.inbox.provider_config?.provider_url ?? ''; + this.baileysProviderUrl = this.inbox.provider_config?.provider_url ?? ''; this.markAsRead = this.inbox.provider_config?.mark_as_read ?? true; + this.zapiInstanceId = this.inbox.provider_config?.instance_id ?? ''; + this.zapiToken = this.inbox.provider_config?.token ?? ''; + this.zapiClientToken = this.inbox.provider_config?.client_token ?? ''; }, handleHmacFlag() { this.updateInbox(); @@ -131,7 +143,7 @@ export default { this.isSyncingTemplates = false; } }, - async updateWhatsAppProviderUrl() { + async updateBaileysProviderUrl() { try { const payload = { id: this.inbox.id, @@ -139,7 +151,7 @@ export default { channel: { provider_config: { ...this.inbox.provider_config, - provider_url: this.whatsAppProviderUrl, + provider_url: this.baileysProviderUrl, }, }, }; @@ -168,11 +180,47 @@ export default { useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); } }, - onOpenBaileysLinkDeviceModal() { - this.showBaileysLinkDeviceModal = true; + onOpenLinkDeviceModal() { + this.showLinkDeviceModal = true; }, - onCloseBaileysLinkDeviceModal() { - this.showBaileysLinkDeviceModal = false; + onCloseLinkDeviceModal() { + this.showLinkDeviceModal = false; + }, + async updateZapiToken() { + try { + const payload = { + id: this.inbox.id, + formData: false, + channel: { + provider_config: { + ...this.inbox.provider_config, + token: this.zapiTokenUpdate, + }, + }, + }; + await this.$store.dispatch('inboxes/updateInbox', payload); + useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); + } catch (error) { + useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); + } + }, + async updateZapiClientToken() { + try { + const payload = { + id: this.inbox.id, + formData: false, + channel: { + provider_config: { + ...this.inbox.provider_config, + client_token: this.zapiClientTokenUpdate, + }, + }, + }; + await this.$store.dispatch('inboxes/updateInbox', payload); + useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); + } catch (error) { + useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); + } }, }, }; @@ -414,10 +462,10 @@ export default { />
-
@@ -440,7 +488,7 @@ export default { with-phone-number with-provider-connection-status /> - + {{ $t( 'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANAGE_PROVIDER_CONNECTION_BUTTON' @@ -459,25 +507,25 @@ export default { class="flex items-center justify-between flex-1 mt-2 whatsapp-settings--content" > {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }}
- + {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_PROVIDER_URL_ERROR') }} @@ -541,6 +589,125 @@ export default {
+
+ +
+ +
+ + + {{ + $t( + 'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MANAGE_PROVIDER_CONNECTION_BUTTON' + ) + }} + +
+
+ + + + + +
+ + + {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }} + +
+
+ + + +
+ + + {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }} + +
+
+
+