feat(whatsapp): gate embedded signup inbox creation (#15046)

WhatsApp inbox creation now shows Embedded Signup for Chatwoot Cloud
accounts only when the new `whatsapp_embedded_signup_inbox_creation`
feature flag is enabled. Cloud accounts without the flag go directly to
manual WhatsApp Cloud API setup, while self-hosted installations with a
configured WhatsApp App ID retain their existing Embedded Signup flow.

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
This commit is contained in:
Muhsin Keloth
2026-07-17 14:09:36 +04:00
committed by GitHub
co-authored by Muhsin
parent c420edce58
commit 90861f8809
8 changed files with 37 additions and 68 deletions
+2
View File
@@ -7,6 +7,8 @@ export const FEATURE_FLAGS = {
AUTOMATIONS: 'automations',
CAMPAIGNS: 'campaigns',
WHATSAPP_CAMPAIGNS: 'whatsapp_campaign',
WHATSAPP_EMBEDDED_SIGNUP_INBOX_CREATION:
'whatsapp_embedded_signup_inbox_creation',
WHATSAPP_MANUAL_TRANSFER: 'whatsapp_manual_transfer',
WHATSAPP_RECONFIGURE: 'whatsapp_reconfigure',
CANNED_RESPONSES: 'canned_responses',
@@ -322,8 +322,6 @@
"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": {
@@ -17,7 +17,10 @@ export function useChannelConfig() {
// WhatsApp is onboarded only via Meta embedded signup, which needs both the
// app id (not the 'none' sentinel) and the signup configuration id.
whatsapp: () =>
!isOnChatwootCloud.value &&
(!isOnChatwootCloud.value ||
isCloudFeatureEnabled(
FEATURE_FLAGS.WHATSAPP_EMBEDDED_SIGNUP_INBOX_CREATION
)) &&
Boolean(installationConfig.whatsappAppId) &&
installationConfig.whatsappAppId !== 'none' &&
Boolean(installationConfig.whatsappConfigurationId),
@@ -7,7 +7,7 @@ vi.mock('dashboard/composables/store', () => ({
useMapGetter: () => ({ value: {} }),
}));
vi.mock('dashboard/composables/useAccount', () => ({
useAccount: () => ({ isCloudFeatureEnabled: () => false }),
useAccount: () => ({ isCloudFeatureEnabled: () => true }),
}));
vi.mock('../../inbox-setup/useChannelConnect', () => ({
useChannelConnect: () => ({
@@ -8,12 +8,12 @@ import CloudWhatsapp from './CloudWhatsapp.vue';
import WhatsappEmbeddedSignup from './WhatsappEmbeddedSignup.vue';
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
import { useAccount } from 'dashboard/composables/useAccount';
import { META_RESTRICTION_STATUS_URL } from 'dashboard/constants/globals';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { isOnChatwootCloud } = useAccount();
const { isCloudFeatureEnabled, isOnChatwootCloud } = useAccount();
const PROVIDER_TYPES = {
WHATSAPP: 'whatsapp',
@@ -24,10 +24,6 @@ const PROVIDER_TYPES = {
THREE_SIXTY_DIALOG: '360dialog',
};
const isWhatsappEmbeddedSignupRestricted = computed(() => {
return isOnChatwootCloud.value;
});
const hasWhatsappAppId = computed(() => {
return (
window.chatwootConfig?.whatsappAppId &&
@@ -41,6 +37,17 @@ const showProviderSelection = computed(() => !selectedProvider.value);
const showConfiguration = computed(() => Boolean(selectedProvider.value));
const shouldShowWhatsappEmbeddedSignup = computed(() => {
return (
selectedProvider.value === PROVIDER_TYPES.WHATSAPP &&
hasWhatsappAppId.value &&
(!isOnChatwootCloud.value ||
isCloudFeatureEnabled(
FEATURE_FLAGS.WHATSAPP_EMBEDDED_SIGNUP_INBOX_CREATION
))
);
});
const availableProviders = computed(() => [
{
key: PROVIDER_TYPES.WHATSAPP,
@@ -67,7 +74,8 @@ const selectProvider = providerValue => {
const shouldShowCloudWhatsapp = provider => {
return (
provider === PROVIDER_TYPES.WHATSAPP_MANUAL ||
(provider === PROVIDER_TYPES.WHATSAPP && !hasWhatsappAppId.value)
(provider === PROVIDER_TYPES.WHATSAPP &&
!shouldShowWhatsappEmbeddedSignup.value)
);
};
@@ -102,17 +110,8 @@ const handleManualLinkClick = () => {
<div v-else-if="showConfiguration">
<div class="px-6 py-5 rounded-2xl border border-n-weak">
<!-- Show embedded signup if app ID is configured -->
<div
v-if="
hasWhatsappAppId && selectedProvider === PROVIDER_TYPES.WHATSAPP
"
>
<WhatsappEmbeddedSignup
:is-disabled="isWhatsappEmbeddedSignupRestricted"
:show-restriction-alert="isWhatsappEmbeddedSignupRestricted"
:restriction-status-url="META_RESTRICTION_STATUS_URL"
/>
<div v-if="shouldShowWhatsappEmbeddedSignup">
<WhatsappEmbeddedSignup />
<!-- Manual setup fallback option -->
<div class="pt-6 mt-6 border-t border-n-weak">
@@ -7,7 +7,6 @@ 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';
@@ -18,22 +17,6 @@ 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();
@@ -98,8 +81,6 @@ const handleSignupSuccess = async inboxData => {
};
const launchEmbeddedSignup = async () => {
if (props.isDisabled) return;
let credentials;
try {
credentials = await runEmbeddedSignup();
@@ -193,33 +174,9 @@ 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 || isDisabled"
:disabled="isAuthenticating"
:is-loading="isAuthenticating"
faded
slate
+4
View File
@@ -264,3 +264,7 @@
display_name: WhatsApp Reconfigure
enabled: false
column: feature_flags_ext_1
- name: whatsapp_embedded_signup_inbox_creation
display_name: WhatsApp Embedded Signup Inbox Creation
enabled: false
column: feature_flags_ext_1
+8 -2
View File
@@ -117,10 +117,16 @@ RSpec.describe Account do
it 'configures the account feature flag extension column' do
expect(described_class.flag_columns).to include('feature_flags', 'feature_flags_ext_1')
expect(described_class.flag_mapping['feature_flags_ext_1']).to eq(feature_whatsapp_manual_transfer: 1, feature_data_import: 1 << 1,
feature_api_and_webhooks: 1 << 2, feature_whatsapp_reconfigure: 1 << 3)
expect(described_class.flag_mapping['feature_flags_ext_1']).to eq(
feature_whatsapp_manual_transfer: 1,
feature_data_import: 1 << 1,
feature_api_and_webhooks: 1 << 2,
feature_whatsapp_reconfigure: 1 << 3,
feature_whatsapp_embedded_signup_inbox_creation: 1 << 4
)
expect(described_class.flag_mapping['feature_flags_ext_1'][:feature_whatsapp_manual_transfer]).to eq(1)
expect(described_class.flag_mapping['feature_flags_ext_1'][:feature_data_import]).to eq(2)
expect(described_class.flag_mapping['feature_flags_ext_1'][:feature_whatsapp_embedded_signup_inbox_creation]).to eq(16)
end
it 'keeps existing feature flags on the original column' do