From e032fc7774ce6448d3880e03c61a3656a54b56ec Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Sat, 18 Apr 2026 20:57:27 -0300 Subject: [PATCH] feat(whatsapp): convert inbox between WhatsApp providers (#268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(whatsapp): allow converting inbox between WhatsApp providers Adds a Convert flow to switch a WhatsApp inbox between the four supported providers (default/360dialog, whatsapp_cloud, baileys, zapi) without losing conversations, agents, or history. - Channel::Whatsapp#convert_provider! runs inside a transaction: disconnects the old provider, clears provider_connection and message_templates, assigns the new provider/config, and triggers webhook setup plus template resync on the new service. - New POST /api/v1/accounts/:id/inboxes/:id/convert_provider endpoint guarded by InboxPolicy#convert_provider? (admin only). - UI adds a Convert button on the inbox Settings page with a type-to-confirm ConvertInboxModal that lists the effects before redirecting to a dedicated route reusing the WhatsApp provider wizard in convert mode (phone number locked, current provider hidden from the picker). * chore(whatsapp): polish convert UI colors and expand specs - Settings: use slate for the Convert trigger and ruby for the modal confirm to mirror the delete gate instead of the less conventional amber variant. - Drop the redundant "current provider is hidden from the list" sentence from the convert wizard description. - Add specs for the post-conversion webhook setup path (triggered and skipped branches) and the sync_templates error-rescue behaviour. * fix: address CodeRabbit review on convert-provider flow - Whitelist provider_config keys in the convert endpoint via permit rather than permit!, and default to an empty hash when omitted so the request no longer crashes. - Pre-validate the new provider config before disconnecting the old session so a bad target config no longer terminates the existing provider; also keep the disconnect bound to the old provider_url. - Guard ConvertInboxModal's submit handler so pressing Enter cannot bypass the type-to-confirm gate, and migrate it to + + diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index b844f4b6a..ac5316ccc 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -248,6 +248,7 @@ "WHATSAPP_CLOUD_DESC": "Quick setup through Meta", "TWILIO_DESC": "Connect via Twilio credentials", "360_DIALOG": "360Dialog", + "360_DIALOG_DESC": "Connect via 360Dialog credentials", "BAILEYS": "Baileys", "BAILEYS_DESC": "Connect via non-official API Baileys", "ZAPI": "Z-API", @@ -682,6 +683,29 @@ "AVATAR_ERROR_MESSAGE": "Could not delete the inbox avatar. Please try again later." } }, + "CONVERT": { + "BUTTON": "Convert", + "CONFIRM": { + "TITLE": "Convert inbox provider", + "INTRO": "You are about to change the WhatsApp provider for '{inboxName}' (currently {currentProvider}). Before continuing, review what will happen:", + "EFFECT_DISCONNECT": "The current session with {currentProvider} will be disconnected.", + "EFFECT_TEMPLATES": "Message templates will be cleared and must be resynced on the new provider.", + "EFFECT_CONNECTION": "Provider connection state will be reset.", + "EFFECT_PRESERVED": "Conversations, messages, contacts, agent assignments and history are preserved.", + "EFFECT_IDENTITY": "Inbox name and phone number stay the same.", + "CONFIRM_PROMPT": "To confirm, type the inbox name below:", + "PLACE_HOLDER": "Type {inboxName} to continue", + "CONTINUE": "Continue to convert", + "CANCEL": "Cancel" + }, + "SELECT_PROVIDER_TITLE": "Select the new provider", + "SELECT_PROVIDER_DESCRIPTION": "Converting '{inboxName}' from {currentProvider}.", + "SUBMIT_BUTTON": "Convert inbox", + "API": { + "SUCCESS_MESSAGE": "Inbox successfully converted", + "ERROR_MESSAGE": "Could not convert inbox. Please check the credentials and try again." + } + }, "TABS": { "SETTINGS": "Settings", "COLLABORATORS": "Collaborators", diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json index bd06f5e43..399754847 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json @@ -248,6 +248,7 @@ "WHATSAPP_CLOUD_DESC": "Configuração rápida via Meta", "TWILIO_DESC": "Conectar através de credenciais Twilio", "360_DIALOG": "360Dialog", + "360_DIALOG_DESC": "Conectar através de credenciais 360Dialog", "BAILEYS": "Baileys", "BAILEYS_DESC": "Conectar via API não-oficial Baileys", "ZAPI": "Z-API", @@ -682,6 +683,29 @@ "AVATAR_ERROR_MESSAGE": "Não foi possível excluir o perfil da caixa de entrada. Por favor, tente novamente mais tarde." } }, + "CONVERT": { + "BUTTON": "Converter", + "CONFIRM": { + "TITLE": "Converter provedor da caixa de entrada", + "INTRO": "Você está prestes a trocar o provedor de WhatsApp de '{inboxName}' (atualmente {currentProvider}). Antes de continuar, revise o que vai acontecer:", + "EFFECT_DISCONNECT": "A sessão atual com {currentProvider} será desconectada.", + "EFFECT_TEMPLATES": "Modelos de mensagem serão limpos e precisarão ser re-sincronizados no novo provedor.", + "EFFECT_CONNECTION": "O estado de conexão do provedor será resetado.", + "EFFECT_PRESERVED": "Conversas, mensagens, contatos, atribuições de agentes e histórico são preservados.", + "EFFECT_IDENTITY": "O nome da caixa de entrada e o número de telefone permanecem os mesmos.", + "CONFIRM_PROMPT": "Para confirmar, digite o nome da caixa de entrada abaixo:", + "PLACE_HOLDER": "Digite {inboxName} para continuar", + "CONTINUE": "Continuar conversão", + "CANCEL": "Cancelar" + }, + "SELECT_PROVIDER_TITLE": "Selecione o novo provedor", + "SELECT_PROVIDER_DESCRIPTION": "Convertendo '{inboxName}' de {currentProvider}.", + "SUBMIT_BUTTON": "Converter caixa de entrada", + "API": { + "SUCCESS_MESSAGE": "Caixa de entrada convertida com sucesso", + "ERROR_MESSAGE": "Não foi possível converter a caixa de entrada. Verifique as credenciais e tente novamente." + } + }, "TABS": { "SETTINGS": "Configurações", "COLLABORATORS": "Agentes", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/InboxConvert.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/InboxConvert.vue new file mode 100644 index 000000000..408e1b2ab --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/InboxConvert.vue @@ -0,0 +1,61 @@ + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index ebd4c142e..f7712eab2 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -30,6 +30,7 @@ import SenderNameExamplePreview from './components/SenderNameExamplePreview.vue' import LockToSingleConversationPreview from './components/LockToSingleConversationPreview.vue'; import NextButton from 'dashboard/components-next/button/Button.vue'; import SpinnerLoader from 'dashboard/components-next/spinner/Spinner.vue'; +import ConvertInboxModal from 'dashboard/components/widgets/modal/ConvertInboxModal.vue'; import { INBOX_TYPES } from 'dashboard/helper/inbox'; import { getInboxIconByType } from 'dashboard/helper/inbox'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; @@ -61,6 +62,7 @@ export default { GoogleReauthorize, NextButton, SpinnerLoader, + ConvertInboxModal, InstagramReauthorize, TiktokReauthorize, WhatsappReauthorize, @@ -106,6 +108,7 @@ export default { widgetBubblePosition: 'right', widgetBubbleType: 'standard', widgetBubbleLauncherTitle: '', + showConvertGate: false, }; }, computed: { @@ -139,6 +142,14 @@ export default { } return ''; }, + isConvertibleWhatsAppChannel() { + return ( + this.isAWhatsAppCloudChannel || + this.isAWhatsAppBaileysChannel || + this.isAWhatsAppZapiChannel || + this.is360DialogWhatsAppChannel + ); + }, tabs() { let visibleToAllChannelTabs = [ { @@ -599,6 +610,22 @@ export default { toggleLockToSingleConversation(value) { this.locktoSingleConversation = value; }, + openConvertGate() { + this.showConvertGate = true; + }, + closeConvertGate() { + this.showConvertGate = false; + }, + goToConvert() { + this.showConvertGate = false; + this.$router.push({ + name: 'settings_inbox_convert', + params: { + accountId: this.$route.params.accountId, + inboxId: this.inbox.id, + }, + }); + }, }, validations: { webhookUrl: { @@ -786,12 +813,21 @@ export default { v-if="isAWhatsAppChannel" :label="$t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.LABEL')" > - +
+ + +
+ diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/360DialogWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/360DialogWhatsapp.vue index def828083..5779eca43 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/360DialogWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/360DialogWhatsapp.vue @@ -12,18 +12,38 @@ export default { components: { NextButton, }, + props: { + mode: { + type: String, + default: 'create', + validator: value => ['create', 'convert'].includes(value), + }, + inbox: { + type: Object, + default: null, + }, + }, setup() { return { v$: useVuelidate() }; }, data() { + const isConvert = this.mode === 'convert'; return { - inboxName: '', - phoneNumber: '', + inboxName: isConvert ? this.inbox?.name || '' : '', + phoneNumber: isConvert ? this.inbox?.phone_number || '' : '', apiKey: '', }; }, computed: { ...mapGetters({ uiFlags: 'inboxes/getUIFlags' }), + isConvertMode() { + return this.mode === 'convert'; + }, + submitButtonLabel() { + return this.isConvertMode + ? this.$t('INBOX_MGMT.CONVERT.SUBMIT_BUTTON') + : this.$t('INBOX_MGMT.ADD.WHATSAPP.SUBMIT_BUTTON'); + }, }, validations: { inboxName: { required }, @@ -38,6 +58,24 @@ export default { } try { + if (this.isConvertMode) { + await this.$store.dispatch('inboxes/convertProvider', { + inboxId: this.inbox.id, + provider: 'default', + providerConfig: { api_key: this.apiKey }, + }); + + useAlert(this.$t('INBOX_MGMT.CONVERT.API.SUCCESS_MESSAGE')); + router.replace({ + name: 'settings_inbox_show', + params: { + accountId: router.currentRoute.value.params.accountId, + inboxId: this.inbox.id, + }, + }); + return; + } + const whatsappChannel = await this.$store.dispatch( 'inboxes/createChannel', { @@ -61,7 +99,12 @@ export default { }); } catch (error) { useAlert( - error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE') + error.message || + this.$t( + this.isConvertMode + ? 'INBOX_MGMT.CONVERT.API.ERROR_MESSAGE' + : 'INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE' + ) ); } }, @@ -77,6 +120,7 @@ export default { @@ -92,6 +136,7 @@ export default { @@ -121,8 +166,8 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue index 26e3512ff..57320d30c 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue @@ -12,12 +12,28 @@ import { isValidURL } from '../../../../../helper/URLHelper'; import NextButton from 'dashboard/components-next/button/Button.vue'; import Switch from 'dashboard/components-next/switch/Switch.vue'; +const props = defineProps({ + mode: { + type: String, + default: 'create', + validator: value => ['create', 'convert'].includes(value), + }, + inbox: { + type: Object, + default: null, + }, +}); + +const isConvertMode = computed(() => props.mode === 'convert'); + const router = useRouter(); const store = useStore(); const { t } = useI18n(); -const inboxName = ref(''); -const phoneNumber = ref(''); +const inboxName = ref(isConvertMode.value ? props.inbox?.name || '' : ''); +const phoneNumber = ref( + isConvertMode.value ? props.inbox?.phone_number || '' : '' +); const apiKey = ref(''); const providerUrl = ref(''); const showAdvancedOptions = ref(false); @@ -43,6 +59,20 @@ const v$ = useVuelidate(rules, { apiKey, }); +const buildProviderConfig = () => { + const providerConfig = { + mark_as_read: markAsRead.value, + presence_subscribe: presenceSubscribe.value, + }; + + if (apiKey.value || providerUrl.value) { + providerConfig.api_key = apiKey.value; + providerConfig.provider_url = providerUrl.value; + } + + return providerConfig; +}; + const createChannel = async () => { v$.value.$touch(); if (v$.value.$invalid) { @@ -50,14 +80,22 @@ const createChannel = async () => { } try { - const providerConfig = { - mark_as_read: markAsRead.value, - presence_subscribe: presenceSubscribe.value, - }; + if (isConvertMode.value) { + await store.dispatch('inboxes/convertProvider', { + inboxId: props.inbox.id, + provider: 'baileys', + providerConfig: buildProviderConfig(), + }); - if (apiKey.value || providerUrl.value) { - providerConfig.api_key = apiKey.value; - providerConfig.url = providerUrl.value; + useAlert(t('INBOX_MGMT.CONVERT.API.SUCCESS_MESSAGE')); + router.replace({ + name: 'settings_inbox_show', + params: { + accountId: router.currentRoute.value.params.accountId, + inboxId: props.inbox.id, + }, + }); + return; } const whatsappChannel = await store.dispatch('inboxes/createChannel', { @@ -66,7 +104,7 @@ const createChannel = async () => { type: 'whatsapp', phone_number: phoneNumber.value, provider: 'baileys', - provider_config: providerConfig, + provider_config: buildProviderConfig(), }, }); @@ -78,7 +116,14 @@ const createChannel = async () => { }, }); } catch (error) { - useAlert(error.message || t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE')); + useAlert( + error.message || + t( + isConvertMode.value + ? 'INBOX_MGMT.CONVERT.API.ERROR_MESSAGE' + : 'INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE' + ) + ); } }; @@ -95,6 +140,7 @@ const setShowAdvancedOptions = () => { @@ -110,6 +156,7 @@ const setShowAdvancedOptions = () => { @@ -186,11 +233,15 @@ const setShowAdvancedOptions = () => {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue index 788c0d03c..fed13ea78 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/CloudWhatsapp.vue @@ -12,13 +12,25 @@ export default { components: { NextButton, }, + props: { + mode: { + type: String, + default: 'create', + validator: value => ['create', 'convert'].includes(value), + }, + inbox: { + type: Object, + default: null, + }, + }, setup() { return { v$: useVuelidate() }; }, data() { + const isConvert = this.mode === 'convert'; return { - inboxName: '', - phoneNumber: '', + inboxName: isConvert ? this.inbox?.name || '' : '', + phoneNumber: isConvert ? this.inbox?.phone_number || '' : '', apiKey: '', phoneNumberId: '', businessAccountId: '', @@ -28,6 +40,14 @@ export default { ...mapGetters({ uiFlags: 'inboxes/getUIFlags', }), + isConvertMode() { + return this.mode === 'convert'; + }, + submitButtonLabel() { + return this.isConvertMode + ? this.$t('INBOX_MGMT.CONVERT.SUBMIT_BUTTON') + : this.$t('INBOX_MGMT.ADD.WHATSAPP.SUBMIT_BUTTON'); + }, }, validations: { inboxName: { required }, @@ -37,6 +57,13 @@ export default { businessAccountId: { required, isNumber }, }, methods: { + buildProviderConfig() { + return { + api_key: this.apiKey, + phone_number_id: this.phoneNumberId, + business_account_id: this.businessAccountId, + }; + }, async createChannel() { this.v$.$touch(); if (this.v$.$invalid) { @@ -44,6 +71,24 @@ export default { } try { + if (this.isConvertMode) { + await this.$store.dispatch('inboxes/convertProvider', { + inboxId: this.inbox.id, + provider: 'whatsapp_cloud', + providerConfig: this.buildProviderConfig(), + }); + + useAlert(this.$t('INBOX_MGMT.CONVERT.API.SUCCESS_MESSAGE')); + router.replace({ + name: 'settings_inbox_show', + params: { + accountId: router.currentRoute.value.params.accountId, + inboxId: this.inbox.id, + }, + }); + return; + } + const whatsappChannel = await this.$store.dispatch( 'inboxes/createChannel', { @@ -52,11 +97,7 @@ export default { type: 'whatsapp', phone_number: this.phoneNumber, provider: 'whatsapp_cloud', - provider_config: { - api_key: this.apiKey, - phone_number_id: this.phoneNumberId, - business_account_id: this.businessAccountId, - }, + provider_config: this.buildProviderConfig(), }, } ); @@ -70,7 +111,12 @@ export default { }); } catch (error) { useAlert( - error.message || this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE') + error.message || + this.$t( + this.isConvertMode + ? 'INBOX_MGMT.CONVERT.API.ERROR_MESSAGE' + : 'INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE' + ) ); } }, @@ -86,6 +132,7 @@ export default { @@ -101,6 +148,7 @@ export default { @@ -167,11 +215,11 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue index c84d7d59c..1547c0e65 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue @@ -11,6 +11,20 @@ import BaileysWhatsapp from './BaileysWhatsapp.vue'; import ZapiWhatsapp from './ZapiWhatsapp.vue'; import PromoBanner from 'dashboard/components-next/banner/PromoBanner.vue'; +const props = defineProps({ + mode: { + type: String, + default: 'create', + validator: value => ['create', 'convert'].includes(value), + }, + inbox: { + type: Object, + default: null, + }, +}); + +const isConvertMode = computed(() => props.mode === 'convert'); + const route = useRoute(); const router = useRouter(); const { t } = useI18n(); @@ -35,41 +49,100 @@ const hasWhatsappAppId = computed(() => { const selectedProvider = computed(() => route.query.provider); -const showProviderSelection = computed(() => !selectedProvider.value); +const INBOX_PROVIDER_TO_KEY = { + whatsapp_cloud: PROVIDER_TYPES.WHATSAPP, + default: PROVIDER_TYPES.THREE_SIXTY_DIALOG, + baileys: PROVIDER_TYPES.BAILEYS, + zapi: PROVIDER_TYPES.ZAPI, +}; -const showConfiguration = computed(() => Boolean(selectedProvider.value)); +const currentProviderKey = computed(() => { + if (!props.inbox?.provider) return null; + return INBOX_PROVIDER_TO_KEY[props.inbox.provider] || null; +}); + +const PROVIDER_CATALOG = computed(() => [ + { + key: PROVIDER_TYPES.WHATSAPP, + title: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WHATSAPP_CLOUD'), + description: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WHATSAPP_CLOUD_DESC'), + icon: 'i-woot-whatsapp', + }, + { + key: PROVIDER_TYPES.TWILIO, + title: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO'), + description: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO_DESC'), + icon: 'i-woot-twilio', + }, + { + key: PROVIDER_TYPES.BAILEYS, + title: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.BAILEYS'), + description: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.BAILEYS_DESC'), + icon: 'i-woot-baileys', + }, + { + key: PROVIDER_TYPES.ZAPI, + title: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.ZAPI'), + description: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.ZAPI_DESC'), + icon: 'i-woot-zapi', + }, + { + key: PROVIDER_TYPES.THREE_SIXTY_DIALOG, + title: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.360_DIALOG'), + description: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.360_DIALOG_DESC'), + icon: 'i-woot-whatsapp', + }, +]); + +// Keys shown in the picker. 360Dialog is intentionally hidden in create mode +// (URL-reachable only) but offered in convert mode where it is a valid target. +const CREATE_PICKER_KEYS = [ + PROVIDER_TYPES.WHATSAPP, + PROVIDER_TYPES.TWILIO, + PROVIDER_TYPES.BAILEYS, + PROVIDER_TYPES.ZAPI, +]; +const CONVERT_PICKER_KEYS = [ + PROVIDER_TYPES.WHATSAPP, + PROVIDER_TYPES.BAILEYS, + PROVIDER_TYPES.ZAPI, + PROVIDER_TYPES.THREE_SIXTY_DIALOG, +]; const availableProviders = computed(() => { - const providers = [ - { - key: PROVIDER_TYPES.WHATSAPP, - title: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WHATSAPP_CLOUD'), - description: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WHATSAPP_CLOUD_DESC'), - icon: 'i-woot-whatsapp', - }, - { - key: PROVIDER_TYPES.TWILIO, - title: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO'), - description: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.TWILIO_DESC'), - icon: 'i-woot-twilio', - }, - { - key: PROVIDER_TYPES.BAILEYS, - title: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.BAILEYS'), - description: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.BAILEYS_DESC'), - icon: 'i-woot-baileys', - }, - { - key: PROVIDER_TYPES.ZAPI, - title: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.ZAPI'), - description: t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.ZAPI_DESC'), - icon: 'i-woot-zapi', - }, - ]; - - return providers; + const allowed = isConvertMode.value + ? CONVERT_PICKER_KEYS + : CREATE_PICKER_KEYS; + return PROVIDER_CATALOG.value + .filter(p => allowed.includes(p.key)) + .filter(p => !isConvertMode.value || p.key !== currentProviderKey.value); }); +const currentProviderLabel = computed(() => { + if (!isConvertMode.value || !currentProviderKey.value) return ''; + return ( + PROVIDER_CATALOG.value.find(({ key }) => key === currentProviderKey.value) + ?.title || '' + ); +}); + +const isValidSelectedProvider = computed(() => { + if (!selectedProvider.value) return false; + // In create mode, allow the embedded-signup manual fallback link and the + // legacy-URL path to 360Dialog even though neither is in the picker. + if (!isConvertMode.value) { + if (selectedProvider.value === PROVIDER_TYPES.WHATSAPP_MANUAL) return true; + if (selectedProvider.value === PROVIDER_TYPES.THREE_SIXTY_DIALOG) + return true; + } + return availableProviders.value.some( + ({ key }) => key === selectedProvider.value + ); +}); + +const showProviderSelection = computed(() => !isValidSelectedProvider.value); +const showConfiguration = computed(() => isValidSelectedProvider.value); + const selectProvider = providerValue => { router.push({ name: route.name, @@ -81,7 +154,8 @@ const selectProvider = providerValue => { const shouldShowCloudWhatsapp = provider => { return ( provider === PROVIDER_TYPES.WHATSAPP_MANUAL || - (provider === PROVIDER_TYPES.WHATSAPP && !hasWhatsappAppId.value) + (provider === PROVIDER_TYPES.WHATSAPP && + (!hasWhatsappAppId.value || isConvertMode.value)) ); }; @@ -95,10 +169,21 @@ const handleManualLinkClick = () => {

- {{ $t('INBOX_MGMT.ADD.WHATSAPP.SELECT_PROVIDER.TITLE') }} + {{ + isConvertMode + ? $t('INBOX_MGMT.CONVERT.SELECT_PROVIDER_TITLE') + : $t('INBOX_MGMT.ADD.WHATSAPP.SELECT_PROVIDER.TITLE') + }}

- {{ $t('INBOX_MGMT.ADD.WHATSAPP.SELECT_PROVIDER.DESCRIPTION') }} + {{ + isConvertMode + ? $t('INBOX_MGMT.CONVERT.SELECT_PROVIDER_DESCRIPTION', { + inboxName: inbox?.name, + currentProvider: currentProviderLabel, + }) + : $t('INBOX_MGMT.ADD.WHATSAPP.SELECT_PROVIDER.DESCRIPTION') + }}

@@ -113,7 +198,7 @@ const handleManualLinkClick = () => { />
-
+
{
@@ -172,7 +259,11 @@ const handleManualLinkClick = () => {
- + { /> - + -
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ZapiWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ZapiWhatsapp.vue index 1610aef81..f254bd413 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ZapiWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ZapiWhatsapp.vue @@ -11,12 +11,28 @@ import { isPhoneE164OrEmpty } from 'shared/helpers/Validators'; import NextButton from 'dashboard/components-next/button/Button.vue'; import PromoBanner from 'dashboard/components-next/banner/PromoBanner.vue'; +const props = defineProps({ + mode: { + type: String, + default: 'create', + validator: value => ['create', 'convert'].includes(value), + }, + inbox: { + type: Object, + default: null, + }, +}); + +const isConvertMode = computed(() => props.mode === 'convert'); + const router = useRouter(); const store = useStore(); const { t } = useI18n(); -const inboxName = ref(''); -const phoneNumber = ref(''); +const inboxName = ref(isConvertMode.value ? props.inbox?.name || '' : ''); +const phoneNumber = ref( + isConvertMode.value ? props.inbox?.phone_number || '' : '' +); const instanceId = ref(''); const token = ref(''); const clientToken = ref(''); @@ -43,6 +59,12 @@ const v$ = useVuelidate(rules, { clientToken, }); +const buildProviderConfig = () => ({ + instance_id: instanceId.value, + token: token.value, + client_token: clientToken.value, +}); + const createChannel = async () => { v$.value.$touch(); if (v$.value.$invalid) { @@ -50,17 +72,31 @@ const createChannel = async () => { } try { + if (isConvertMode.value) { + await store.dispatch('inboxes/convertProvider', { + inboxId: props.inbox.id, + provider: 'zapi', + providerConfig: buildProviderConfig(), + }); + + useAlert(t('INBOX_MGMT.CONVERT.API.SUCCESS_MESSAGE')); + router.replace({ + name: 'settings_inbox_show', + params: { + accountId: router.currentRoute.value.params.accountId, + inboxId: props.inbox.id, + }, + }); + return; + } + const whatsappChannel = await store.dispatch('inboxes/createChannel', { name: inboxName.value, channel: { type: 'whatsapp', phone_number: phoneNumber.value, provider: 'zapi', - provider_config: { - instance_id: instanceId.value, - token: token.value, - client_token: clientToken.value, - }, + provider_config: buildProviderConfig(), }, }); @@ -72,14 +108,21 @@ const createChannel = async () => { }, }); } catch (error) { - useAlert(error.message || t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE')); + useAlert( + error.message || + t( + isConvertMode.value + ? 'INBOX_MGMT.CONVERT.API.ERROR_MESSAGE' + : 'INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE' + ) + ); } };