add feature flag for wa es

This commit is contained in:
Tanmay Deep Sharma
2025-07-11 18:07:44 +07:00
parent ac83c28466
commit fcc13a5064
9 changed files with 465 additions and 10 deletions
@@ -1,4 +1,6 @@
class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController
before_action :validate_feature_enabled!
# POST /api/v1/accounts/:account_id/whatsapp/authorization
# Handles the embedded signup callback data from the Facebook SDK
def create
@@ -40,6 +42,15 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts:
}, status: :unprocessable_entity
end
def validate_feature_enabled!
return if Current.account.feature_whatsapp_embedded_signup?
render json: {
success: false,
error: 'WhatsApp embedded signup is not enabled for this account'
}, status: :forbidden
end
def validate_embedded_signup_params!
missing_params = []
missing_params << 'code' if params[:code].blank?
@@ -2,6 +2,7 @@
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStore } from 'vuex';
import Twilio from './Twilio.vue';
import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp.vue';
import CloudWhatsapp from './CloudWhatsapp.vue';
@@ -12,6 +13,7 @@ import twilioIcon from 'dashboard/assets/images/twilio.png';
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const store = useStore();
const PROVIDER_TYPES = {
WHATSAPP: 'whatsapp',
@@ -28,6 +30,14 @@ const hasWhatsappAppId = computed(() => {
);
});
const isWhatsappEmbeddedSignupEnabled = computed(() => {
const accountId = route.params.accountId;
return store.getters['accounts/isFeatureEnabledonAccount'](
accountId,
'whatsapp_embedded_signup'
);
});
const selectedProvider = computed(() => route.query.provider);
const showProviderSelection = computed(() => !selectedProvider.value);
@@ -58,6 +68,11 @@ const selectProvider = providerValue => {
};
const shouldShowEmbeddedSignup = provider => {
// Check if the feature is enabled for the account
if (!isWhatsappEmbeddedSignupEnabled.value) {
return false;
}
return (
(provider === PROVIDER_TYPES.WHATSAPP && hasWhatsappAppId.value) ||
provider === PROVIDER_TYPES.WHATSAPP_EMBEDDED
@@ -65,7 +80,15 @@ const shouldShowEmbeddedSignup = provider => {
};
const shouldShowCloudWhatsapp = provider => {
return provider === PROVIDER_TYPES.WHATSAPP && !hasWhatsappAppId.value;
// If embedded signup feature is enabled and app ID is configured, don't show cloud whatsapp
if (isWhatsappEmbeddedSignupEnabled.value && hasWhatsappAppId.value) {
return false;
}
// Show cloud whatsapp when:
// 1. Provider is whatsapp AND
// 2. Either no app ID is configured OR embedded signup feature is disabled
return provider === PROVIDER_TYPES.WHATSAPP;
};
</script>
@@ -10,8 +10,6 @@ class Whatsapp::EmbeddedSignupService
def perform
validate_parameters!
GlobalConfig.clear_cache
# Exchange code for user access token
access_token = Whatsapp::TokenExchangeService.new(@code).perform
+3
View File
@@ -176,3 +176,6 @@
- name: notion_integration
display_name: Notion Integration
enabled: false
- name: whatsapp_embedded_signup
display_name: WhatsApp Embedded Signup
enabled: false
@@ -0,0 +1,279 @@
require 'rails_helper'
RSpec.describe Api::V1::Accounts::Whatsapp::AuthorizationsController, type: :controller do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
describe 'POST #create' do
context 'when user is not authenticated' do
it 'returns unauthorized' do
post :create, params: { account_id: account.id }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when user is authenticated' do
before { sign_in(agent) }
context 'when feature is not enabled' do
before do
account.disable_features!(:whatsapp_embedded_signup)
end
it 'returns forbidden' do
post :create, params: {
account_id: account.id,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
}
expect(response).to have_http_status(:forbidden)
expect(response.parsed_body['error']).to eq('WhatsApp embedded signup is not enabled for this account')
end
end
context 'when feature is enabled' do
before do
account.enable_features!(:whatsapp_embedded_signup)
end
it 'returns unprocessable entity when code is missing' do
post :create, params: {
account_id: account.id,
business_id: 'test_business_id',
waba_id: 'test_waba_id'
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to include('code')
end
it 'returns unprocessable entity when business_id is missing' do
post :create, params: {
account_id: account.id,
code: 'test_code',
waba_id: 'test_waba_id'
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to include('business_id')
end
it 'returns unprocessable entity when waba_id is missing' do
post :create, params: {
account_id: account.id,
code: 'test_code',
business_id: 'test_business_id'
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to include('waba_id')
end
it 'creates whatsapp channel successfully' do
whatsapp_channel = create(:channel_whatsapp, account: account)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
allow(whatsapp_channel).to receive(:inbox).and_return(inbox)
# Stub webhook setup service to prevent HTTP calls
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
post :create, params: {
account_id: account.id,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id',
phone_number_id: 'test_phone_id'
}
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['success']).to be true
expect(response_data['id']).to eq(inbox.id)
expect(response_data['name']).to eq(inbox.name)
expect(response_data['channel_type']).to eq('whatsapp')
end
it 'calls the embedded signup service with correct parameters' do
whatsapp_channel = create(:channel_whatsapp, account: account)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
expect(Whatsapp::EmbeddedSignupService).to receive(:new).with(
account: account,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id',
phone_number_id: 'test_phone_id'
).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
allow(whatsapp_channel).to receive(:inbox).and_return(inbox)
# Stub webhook setup service
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
post :create, params: {
account_id: account.id,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id',
phone_number_id: 'test_phone_id'
}
end
it 'accepts phone_number_id as optional parameter' do
whatsapp_channel = create(:channel_whatsapp, account: account)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
expect(Whatsapp::EmbeddedSignupService).to receive(:new).with(
account: account,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id',
phone_number_id: nil
).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
allow(whatsapp_channel).to receive(:inbox).and_return(inbox)
# Stub webhook setup service
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
post :create, params: {
account_id: account.id,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
}
expect(response).to have_http_status(:success)
end
it 'returns unprocessable entity when service fails' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error')
post :create, params: {
account_id: account.id,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
}
expect(response).to have_http_status(:unprocessable_entity)
response_data = response.parsed_body
expect(response_data['success']).to be false
expect(response_data['error']).to eq('Service error')
end
it 'logs error when service fails' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error')
expect(Rails.logger).to receive(:error).with(/\[WHATSAPP AUTHORIZATION\] Embedded signup error: Service error/)
expect(Rails.logger).to receive(:error).with(/authorizations_controller/)
post :create, params: {
account_id: account.id,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
}
end
it 'handles token exchange errors' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new)
.and_raise(StandardError, 'Invalid authorization code')
post :create, params: {
account_id: account.id,
code: 'invalid_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Invalid authorization code')
end
it 'handles channel already exists error' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new)
.and_raise(StandardError, 'Channel already exists')
post :create, params: {
account_id: account.id,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
}
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Channel already exists')
end
end
context 'when user is not authorized for the account' do
let(:other_account) { create(:account) }
before do
account.enable_features!(:whatsapp_embedded_signup)
end
it 'returns unauthorized' do
post :create, params: {
account_id: other_account.id,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
}
expect(response).to have_http_status(:unauthorized)
end
end
context 'when user is an administrator' do
let(:administrator) { create(:user, account: account, role: :administrator) }
before do
sign_in(administrator)
account.enable_features!(:whatsapp_embedded_signup)
end
it 'allows channel creation' do
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
whatsapp_channel = create(:channel_whatsapp, account: account)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_return(embedded_signup_service)
allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
allow(whatsapp_channel).to receive(:inbox).and_return(inbox)
# Stub webhook setup service
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
post :create, params: {
account_id: account.id,
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
}
expect(response).to have_http_status(:success)
end
end
end
end
end
+81
View File
@@ -61,4 +61,85 @@ RSpec.describe Channel::Whatsapp do
expect(channel.provider_config['webhook_verify_token']).to eq '123'
end
end
describe 'webhook setup after creation' do
let(:account) { create(:account) }
let(:webhook_service) { instance_double(Whatsapp::WebhookSetupService) }
before do
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
end
context 'when channel is created through embedded signup' do
it 'sets up webhooks automatically' do
expect(Whatsapp::WebhookSetupService).to receive(:new).with(
anything,
'test_waba_id',
'test_access_token'
)
expect(webhook_service).to receive(:perform)
create(:channel_whatsapp,
account: account,
provider: 'whatsapp_cloud',
provider_config: {
'source' => 'embedded_signup',
'business_account_id' => 'test_waba_id',
'api_key' => 'test_access_token'
},
validate_provider_config: false,
sync_templates: false)
end
it 'does not raise error if webhook setup fails' do
allow(webhook_service).to receive(:perform).and_raise(StandardError, 'Webhook error')
expect do
create(:channel_whatsapp,
account: account,
provider: 'whatsapp_cloud',
provider_config: {
'source' => 'embedded_signup',
'business_account_id' => 'test_waba_id',
'api_key' => 'test_access_token'
},
validate_provider_config: false,
sync_templates: false)
end.not_to raise_error
end
end
context 'when channel is created through manual setup' do
it 'does not setup webhooks' do
expect(Whatsapp::WebhookSetupService).not_to receive(:new)
create(:channel_whatsapp,
account: account,
provider: 'whatsapp_cloud',
provider_config: {
'business_account_id' => 'test_waba_id',
'api_key' => 'test_access_token'
},
validate_provider_config: false,
sync_templates: false)
end
end
context 'when channel is created with different provider' do
it 'does not setup webhooks for 360dialog provider' do
expect(Whatsapp::WebhookSetupService).not_to receive(:new)
create(:channel_whatsapp,
account: account,
provider: 'default',
provider_config: {
'source' => 'embedded_signup',
'api_key' => 'test_360dialog_key'
},
validate_provider_config: false,
sync_templates: false)
end
end
end
end
+57
View File
@@ -0,0 +1,57 @@
require 'rails_helper'
RSpec.describe Featurable do
let(:account) { create(:account) }
describe 'WhatsApp embedded signup feature' do
it 'is disabled by default' do
expect(account.feature_whatsapp_embedded_signup?).to be false
expect(account.feature_enabled?('whatsapp_embedded_signup')).to be false
end
describe '#enable_features!' do
it 'enables the whatsapp embedded signup feature' do
account.enable_features!(:whatsapp_embedded_signup)
expect(account.feature_whatsapp_embedded_signup?).to be true
expect(account.feature_enabled?('whatsapp_embedded_signup')).to be true
end
it 'enables multiple features at once' do
account.enable_features!(:whatsapp_embedded_signup, :help_center)
expect(account.feature_whatsapp_embedded_signup?).to be true
expect(account.feature_help_center?).to be true
end
end
describe '#disable_features!' do
before do
account.enable_features!(:whatsapp_embedded_signup)
end
it 'disables the whatsapp embedded signup feature' do
expect(account.feature_whatsapp_embedded_signup?).to be true
account.disable_features!(:whatsapp_embedded_signup)
expect(account.feature_whatsapp_embedded_signup?).to be false
end
end
describe '#enabled_features' do
it 'includes whatsapp_embedded_signup when enabled' do
account.enable_features!(:whatsapp_embedded_signup)
expect(account.enabled_features).to include('whatsapp_embedded_signup' => true)
end
it 'does not include whatsapp_embedded_signup when disabled' do
account.disable_features!(:whatsapp_embedded_signup)
expect(account.enabled_features).not_to include('whatsapp_embedded_signup' => true)
end
end
describe '#all_features' do
it 'includes whatsapp_embedded_signup in all features list' do
expect(account.all_features).to have_key('whatsapp_embedded_signup')
end
end
end
end
@@ -19,6 +19,11 @@ describe Whatsapp::ChannelCreationService do
# Clean up any existing channels to avoid phone number conflicts
Channel::Whatsapp.destroy_all
# Stub the webhook setup service to prevent HTTP calls during tests
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
# Stub the provider validation and sync_templates
allow(Channel::Whatsapp).to receive(:new).and_wrap_original do |method, *args|
channel = method.call(*args)
@@ -111,4 +116,4 @@ describe Whatsapp::ChannelCreationService do
end
end
end
end
end
@@ -32,7 +32,6 @@ describe Whatsapp::EmbeddedSignupService do
let(:phone_info_service) { instance_double(Whatsapp::PhoneInfoService) }
let(:token_validation_service) { instance_double(Whatsapp::TokenValidationService) }
let(:channel_creation_service) { instance_double(Whatsapp::ChannelCreationService) }
let(:webhook_setup_service) { instance_double(Whatsapp::WebhookSetupService) }
before do
allow(GlobalConfig).to receive(:clear_cache)
@@ -53,9 +52,9 @@ describe Whatsapp::EmbeddedSignupService do
.and_return(channel_creation_service)
allow(channel_creation_service).to receive(:perform).and_return(channel)
allow(Whatsapp::WebhookSetupService).to receive(:new)
.with(channel, waba_id, access_token).and_return(webhook_setup_service)
allow(webhook_setup_service).to receive(:perform)
# Webhook setup is now handled in the channel after_create callback
# So we stub it at the channel level
allow(channel).to receive(:setup_webhooks)
end
it 'orchestrates all services in the correct order' do
@@ -64,7 +63,6 @@ describe Whatsapp::EmbeddedSignupService do
expect(phone_info_service).to receive(:perform).ordered
expect(token_validation_service).to receive(:perform).ordered
expect(channel_creation_service).to receive(:perform).ordered
expect(webhook_setup_service).to receive(:perform).ordered
result = service.perform
expect(result).to eq(channel)
@@ -125,4 +123,4 @@ describe Whatsapp::EmbeddedSignupService do
end
end
end
end
end