fix(meta): show restriction alerts for inbox setup (#14974)

Instagram inbox creation and WhatsApp embedded signup on Chatwoot Cloud
now reflect the temporary Meta restriction. Instagram is hidden from
onboarding on Cloud, while the regular Instagram inbox creation page
shows a disabled action with a status-linked amber warning. WhatsApp
embedded signup on Cloud stays visible with its connect action disabled.
WhatsApp Call setup always uses the manual WhatsApp form.

Existing Instagram conversations and Instagram inbox settings on Cloud
also show amber warning banners with the public incident link.
Self-hosted installations keep their existing Instagram, WhatsApp, and
WhatsApp Call setup behavior because the temporary restriction is based
only on the Chatwoot Cloud environment check.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Muhsin Keloth
2026-07-10 16:22:49 +04:00
committed by GitHub
co-authored by Muhsin Sivin Varghese
parent 03a1b1dbc1
commit 98154bbeab
14 changed files with 196 additions and 48 deletions
@@ -1,7 +1,6 @@
<script setup>
import { computed } from 'vue';
import ChannelSelector from '../ChannelSelector.vue';
import { IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals';
const props = defineProps({
channel: {
@@ -21,10 +20,7 @@ const hasFbConfigured = computed(() => {
});
const hasInstagramConfigured = computed(() => {
return (
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
window.chatwootConfig?.instagramAppId
);
return window.chatwootConfig?.instagramAppId;
});
const hasTiktokConfigured = computed(() => {
@@ -33,7 +33,9 @@ import {
// constants
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals';
import wootConstants, {
META_RESTRICTION_STATUS_URL,
} from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
@@ -93,6 +95,7 @@ export default {
currentUserId: 'getCurrentUserID',
listLoadingStatus: 'getAllMessagesLoaded',
currentAccountId: 'getCurrentAccountId',
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
}),
isOpen() {
return this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN;
@@ -170,6 +173,12 @@ export default {
instagramInbox
);
},
isInstagramRestrictionBannerVisible() {
return this.isOnChatwootCloud && this.isAnInstagramChannel;
},
instagramRestrictionStatusUrl() {
return META_RESTRICTION_STATUS_URL;
},
replyWindowBannerMessage() {
if (this.isAWhatsAppChannel) {
@@ -455,7 +464,15 @@ export default {
>
<div ref="topBannerRef">
<Banner
v-if="!currentChat.can_reply"
v-if="isInstagramRestrictionBannerVisible"
color-scheme="warning"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="$t('CONVERSATION.INSTAGRAM_RESTRICTION_BANNER')"
:href-link="instagramRestrictionStatusUrl"
:href-link-text="$t('CONVERSATION.INSTAGRAM_RESTRICTION_STATUS_LINK')"
/>
<Banner
v-else-if="!currentChat.can_reply"
color-scheme="alert"
class="mx-2 mt-2 overflow-hidden rounded-lg"
:banner-message="replyWindowBannerMessage"
@@ -78,8 +78,5 @@ export default {
},
};
export const DEFAULT_REDIRECT_URL = '/app/';
// Temporarily disables Instagram and WhatsApp inbox creation
// (WhatsApp embedded signup popup, Instagram OAuth, WhatsApp Call).
// Flip to false when the channels are brought back.
export const IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED = true;
export const META_RESTRICTION_STATUS_URL =
'https://status.chatwoot.com/incident/948346';
@@ -44,6 +44,8 @@
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"OLD_INSTAGRAM_INBOX_REPLY_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. All new messages will show up there. You wont be able to send messages from this conversation anymore.",
"INSTAGRAM_RESTRICTION_BANNER": "Instagram is currently restricted. Some messages or actions may be delayed or unavailable while we restore full support.",
"INSTAGRAM_RESTRICTION_STATUS_LINK": "View status update",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -58,7 +58,10 @@
"ERROR_MESSAGE": "There was an error connecting to Instagram, please try again",
"ERROR_AUTH": "There was an error connecting to Instagram, please try again",
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You wont be able to send/receive Instagram messages from this inbox anymore."
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You wont be able to send/receive Instagram messages from this inbox anymore.",
"RESTRICTED_WARNING": "Instagram inbox creation is temporarily unavailable due to current Instagram platform restrictions. Well restore support as soon as possible.",
"SETTINGS_RESTRICTED_WARNING": "Instagram is currently restricted. Some messages or actions may be delayed or unavailable while we restore full support.",
"STATUS_LINK": "View status update"
},
"TIKTOK": {
"CONTINUE_WITH_TIKTOK": "Continue with TikTok",
@@ -320,6 +323,8 @@
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured",
"MANUAL_FALLBACK": "If your number is already connected to the WhatsApp Business Platform (API), or if youre a tech provider onboarding your own number, please use the {link} flow",
"MANUAL_LINK_TEXT": "manual setup flow",
"RESTRICTED_WARNING": "WhatsApp embedded signup is temporarily unavailable due to current Meta platform restrictions. Well restore support as soon as possible.",
"STATUS_LINK": "View status update",
"CALLING_ENABLE_FAILED": "Your WhatsApp inbox is ready, but voice calling couldn't be turned on — this number isn't enrolled in the WhatsApp Business Calling API yet. Reach out to Meta or your WhatsApp Business Solution Provider to onboard it, then turn calling on from the inbox's Calls settings."
},
"API": {
@@ -1,5 +1,4 @@
import { useMapGetter } from 'dashboard/composables/store';
import { IS_INSTAGRAM_WHATSAPP_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
@@ -8,20 +7,20 @@ import { IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constan
// Mirrors the availability checks in ChannelItem.vue.
export function useChannelConfig() {
const globalConfig = useMapGetter('globalConfig/get');
const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud');
const installationConfig = window.chatwootConfig || {};
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: () =>
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
!isOnChatwootCloud.value &&
Boolean(installationConfig.whatsappAppId) &&
installationConfig.whatsappAppId !== 'none' &&
Boolean(installationConfig.whatsappConfigurationId),
facebook: () => Boolean(installationConfig.fbAppId),
instagram: () =>
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
Boolean(installationConfig.instagramAppId),
!isOnChatwootCloud.value && Boolean(installationConfig.instagramAppId),
tiktok: () => Boolean(installationConfig.tiktokAppId),
gmail: () => Boolean(installationConfig.googleOAuthClientId),
outlook: () => Boolean(globalConfig.value.azureAppId),
@@ -1,6 +1,7 @@
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import { useStore } from 'dashboard/composables/store';
import { useAccount } from 'dashboard/composables/useAccount';
import { useWhatsappEmbeddedSignup } from 'dashboard/composables/useWhatsappEmbeddedSignup';
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import googleClient from 'dashboard/api/channel/googleClient';
@@ -23,11 +24,17 @@ export function useChannelConnect() {
const { t } = useI18n();
const store = useStore();
const { runEmbeddedSignup } = useWhatsappEmbeddedSignup();
const { isOnChatwootCloud } = useAccount();
const connectViaOAuth = async provider => {
const client = OAUTH_CLIENTS[provider];
if (!client) return;
if (provider === 'instagram' && isOnChatwootCloud.value) {
useAlert(t('INBOX_MGMT.ADD.INSTAGRAM.RESTRICTED_WARNING'));
return;
}
try {
const {
data: { url },
@@ -6,21 +6,25 @@ import { useDetectedChannels } from '../../inbox-setup/useDetectedChannels';
vi.mock('vue-router');
// Neutralize the temporary Instagram/WhatsApp kill switch so these specs keep
// covering the credential-based gating it currently short-circuits.
vi.mock('dashboard/constants/globals', async importOriginal => ({
...(await importOriginal()),
IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED: false,
}));
// Mounts the composable against a real store and the real useAccount (only
// useRoute and the underlying getters are faked), so a change to how useAccount
// resolves the current account is exercised here too. The real ./constants are
// used, so assertions validate against the actual channel identity (label keys,
// channel_type, social ordering) derived from CHANNEL_LIST.
const mountComposable = ({ brandInfo, inboxes = [] } = {}) => {
const mountComposable = ({
brandInfo,
inboxes = [],
isOnChatwootCloud = false,
} = {}) => {
const store = createStore({
modules: {
globalConfig: {
namespaced: true,
getters: {
get: () => ({}),
isOnChatwootCloud: () => isOnChatwootCloud,
},
},
accounts: {
namespaced: true,
getters: {
@@ -202,6 +206,22 @@ describe('useDetectedChannels', () => {
'line',
]);
});
it('hides Instagram from onboarding on Chatwoot Cloud', () => {
const { displayedChannels } = mountComposable({
isOnChatwootCloud: true,
brandInfo: {
socials: [
{ type: 'instagram', url: 'https://instagram.com/acme' },
{ type: 'tiktok', url: 'https://tiktok.com/@acme' },
],
},
});
expect(displayedChannels.value.map(channel => channel.type)).toEqual([
'tiktok',
]);
});
});
describe('remainingChannels', () => {
@@ -4,6 +4,8 @@ import { shouldBeUrl } from 'shared/helpers/Validators';
import { useAlert } from 'dashboard/composables';
import { useVuelidate } from '@vuelidate/core';
import Avatar from 'next/avatar/Avatar.vue';
import Banner from 'dashboard/components-next/banner/Banner.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner.vue';
import SettingsToggleSection from 'dashboard/components-next/Settings/SettingsToggleSection.vue';
import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFieldSection.vue';
@@ -42,9 +44,11 @@ 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 { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals';
export default {
components: {
Banner,
BotConfiguration,
CollaboratorsPage,
ConfigurationPage,
@@ -76,6 +80,7 @@ export default {
AccountHealth,
Widget,
AccessToken,
Icon,
},
mixins: [inboxMixin],
setup() {
@@ -338,6 +343,12 @@ export default {
instagramUnauthorized() {
return this.isAnInstagramChannel && this.inbox.reauthorization_required;
},
showInstagramRestrictionSettingsBanner() {
return this.isOnChatwootCloud && this.isAnInstagramChannel;
},
metaRestrictionStatusUrl() {
return META_RESTRICTION_STATUS_URL;
},
tiktokUnauthorized() {
return this.isATiktokChannel && this.inbox.reauthorization_required;
},
@@ -737,6 +748,29 @@ export default {
class="mx-6 mb-4"
:class="bannerMaxWidth"
/>
<Banner
v-if="showInstagramRestrictionSettingsBanner"
color="amber"
class="mx-6 mb-4 max-w-4xl"
>
<div class="flex items-start gap-3 text-start">
<Icon
icon="i-lucide-triangle-alert"
class="flex-shrink-0 size-4 mt-0.5"
/>
<span>
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.SETTINGS_RESTRICTED_WARNING') }}
<a
:href="metaRestrictionStatusUrl"
class="link underline"
rel="noopener noreferrer nofollow"
target="_blank"
>
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.STATUS_LINK') }}
</a>
</span>
</div>
</Banner>
<div
v-if="selectedTabKey === 'inbox-settings'"
@@ -180,6 +180,7 @@ export default {
<div class="w-full mt-4">
<NextButton
:disabled="uiFlags.isCreating"
:is-loading="uiFlags.isCreating"
type="submit"
solid
@@ -1,15 +1,23 @@
<script setup>
import { ref, onMounted } from 'vue';
import { computed, ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import instagramClient from 'dashboard/api/channel/instagramClient';
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 { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals';
const { t } = useI18n();
const { isOnChatwootCloud } = useAccount();
const hasError = ref(false);
const errorStateMessage = ref('');
const errorStateDescription = ref('');
const isRequestingAuthorization = ref(false);
const isInstagramConnectionRestricted = computed(() => {
return isOnChatwootCloud.value;
});
onMounted(() => {
const urlParams = new URLSearchParams(window.location.search);
@@ -56,7 +64,7 @@ const requestAuthorization = async () => {
</div>
<div
v-else
class="flex flex-col items-center justify-center px-8 py-10 text-center rounded-2xl outline outline-1 outline-n-weak"
class="flex flex-col items-center justify-center w-full px-8 py-10 text-center rounded-2xl outline outline-1 outline-n-weak"
>
<h6 class="text-2xl font-medium">
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.CONNECT_YOUR_INSTAGRAM_PROFILE') }}
@@ -68,11 +76,36 @@ const requestAuthorization = async () => {
class="text-white !rounded-full !px-6 bg-gradient-to-r from-[#833AB4] via-[#FD1D1D] to-[#FCAF45]"
lg
icon="i-ri-instagram-line"
:disabled="isRequestingAuthorization"
:disabled="
isRequestingAuthorization || isInstagramConnectionRestricted
"
:is-loading="isRequestingAuthorization"
:label="$t('INBOX_MGMT.ADD.INSTAGRAM.CONTINUE_WITH_INSTAGRAM')"
@click="requestAuthorization()"
/>
<Banner
v-if="isInstagramConnectionRestricted"
color="amber"
class="w-full max-w-2xl mt-6"
>
<div class="flex items-start gap-3 text-left">
<Icon
icon="i-lucide-triangle-alert"
class="flex-shrink-0 size-4 mt-0.5"
/>
<span>
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.RESTRICTED_WARNING') }}
<a
:href="META_RESTRICTION_STATUS_URL"
class="link underline"
rel="noopener noreferrer nofollow"
target="_blank"
>
{{ $t('INBOX_MGMT.ADD.INSTAGRAM.STATUS_LINK') }}
</a>
</span>
</div>
</Banner>
</div>
</div>
</div>
@@ -7,11 +7,13 @@ import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp.vue';
import CloudWhatsapp from './CloudWhatsapp.vue';
import WhatsappEmbeddedSignup from './WhatsappEmbeddedSignup.vue';
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
import { IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals';
import { useAccount } from 'dashboard/composables/useAccount';
import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals';
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { isOnChatwootCloud } = useAccount();
const PROVIDER_TYPES = {
WHATSAPP: 'whatsapp',
@@ -22,9 +24,12 @@ const PROVIDER_TYPES = {
THREE_SIXTY_DIALOG: '360dialog',
};
const isWhatsappEmbeddedSignupRestricted = computed(() => {
return isOnChatwootCloud.value;
});
const hasWhatsappAppId = computed(() => {
return (
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
window.chatwootConfig?.whatsappAppId &&
window.chatwootConfig.whatsappAppId !== 'none'
);
@@ -103,7 +108,11 @@ const handleManualLinkClick = () => {
hasWhatsappAppId && selectedProvider === PROVIDER_TYPES.WHATSAPP
"
>
<WhatsappEmbeddedSignup />
<WhatsappEmbeddedSignup
:is-disabled="isWhatsappEmbeddedSignupRestricted"
:show-restriction-alert="isWhatsappEmbeddedSignupRestricted"
:restriction-status-url="META_RESTRICTION_STATUS_URL"
/>
<!-- Manual setup fallback option -->
<div class="pt-6 mt-6 border-t border-n-weak">
@@ -1,26 +1,11 @@
<script setup>
import { computed } from 'vue';
import WhatsappEmbeddedSignup from './WhatsappEmbeddedSignup.vue';
import CloudWhatsapp from './CloudWhatsapp.vue';
import { IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals';
const hasWhatsappAppId = computed(() => {
return (
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
window.chatwootConfig?.whatsappAppId &&
window.chatwootConfig.whatsappAppId !== 'none'
);
});
</script>
<template>
<div class="overflow-auto col-span-6 p-6 w-full h-full">
<div class="px-6 py-5 rounded-2xl border border-n-weak">
<WhatsappEmbeddedSignup
v-if="hasWhatsappAppId"
enable-calling-on-complete
/>
<CloudWhatsapp v-else enable-calling-on-complete />
<CloudWhatsapp enable-calling-on-complete />
</div>
</div>
</template>
@@ -7,6 +7,7 @@ import { useAlert } from 'dashboard/composables';
import { useWhatsappEmbeddedSignup } from 'dashboard/composables/useWhatsappEmbeddedSignup';
import Icon from 'next/icon/Icon.vue';
import NextButton from 'next/button/Button.vue';
import Banner from 'next/banner/Banner.vue';
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
import InboxesAPI from 'dashboard/api/inboxes';
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
@@ -17,6 +18,22 @@ const props = defineProps({
type: Boolean,
default: false,
},
isDisabled: {
type: Boolean,
default: false,
},
showRestrictionAlert: {
type: Boolean,
default: false,
},
restrictionStatusUrl: {
type: String,
default: '',
},
restrictionWarningText: {
type: String,
default: '',
},
});
const store = useStore();
@@ -81,6 +98,8 @@ const handleSignupSuccess = async inboxData => {
};
const launchEmbeddedSignup = async () => {
if (props.isDisabled) return;
let credentials;
try {
credentials = await runEmbeddedSignup();
@@ -174,9 +193,33 @@ const launchEmbeddedSignup = async () => {
</I18nT>
</div>
<Banner v-if="showRestrictionAlert" color="amber" class="w-full mb-6">
<div class="flex items-start gap-3 text-left">
<Icon
icon="i-lucide-triangle-alert"
class="flex-shrink-0 size-4 mt-0.5"
/>
<span>
{{
restrictionWarningText ||
$t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.RESTRICTED_WARNING')
}}
<a
v-if="restrictionStatusUrl"
:href="restrictionStatusUrl"
class="link underline"
rel="noopener noreferrer nofollow"
target="_blank"
>
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.STATUS_LINK') }}
</a>
</span>
</div>
</Banner>
<div class="flex mt-4">
<NextButton
:disabled="isAuthenticating"
:disabled="isAuthenticating || isDisabled"
:is-loading="isAuthenticating"
faded
slate