From 950d8718302ccd5a1023b809872f4cc61a622359 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 4 Aug 2026 15:13:59 +0400 Subject: [PATCH] fix(meta): add independent incident runtime controls (#15318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This restores the temporary Meta incident safeguards with two independent runtime controls for Chatwoot Cloud. Super admins can now re-enable Instagram messaging first and keep new Meta inbox creation disabled until onboarding is stable. When messaging is disabled, Instagram conversations show the incident notice and remain in private-note mode. The separate inbox-creation control disables Facebook, Instagram, and WhatsApp Embedded Signup entry points. Self-hosted installations remain unchanged. Related: https://github.com/chatwoot/chatwoot/pull/15210 Related: https://status.chatwoot.com/incident/976975 ### Things to know - `DISABLE_META_MESSAGE_SENDING` affects Instagram messaging only. - `DISABLE_META_INBOX_CREATION` affects Facebook, Instagram, and WhatsApp Embedded Signup inbox creation. - Both controls default to `true` for the active incident and are editable from Super Admin installation configs. - The Cloud-only boundary is enforced by the dashboard configuration getters. ### How to test 1. Open Super Admin → Installation Configs and set `DISABLE_META_MESSAGE_SENDING` to `false`. 2. Reload an Instagram conversation and confirm the incident banner disappears and the runtime restriction no longer forces private-note mode. 3. Set the flag back to `true`, reload, and confirm the banner and restriction return. 4. Set `DISABLE_META_INBOX_CREATION` to `false`, reload the Facebook, Instagram, or WhatsApp Embedded Signup setup flow, and confirm connection is enabled. 5. Set the flag back to `true`, reload, and confirm the incident notice appears and connection is disabled. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> --- app/controllers/dashboard_controller.rb | 2 + .../super_admin/app_configs_controller.rb | 11 ++- .../widgets/conversation/MessagesView.vue | 9 +- .../widgets/conversation/ReplyBox.vue | 52 ++++++++++-- .../conversation/specs/ReplyBox.spec.js | 83 ++++++++++++++++++- .../dashboard/composables/useAccount.js | 8 ++ app/javascript/dashboard/constants/globals.js | 1 - .../dashboard/onboarding/InboxSetup.vue | 15 ++-- .../inbox-setup/useChannelConfig.js | 16 ++-- .../inbox-setup/useChannelConnect.js | 9 +- .../inbox-setup/InboxChannelsDialog.spec.js | 11 ++- .../inbox-setup/useDetectedChannels.spec.js | 5 ++ .../dashboard/settings/inbox/Settings.vue | 12 +-- .../settings/inbox/channels/Facebook.vue | 11 +-- .../settings/inbox/channels/Instagram.vue | 9 +- .../settings/inbox/channels/Whatsapp.vue | 13 +-- app/javascript/shared/store/globalConfig.js | 8 ++ config/installation_config.yml | 12 +++ 18 files changed, 214 insertions(+), 73 deletions(-) diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index a72687b42..1dc1921b9 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -23,6 +23,8 @@ class DashboardController < ActionController::Base HCAPTCHA_SITE_KEY LOGOUT_REDIRECT_LINK DISABLE_USER_PROFILE_UPDATE + DISABLE_META_INBOX_CREATION + DISABLE_META_MESSAGE_SENDING DEPLOYMENT_ENV INSTALLATION_PRICING_PLAN ].freeze diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 86d1b70ef..593b40761 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -1,4 +1,8 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController + GENERAL_CONFIGS = %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS WEBHOOK_TIMEOUT MAXIMUM_FILE_UPLOAD_SIZE + WIDGET_TOKEN_EXPIRY].freeze + META_INCIDENT_CONFIGS = %w[DISABLE_META_INBOX_CREATION DISABLE_META_MESSAGE_SENDING].freeze + before_action :set_config before_action :allowed_configs def show @@ -38,6 +42,8 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController end def allowed_configs + general_configs = GENERAL_CONFIGS + (ChatwootApp.chatwoot_cloud? ? META_INCIDENT_CONFIGS : []) + mapping = { 'facebook' => %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET IG_VERIFY_TOKEN FACEBOOK_API_VERSION ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT], 'shopify' => %w[SHOPIFY_CLIENT_ID SHOPIFY_CLIENT_SECRET], @@ -53,10 +59,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController 'captain' => %w[CAPTAIN_OPEN_AI_API_KEY CAPTAIN_OPEN_AI_MODEL CAPTAIN_OPEN_AI_ENDPOINT] } - @allowed_configs = mapping.fetch( - @config, - %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS WEBHOOK_TIMEOUT MAXIMUM_FILE_UPLOAD_SIZE WIDGET_TOKEN_EXPIRY] - ) + @allowed_configs = mapping.fetch(@config, general_configs) end def success_notice diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 4cc3cafc1..071652580 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -34,7 +34,6 @@ import { import { BUS_EVENTS } from 'shared/constants/busEvents'; import { REPLY_POLICY } from 'shared/constants/links'; import wootConstants, { - IS_META_INBOX_CREATION_DISABLED, META_RESTRICTION_STATUS_URL, } from 'dashboard/constants/globals'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; @@ -96,7 +95,7 @@ export default { currentUserId: 'getCurrentUserID', listLoadingStatus: 'getAllMessagesLoaded', currentAccountId: 'getCurrentAccountId', - isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', + isMetaMessageSendingDisabled: 'globalConfig/isMetaMessageSendingDisabled', }), isOpen() { return this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN; @@ -175,11 +174,7 @@ export default { ); }, isInstagramRestrictionBannerVisible() { - return ( - this.isOnChatwootCloud && - IS_META_INBOX_CREATION_DISABLED && - this.isAnInstagramChannel - ); + return this.isMetaMessageSendingDisabled && this.isAnInstagramChannel; }, instagramRestrictionStatusUrl() { return META_RESTRICTION_STATUS_URL; diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index c8c350043..b0894f8f0 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -121,6 +121,8 @@ export default { recordingAudioState: '', recordingAudioDurationText: '', replyType: REPLY_EDITOR_MODES.REPLY, + draftConversationId: null, + draftReplyMode: null, bccEmails: '', ccEmails: '', toEmails: '', @@ -146,6 +148,7 @@ export default { currentUser: 'getCurrentUser', lastEmail: 'getLastEmailInSelectedChat', globalConfig: 'globalConfig/get', + isMetaMessageSendingDisabled: 'globalConfig/isMetaMessageSendingDisabled', }), currentContact() { const senderId = this.currentChat?.meta?.sender?.id; @@ -181,15 +184,24 @@ export default { }, canSendPublicReply() { return ( - this.isWithinMessagingWindow && !this.isBotOwnedPendingConversation + this.isWithinMessagingWindow && + !this.isBotOwnedPendingConversation && + !this.isInstagramReplyRestricted ); }, + isInstagramReplyRestricted() { + return this.isMetaMessageSendingDisabled && this.isAnInstagramChannel; + }, isPrivate() { return ( !this.canSendPublicReply || this.replyType === REPLY_EDITOR_MODES.NOTE ); }, isOnPrivateNote() { + if (this.isInstagramReplyRestricted) { + return true; + } + return this.isBotOwnedPendingConversation ? this.isPrivate : this.replyType === REPLY_EDITOR_MODES.NOTE; @@ -479,6 +491,11 @@ export default { this.copilot.reset(); } + if (this.isInstagramReplyRestricted) { + this.replyType = REPLY_EDITOR_MODES.NOTE; + return; + } + if (this.isOnPrivateNote) { return; } @@ -503,8 +520,7 @@ export default { }, conversationIdByRoute(conversationId, oldConversationId) { if (conversationId !== oldConversationId) { - this.setToDraft(oldConversationId, this.effectiveReplyMode); - this.getFromDraft(); + this.switchDraftContext(conversationId, this.effectiveReplyMode); this.resetRecorderAndClearAttachments(); } }, @@ -518,20 +534,26 @@ export default { showContentTemplates(isAvailable) { if (!isAvailable) this.hideContentTemplatesModal(); }, - effectiveReplyMode(updatedReplyType, oldReplyType) { + effectiveReplyMode(updatedReplyType) { this.$store.dispatch('draftMessages/setReplyEditorMode', { mode: updatedReplyType, }); - this.setToDraft(this.conversationIdByRoute, oldReplyType); - this.getFromDraft(); + this.switchDraftContext(this.conversationIdByRoute, updatedReplyType); }, }, mounted() { + if (this.isInstagramReplyRestricted) { + this.replyType = REPLY_EDITOR_MODES.NOTE; + } + this.$store.dispatch('draftMessages/setReplyEditorMode', { mode: this.effectiveReplyMode, }); - this.getFromDraft(); + this.switchDraftContext( + this.conversationIdByRoute, + this.effectiveReplyMode + ); // Don't use the keyboard listener mixin here as the events here are supposed to be // working even if the editor is focussed. document.addEventListener('paste', this.onPaste); @@ -654,6 +676,22 @@ export default { this.saveDraft(conversationId, replyType); this.message = ''; }, + switchDraftContext(conversationId, replyMode) { + if ( + this.draftConversationId === conversationId && + this.draftReplyMode === replyMode + ) { + return; + } + + if (this.draftConversationId) { + this.setToDraft(this.draftConversationId, this.draftReplyMode); + } + + this.draftConversationId = conversationId; + this.draftReplyMode = replyMode; + this.getFromDraft(); + }, getFromDraft() { if (this.conversationIdByRoute) { const key = this.getDraftKey(); diff --git a/app/javascript/dashboard/components/widgets/conversation/specs/ReplyBox.spec.js b/app/javascript/dashboard/components/widgets/conversation/specs/ReplyBox.spec.js index 135a0a638..7de60cd4c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/specs/ReplyBox.spec.js +++ b/app/javascript/dashboard/components/widgets/conversation/specs/ReplyBox.spec.js @@ -33,7 +33,14 @@ const REPLIABLE = { messages: [], }; -const buildStore = ({ inbox, chat, templates, drafts = {} }) => +const buildStore = ({ + inbox, + chat, + templates, + drafts = {}, + inboxes, + isMetaMessageSendingDisabled = false, +}) => createStore({ state: { chat: { ...REPLIABLE, ...chat }, @@ -64,7 +71,12 @@ const buildStore = ({ inbox, chat, templates, drafts = {} }) => getUISettings: () => ({}), getLastEmailInSelectedChat: () => null, 'globalConfig/get': () => ({}), - 'inboxes/getInbox': () => () => ({ id: 1, ...inbox }), + 'globalConfig/isMetaMessageSendingDisabled': () => + isMetaMessageSendingDisabled, + 'inboxes/getInbox': () => inboxId => ({ + id: inboxId, + ...(inboxes?.[inboxId] || inbox), + }), 'inboxes/getWhatsAppTemplates': () => () => templates, 'contacts/getContact': () => () => ({}), 'draftMessages/get': s => key => s.drafts[key] || '', @@ -81,8 +93,17 @@ const mountWith = ({ chat, templates = [{ name: 'greeting' }], drafts, + inboxes, + isMetaMessageSendingDisabled, }) => { - const store = buildStore({ inbox, chat, templates, drafts }); + const store = buildStore({ + inbox, + chat, + templates, + drafts, + inboxes, + isMetaMessageSendingDisabled, + }); const wrapper = shallowMount(ReplyBox, { global: { plugins: [store], @@ -103,6 +124,62 @@ const editor = wrapper => wrapper.findComponent({ name: 'WootMessageEditor' }).props(); describe('ReplyBox', () => { + describe('Instagram incident restriction', () => { + it('opens in note mode and restores only the private-note draft', async () => { + const { wrapper, store } = mountWith({ + inbox: { channel_type: 'Channel::Instagram' }, + isMetaMessageSendingDisabled: true, + drafts: { + 'draft-1-REPLY': 'unsent public reply', + 'draft-1-NOTE': 'incident note', + }, + }); + await nextTick(); + + expect(topPanel(wrapper).mode).toBe(REPLY_EDITOR_MODES.NOTE); + expect(topPanel(wrapper).isReplyRestricted).toBe(true); + expect(bottomPanel(wrapper).isOnPrivateNote).toBe(true); + expect(editor(wrapper).editorId).toBe('draft-1-NOTE'); + expect(wrapper.vm.message).toBe('incident note'); + expect(store.getters['draftMessages/getReplyEditorMode']).toBe( + REPLY_EDITOR_MODES.NOTE + ); + expect(store.getters['draftMessages/get']('draft-1-REPLY')).toBe( + 'unsent public reply' + ); + }); + + it('preserves draft ownership when switching to a restricted conversation', async () => { + const drafts = { + 'draft-1-REPLY': 'conversation A reply', + 'draft-1-NOTE': 'conversation A note', + 'draft-2-REPLY': 'conversation B reply', + 'draft-2-NOTE': 'conversation B note', + }; + const { wrapper, store } = mountWith({ + inbox: { channel_type: 'Channel::WebWidget' }, + inboxes: { + 1: { channel_type: 'Channel::WebWidget' }, + 2: { channel_type: 'Channel::Instagram' }, + }, + drafts, + isMetaMessageSendingDisabled: true, + }); + await nextTick(); + + store.commit('selectChat', { ...REPLIABLE, id: 2, inbox_id: 2 }); + await nextTick(); + + expect(editor(wrapper)).toMatchObject({ + editorId: 'draft-2-NOTE', + modelValue: 'conversation B note', + }); + Object.entries(drafts).forEach(([key, message]) => { + expect(store.getters['draftMessages/get'](key)).toBe(message); + }); + }); + }); + describe.each(CHANNELS)('$name', ({ name, inbox }) => { it('locks the composer and hides template sends when a bot owns a pending conversation', () => { const { wrapper } = mountWith({ diff --git a/app/javascript/dashboard/composables/useAccount.js b/app/javascript/dashboard/composables/useAccount.js index c6149044f..02cf26726 100644 --- a/app/javascript/dashboard/composables/useAccount.js +++ b/app/javascript/dashboard/composables/useAccount.js @@ -15,6 +15,12 @@ export function useAccount() { const store = useStore(); const getAccountFn = useMapGetter('accounts/getAccount'); const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud'); + const isMetaInboxCreationDisabled = useMapGetter( + 'globalConfig/isMetaInboxCreationDisabled' + ); + const isMetaMessageSendingDisabled = useMapGetter( + 'globalConfig/isMetaMessageSendingDisabled' + ); const isFeatureEnabledonAccount = useMapGetter( 'accounts/isFeatureEnabledonAccount' ); @@ -64,6 +70,8 @@ export function useAccount() { accountScopedRoute, isCloudFeatureEnabled, isOnChatwootCloud, + isMetaInboxCreationDisabled, + isMetaMessageSendingDisabled, updateAccount, finishOnboarding, }; diff --git a/app/javascript/dashboard/constants/globals.js b/app/javascript/dashboard/constants/globals.js index 698072345..da913464a 100644 --- a/app/javascript/dashboard/constants/globals.js +++ b/app/javascript/dashboard/constants/globals.js @@ -78,6 +78,5 @@ export default { }, }; export const DEFAULT_REDIRECT_URL = '/app/'; -export const IS_META_INBOX_CREATION_DISABLED = true; export const META_RESTRICTION_STATUS_URL = 'https://status.chatwoot.com/incidents'; diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/InboxSetup.vue b/app/javascript/dashboard/routes/dashboard/onboarding/InboxSetup.vue index 8248d26b4..b3f206cf7 100644 --- a/app/javascript/dashboard/routes/dashboard/onboarding/InboxSetup.vue +++ b/app/javascript/dashboard/routes/dashboard/onboarding/InboxSetup.vue @@ -20,16 +20,17 @@ import { useChannelConnect } from './inbox-setup/useChannelConnect'; import { useDetectedChannels } from './inbox-setup/useDetectedChannels'; import { DIALOG_CHANNELS } from './inbox-setup/constants'; import Banner from 'dashboard/components-next/banner/Banner.vue'; -import { - IS_META_INBOX_CREATION_DISABLED, - META_RESTRICTION_STATUS_URL, -} from 'dashboard/constants/globals'; +import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals'; const { t } = useI18n(); const store = useStore(); const router = useRouter(); -const { accountId, currentAccount, finishOnboarding, isOnChatwootCloud } = - useAccount(); +const { + accountId, + currentAccount, + finishOnboarding, + isMetaInboxCreationDisabled, +} = useAccount(); const { isEnterprise } = useConfig(); const { connectViaOAuth, connectWhatsapp } = useChannelConnect(); @@ -50,7 +51,7 @@ const { const channelsDialogRef = ref(null); const showMetaRestrictionBanner = computed( - () => isOnChatwootCloud.value && IS_META_INBOX_CREATION_DISABLED + () => isMetaInboxCreationDisabled.value ); // The initial inboxes fetch happens in WebWidgetCreationStatus, which polls diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConfig.js b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConfig.js index 1557ad0e2..7ee16a06b 100644 --- a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConfig.js +++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConfig.js @@ -1,7 +1,6 @@ import { useMapGetter } from 'dashboard/composables/store'; import { useAccount } from 'dashboard/composables/useAccount'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; -import { IS_META_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals'; // OAuth/SDK channels need installation-level app credentials to be usable. When // the credential is missing the channel is "not configured" and is hidden from @@ -10,26 +9,27 @@ import { IS_META_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals'; // Mirrors the availability checks in ChannelItem.vue. export function useChannelConfig() { const globalConfig = useMapGetter('globalConfig/get'); - const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud'); - const { isCloudFeatureEnabled } = useAccount(); + const { + isCloudFeatureEnabled, + isOnChatwootCloud, + isMetaInboxCreationDisabled, + } = useAccount(); const installationConfig = window.chatwootConfig || {}; - const isMetaInboxCreationDisabled = () => - isOnChatwootCloud.value && IS_META_INBOX_CREATION_DISABLED; const CHANNEL_CONFIGURED = { // WhatsApp is onboarded only via Meta embedded signup, which needs both the // app id (not the 'none' sentinel) and the signup configuration id. whatsapp: () => - !isMetaInboxCreationDisabled() && + !isMetaInboxCreationDisabled.value && (!isOnChatwootCloud.value || isCloudFeatureEnabled(FEATURE_FLAGS.WHATSAPP_EMBEDDED_SIGNUP_FLOW)) && Boolean(installationConfig.whatsappAppId) && installationConfig.whatsappAppId !== 'none' && Boolean(installationConfig.whatsappConfigurationId), facebook: () => - !isMetaInboxCreationDisabled() && Boolean(installationConfig.fbAppId), + !isMetaInboxCreationDisabled.value && Boolean(installationConfig.fbAppId), instagram: () => - !isMetaInboxCreationDisabled() && + !isMetaInboxCreationDisabled.value && Boolean(installationConfig.instagramAppId) && isCloudFeatureEnabled(FEATURE_FLAGS.CHANNEL_INSTAGRAM), tiktok: () => Boolean(installationConfig.tiktokAppId), diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConnect.js b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConnect.js index 8ea97dcc0..e6f1316ce 100644 --- a/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConnect.js +++ b/app/javascript/dashboard/routes/dashboard/onboarding/inbox-setup/useChannelConnect.js @@ -3,7 +3,6 @@ import { useAlert } from 'dashboard/composables'; import { useStore } from 'dashboard/composables/store'; import { useAccount } from 'dashboard/composables/useAccount'; import { useWhatsappEmbeddedSignup } from 'dashboard/composables/useWhatsappEmbeddedSignup'; -import { IS_META_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals'; import { parseAPIErrorResponse } from 'dashboard/store/utils/api'; import googleClient from 'dashboard/api/channel/googleClient'; import microsoftClient from 'dashboard/api/channel/microsoftClient'; @@ -24,16 +23,14 @@ const OAUTH_CLIENTS = { export function useChannelConnect() { const { t } = useI18n(); const store = useStore(); - const { isOnChatwootCloud } = useAccount(); + const { isMetaInboxCreationDisabled } = useAccount(); const { runEmbeddedSignup } = useWhatsappEmbeddedSignup(); - const isMetaInboxCreationDisabled = () => - isOnChatwootCloud.value && IS_META_INBOX_CREATION_DISABLED; const connectViaOAuth = async provider => { const client = OAUTH_CLIENTS[provider]; if (!client) return; - if (provider === 'instagram' && isMetaInboxCreationDisabled()) { + if (provider === 'instagram' && isMetaInboxCreationDisabled.value) { useAlert(t('ONBOARDING_INBOX_SETUP.META_RESTRICTION.MESSAGE')); return; } @@ -53,7 +50,7 @@ export function useChannelConnect() { // inbox, and surface the result inline — then refetch so the connected state // reflects the freshly created inbox (and renders its real channel icon). const connectWhatsapp = async () => { - if (isMetaInboxCreationDisabled()) { + if (isMetaInboxCreationDisabled.value) { useAlert(t('ONBOARDING_INBOX_SETUP.META_RESTRICTION.MESSAGE')); return; } diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/specs/inbox-setup/InboxChannelsDialog.spec.js b/app/javascript/dashboard/routes/dashboard/onboarding/specs/inbox-setup/InboxChannelsDialog.spec.js index 19bd13ac5..85b648378 100644 --- a/app/javascript/dashboard/routes/dashboard/onboarding/specs/inbox-setup/InboxChannelsDialog.spec.js +++ b/app/javascript/dashboard/routes/dashboard/onboarding/specs/inbox-setup/InboxChannelsDialog.spec.js @@ -2,8 +2,9 @@ import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import InboxChannelsDialog from '../../inbox-setup/InboxChannelsDialog.vue'; -const { isOnChatwootCloud } = vi.hoisted(() => ({ +const { isOnChatwootCloud, isMetaInboxCreationDisabled } = vi.hoisted(() => ({ isOnChatwootCloud: { value: false }, + isMetaInboxCreationDisabled: { value: false }, })); vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: key => key }) })); @@ -14,7 +15,11 @@ vi.mock('dashboard/composables/store', () => ({ : { value: {} }, })); vi.mock('dashboard/composables/useAccount', () => ({ - useAccount: () => ({ isCloudFeatureEnabled: () => true }), + useAccount: () => ({ + isCloudFeatureEnabled: () => true, + isOnChatwootCloud, + isMetaInboxCreationDisabled, + }), })); vi.mock('../../inbox-setup/useChannelConnect', () => ({ useChannelConnect: () => ({ @@ -44,6 +49,7 @@ describe('InboxChannelsDialog Facebook gating', () => { afterEach(() => { delete window.chatwootConfig; isOnChatwootCloud.value = false; + isMetaInboxCreationDisabled.value = false; }); it('opens the Facebook page picker when fbAppId is configured', async () => { @@ -70,6 +76,7 @@ describe('InboxChannelsDialog Facebook gating', () => { it('shows the grid when Meta inbox creation is disabled on Chatwoot Cloud', async () => { isOnChatwootCloud.value = true; + isMetaInboxCreationDisabled.value = true; window.chatwootConfig = { fbAppId: 'fb-app' }; const wrapper = mountDialog(); diff --git a/app/javascript/dashboard/routes/dashboard/onboarding/specs/inbox-setup/useDetectedChannels.spec.js b/app/javascript/dashboard/routes/dashboard/onboarding/specs/inbox-setup/useDetectedChannels.spec.js index 689348711..a0cb0fefc 100644 --- a/app/javascript/dashboard/routes/dashboard/onboarding/specs/inbox-setup/useDetectedChannels.spec.js +++ b/app/javascript/dashboard/routes/dashboard/onboarding/specs/inbox-setup/useDetectedChannels.spec.js @@ -16,6 +16,7 @@ const mountComposable = ({ features = { channel_instagram: true }, inboxes = [], isOnChatwootCloud = false, + disableMetaInboxCreation = false, } = {}) => { const store = createStore({ modules: { @@ -24,6 +25,9 @@ const mountComposable = ({ getters: { get: () => ({}), isOnChatwootCloud: () => isOnChatwootCloud, + isMetaInboxCreationDisabled: () => + isOnChatwootCloud && disableMetaInboxCreation, + isMetaMessageSendingDisabled: () => false, }, }, accounts: { @@ -218,6 +222,7 @@ describe('useDetectedChannels', () => { whatsapp_embedded_signup_inbox_creation: true, }, isOnChatwootCloud: true, + disableMetaInboxCreation: true, brandInfo: { socials: [ { type: 'whatsapp', url: 'https://wa.me/14155552671' }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index c729fd474..cb4062a98 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -46,10 +46,7 @@ import SelectInput from 'dashboard/components-next/select/Select.vue'; import Widget from 'dashboard/modules/widget-preview/components/Widget.vue'; import AccessToken from 'dashboard/routes/dashboard/settings/profile/AccessToken.vue'; import { copyTextToClipboard } from 'shared/helpers/clipboard'; -import { - IS_META_INBOX_CREATION_DISABLED, - META_RESTRICTION_STATUS_URL, -} from 'dashboard/constants/globals'; +import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals'; export default { components: { @@ -130,6 +127,7 @@ export default { accountId: 'getCurrentAccountId', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', isOnChatwootCloud: 'globalConfig/isOnChatwootCloud', + isMetaMessageSendingDisabled: 'globalConfig/isMetaMessageSendingDisabled', uiFlags: 'inboxes/getUIFlags', portals: 'portals/allPortals', }), @@ -356,11 +354,7 @@ export default { return this.isAnInstagramChannel && this.inbox.reauthorization_required; }, showInstagramRestrictionSettingsBanner() { - return ( - this.isOnChatwootCloud && - IS_META_INBOX_CREATION_DISABLED && - this.isAnInstagramChannel - ); + return this.isMetaMessageSendingDisabled && this.isAnInstagramChannel; }, metaRestrictionStatusUrl() { return META_RESTRICTION_STATUS_URL; diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue index 68487ed0e..171bf42c6 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Facebook.vue @@ -13,10 +13,7 @@ import { useBranding } from 'shared/composables/useBranding'; import { useAccount } from 'dashboard/composables/useAccount'; import NextButton from 'dashboard/components-next/button/Button.vue'; import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue'; -import { - IS_META_INBOX_CREATION_DISABLED, - META_RESTRICTION_STATUS_URL, -} from 'dashboard/constants/globals'; +import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals'; import * as Sentry from '@sentry/vue'; @@ -31,11 +28,11 @@ export default { }, setup() { const { replaceInstallationName } = useBranding(); - const { isOnChatwootCloud } = useAccount(); + const { isMetaInboxCreationDisabled } = useAccount(); const { preloadSdk, loginAndFetchPages } = useFacebookPageConnect(); return { replaceInstallationName, - isOnChatwootCloud, + isMetaInboxCreationDisabled, preloadSdk, loginAndFetchPages, META_RESTRICTION_STATUS_URL, @@ -83,7 +80,7 @@ export default { })); }, isFacebookConnectionDisabled() { - return this.isOnChatwootCloud && IS_META_INBOX_CREATION_DISABLED; + return this.isMetaInboxCreationDisabled; }, }, diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Instagram.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Instagram.vue index bd33acbad..3f1306534 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Instagram.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Instagram.vue @@ -6,20 +6,17 @@ import Button from 'dashboard/components-next/button/Button.vue'; import Banner from 'dashboard/components-next/banner/Banner.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import { useAccount } from 'dashboard/composables/useAccount'; -import { - IS_META_INBOX_CREATION_DISABLED, - META_RESTRICTION_STATUS_URL, -} from 'dashboard/constants/globals'; +import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals'; const { t } = useI18n(); -const { isOnChatwootCloud } = useAccount(); +const { isMetaInboxCreationDisabled } = useAccount(); const hasError = ref(false); const errorStateMessage = ref(''); const errorStateDescription = ref(''); const isRequestingAuthorization = ref(false); const isInstagramConnectionDisabled = computed( - () => isOnChatwootCloud.value && IS_META_INBOX_CREATION_DISABLED + () => isMetaInboxCreationDisabled.value ); onMounted(() => { 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 9480069b3..42c64aed9 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Whatsapp.vue @@ -11,15 +11,16 @@ import Banner from 'dashboard/components-next/banner/Banner.vue'; import Icon from 'dashboard/components-next/icon/Icon.vue'; import { useAccount } from 'dashboard/composables/useAccount'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; -import { - IS_META_INBOX_CREATION_DISABLED, - META_RESTRICTION_STATUS_URL, -} from 'dashboard/constants/globals'; +import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals'; const route = useRoute(); const router = useRouter(); const { t } = useI18n(); -const { isCloudFeatureEnabled, isOnChatwootCloud } = useAccount(); +const { + isCloudFeatureEnabled, + isOnChatwootCloud, + isMetaInboxCreationDisabled, +} = useAccount(); const PROVIDER_TYPES = { WHATSAPP: 'whatsapp', @@ -43,7 +44,7 @@ const showProviderSelection = computed(() => !selectedProvider.value); const showConfiguration = computed(() => Boolean(selectedProvider.value)); const isWhatsappEmbeddedSignupDisabled = computed( - () => isOnChatwootCloud.value && IS_META_INBOX_CREATION_DISABLED + () => isMetaInboxCreationDisabled.value ); const shouldShowWhatsappEmbeddedSignup = computed(() => { diff --git a/app/javascript/shared/store/globalConfig.js b/app/javascript/shared/store/globalConfig.js index c1786238b..c0030fe48 100644 --- a/app/javascript/shared/store/globalConfig.js +++ b/app/javascript/shared/store/globalConfig.js @@ -23,6 +23,8 @@ const { TERMS_URL: termsURL, WIDGET_BRAND_URL: widgetBrandURL, DISABLE_USER_PROFILE_UPDATE: disableUserProfileUpdate, + DISABLE_META_INBOX_CREATION: disableMetaInboxCreation, + DISABLE_META_MESSAGE_SENDING: disableMetaMessageSending, DEPLOYMENT_ENV: deploymentEnv, ACTIVE_PLATFORM_BANNERS: activePlatformBanners, } = window.globalConfig || {}; @@ -38,6 +40,8 @@ const state = { createNewAccountFromDashboard, directUploadsEnabled: parseBoolean(directUploadsEnabled), disableUserProfileUpdate: parseBoolean(disableUserProfileUpdate), + disableMetaInboxCreation: parseBoolean(disableMetaInboxCreation), + disableMetaMessageSending: parseBoolean(disableMetaMessageSending), displayManifest, gitSha, maximumFileUploadSize: resolveMaximumFileUploadSize(maximumFileUploadSize), @@ -56,6 +60,10 @@ const state = { export const getters = { get: $state => $state, isOnChatwootCloud: $state => $state.deploymentEnv === 'cloud', + isMetaInboxCreationDisabled: $state => + $state.deploymentEnv === 'cloud' && $state.disableMetaInboxCreation, + isMetaMessageSendingDisabled: $state => + $state.deploymentEnv === 'cloud' && $state.disableMetaMessageSending, isACustomBrandedInstance: $state => $state.installationName !== 'Chatwoot', isAChatwootInstance: $state => $state.installationName === 'Chatwoot', }; diff --git a/config/installation_config.yml b/config/installation_config.yml index cfe64767f..b93e6ed91 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -237,6 +237,18 @@ # ------- End of Context.dev Config ------- # # ------- Chatwoot Internal Config for Cloud ----# +- name: DISABLE_META_INBOX_CREATION + value: true + display_title: 'Disable Meta Inbox Creation' + description: 'Disable Facebook, Instagram, and WhatsApp Embedded Signup inbox creation on Chatwoot Cloud during Meta incidents' + locked: false + type: boolean +- name: DISABLE_META_MESSAGE_SENDING + value: true + display_title: 'Disable Meta Message Sending' + description: 'Disable Instagram message sending on Chatwoot Cloud during Meta incidents' + locked: false + type: boolean - name: CHATWOOT_INBOX_TOKEN value: display_title: 'Inbox Token'