-
{{ inbox.name }}
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
index 9214a8234..cfac915bf 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
@@ -1,10 +1,10 @@
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils.js
new file mode 100644
index 000000000..be2721bc4
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils.js
@@ -0,0 +1,89 @@
+import { loadScript } from 'dashboard/helper/DOMHelpers';
+
+export const loadFacebookSdk = async () => {
+ return loadScript('https://connect.facebook.net/en_US/sdk.js', {
+ async: true,
+ defer: true,
+ crossOrigin: 'anonymous',
+ });
+};
+
+export const initializeFacebook = (appId, apiVersion) => {
+ const version = apiVersion || 'v22.0';
+ return new Promise(resolve => {
+ const init = () => {
+ window.FB.init({
+ appId,
+ autoLogAppEvents: true,
+ xfbml: true,
+ version,
+ });
+ resolve();
+ };
+
+ if (window.FB) {
+ init();
+ } else {
+ window.fbAsyncInit = init;
+ }
+ });
+};
+
+export const isValidBusinessData = businessData => {
+ return businessData && businessData.business_id && businessData.waba_id;
+};
+
+export const createMessageHandler = onEmbeddedSignupData => {
+ return event => {
+ if (!event.origin.endsWith('facebook.com')) return;
+
+ try {
+ let data;
+ if (typeof event.data === 'string') {
+ data = JSON.parse(event.data);
+ } else if (typeof event.data === 'object' && event.data !== null) {
+ data = event.data;
+ } else {
+ return;
+ }
+
+ if (data.type === 'WA_EMBEDDED_SIGNUP') {
+ onEmbeddedSignupData(data);
+ }
+ } catch {
+ // Ignore non-JSON or irrelevant messages
+ }
+ };
+};
+
+export const initWhatsAppEmbeddedSignup = configId => {
+ return new Promise((resolve, reject) => {
+ window.FB.login(
+ response => {
+ if (response.authResponse && response.authResponse.code) {
+ resolve(response.authResponse.code);
+ } else if (response.error) {
+ reject(new Error(response.error));
+ } else {
+ reject(new Error('Login cancelled'));
+ }
+ },
+ {
+ config_id: configId,
+ response_type: 'code',
+ override_default_response_type: true,
+ extras: {
+ setup: {},
+ featureType: '',
+ sessionInfoVersion: '3',
+ },
+ }
+ );
+ });
+};
+
+export const setupFacebookSdk = async (appId, apiVersion) => {
+ const version = apiVersion || 'v22.0';
+ await loadFacebookSdk();
+ await initializeFacebook(appId, version);
+};
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue
index 88af2e13f..71aea3f59 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue
@@ -7,6 +7,7 @@ import SmtpSettings from '../SmtpSettings.vue';
import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import NextButton from 'dashboard/components-next/button/Button.vue';
+import WhatsappReauthorize from '../channels/whatsapp/Reauthorize.vue';
export default {
components: {
@@ -14,6 +15,7 @@ export default {
ImapSettings,
SmtpSettings,
NextButton,
+ WhatsappReauthorize,
},
mixins: [inboxMixin],
props: {
@@ -29,12 +31,21 @@ export default {
return {
hmacMandatory: false,
whatsAppInboxAPIKey: '',
+ isRequestingReauthorization: false,
isSyncingTemplates: false,
};
},
validations: {
whatsAppInboxAPIKey: { required },
},
+ computed: {
+ isEmbeddedSignupWhatsApp() {
+ return this.inbox.provider_config?.source === 'embedded_signup';
+ },
+ whatsappAppId() {
+ return window.chatwootConfig?.whatsappAppId;
+ },
+ },
watch: {
inbox() {
this.setDefaults();
@@ -84,6 +95,11 @@ export default {
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
}
},
+ async handleReconfigure() {
+ if (this.$refs.whatsappReauth) {
+ await this.$refs.whatsappReauth.requestAuthorization();
+ }
+ },
async syncTemplates() {
this.isSyncingTemplates = true;
try {
@@ -210,45 +226,80 @@ export default {
-
-
-
-
-
-
-
-
+
+
-
-
+
+ {{
+ $t(
+ 'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION'
+ )
+ }}
+
+
+ {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_RECONFIGURE_BUTTON') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }}
-
-
-
+
+
+ {{
+ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON')
+ }}
+
+
+
+
diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb
index 8d054a586..7471cf807 100644
--- a/app/models/channel/whatsapp.rb
+++ b/app/models/channel/whatsapp.rb
@@ -32,8 +32,8 @@ class Channel::Whatsapp < ApplicationRecord
validates :phone_number, presence: true, uniqueness: true
validate :validate_provider_config
+ before_save :setup_webhooks
after_create :sync_templates
- after_create_commit :setup_webhooks
before_destroy :teardown_webhooks
def name
@@ -78,8 +78,12 @@ class Channel::Whatsapp < ApplicationRecord
handle_webhook_setup_error(e)
end
+ def provider_config_changed?
+ will_save_change_to_provider_config?
+ end
+
def should_setup_webhooks?
- whatsapp_cloud_provider? && embedded_signup_source? && webhook_config_present?
+ whatsapp_cloud_provider? && embedded_signup_source? && webhook_config_present? && provider_config_changed?
end
def whatsapp_cloud_provider?
diff --git a/app/services/whatsapp/embedded_signup_service.rb b/app/services/whatsapp/embedded_signup_service.rb
index 0adf073cd..e66506638 100644
--- a/app/services/whatsapp/embedded_signup_service.rb
+++ b/app/services/whatsapp/embedded_signup_service.rb
@@ -1,10 +1,11 @@
class Whatsapp::EmbeddedSignupService
- def initialize(account:, code:, business_id:, waba_id:, phone_number_id:)
+ def initialize(account:, params:, inbox_id: nil)
@account = account
- @code = code
- @business_id = business_id
- @waba_id = waba_id
- @phone_number_id = phone_number_id
+ @code = params[:code]
+ @business_id = params[:business_id]
+ @waba_id = params[:waba_id]
+ @phone_number_id = params[:phone_number_id]
+ @inbox_id = inbox_id
end
def perform
@@ -19,11 +20,19 @@ class Whatsapp::EmbeddedSignupService
# Validate token has access to the WABA
Whatsapp::TokenValidationService.new(access_token, @waba_id).perform
- # Create channel
- waba_info = { waba_id: @waba_id, business_name: phone_info[:business_name] }
-
- # Webhook setup is now handled in the channel after_create_commit callback
- Whatsapp::ChannelCreationService.new(@account, waba_info, phone_info, access_token).perform
+ # Reauthorization flow if inbox_id is present
+ if @inbox_id.present?
+ Whatsapp::ReauthorizationService.new(
+ account: @account,
+ inbox_id: @inbox_id,
+ phone_number_id: @phone_number_id,
+ business_id: @business_id
+ ).perform(access_token, phone_info)
+ else
+ # Create channel for new authorization
+ waba_info = { waba_id: @waba_id, business_name: phone_info[:business_name] }
+ Whatsapp::ChannelCreationService.new(@account, waba_info, phone_info, access_token).perform
+ end
rescue StandardError => e
Rails.logger.error("[WHATSAPP] Embedded signup failed: #{e.message}")
raise e
diff --git a/app/services/whatsapp/reauthorization_service.rb b/app/services/whatsapp/reauthorization_service.rb
new file mode 100644
index 000000000..aeb6dfbef
--- /dev/null
+++ b/app/services/whatsapp/reauthorization_service.rb
@@ -0,0 +1,42 @@
+class Whatsapp::ReauthorizationService
+ def initialize(account:, inbox_id:, phone_number_id:, business_id:)
+ @account = account
+ @inbox_id = inbox_id
+ @phone_number_id = phone_number_id
+ @business_id = business_id
+ end
+
+ def perform(access_token, phone_info)
+ inbox = @account.inboxes.find(@inbox_id)
+ channel = inbox.channel
+
+ # Validate phone number matches for reauthorization
+ if phone_info[:phone_number] != channel.phone_number
+ raise StandardError, "Phone number mismatch. Expected #{channel.phone_number}, got #{phone_info[:phone_number]}"
+ end
+
+ # Update channel configuration
+ update_channel_config(channel, access_token, phone_info)
+ # Mark as reauthorized
+ channel.reauthorized! if channel.respond_to?(:reauthorized!)
+
+ channel
+ end
+
+ private
+
+ def update_channel_config(channel, access_token, phone_info)
+ current_config = channel.provider_config || {}
+ channel.provider_config = current_config.merge(
+ 'api_key' => access_token,
+ 'phone_number_id' => @phone_number_id,
+ 'business_account_id' => @business_id,
+ 'source' => 'embedded_signup'
+ )
+ channel.save!
+
+ # Update inbox name if business name changed
+ business_name = phone_info[:business_name] || phone_info[:verified_name]
+ channel.inbox.update!(name: business_name) if business_name.present?
+ end
+end
diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder
index c66a6eb78..8042ee47c 100644
--- a/app/views/api/v1/models/_inbox.json.jbuilder
+++ b/app/views/api/v1/models/_inbox.json.jbuilder
@@ -116,4 +116,5 @@ json.provider resource.channel.try(:provider)
if resource.whatsapp?
json.message_templates resource.channel.try(:message_templates)
json.provider_config resource.channel.try(:provider_config) if Current.account_user&.administrator?
+ json.reauthorization_required resource.channel.try(:reauthorization_required?)
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9faa0cd9d..1d8347679 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -31,6 +31,11 @@
en:
hello: 'Hello world'
+ inbox:
+ reauthorization:
+ success: 'Channel reauthorized successfully'
+ not_required: 'Reauthorization is not required for this inbox'
+ invalid_channel: 'Invalid channel type for reauthorization'
messages:
reset_password_success: Woot! Request for password reset is successful. Check your mail for instructions.
reset_password_failure: Uh ho! We could not find any user with the specified email.
@@ -67,6 +72,13 @@ en:
invalid_message_type: 'Invalid message type. Action not permitted'
slack:
invalid_channel_id: 'Invalid slack channel. Please try again'
+ whatsapp:
+ token_exchange_failed: 'Failed to exchange code for access token. Please try again.'
+ invalid_token_permissions: 'The access token does not have the required permissions for WhatsApp.'
+ phone_info_fetch_failed: 'Failed to fetch phone number information. Please try again.'
+ reauthorization:
+ generic: 'Failed to reauthorize WhatsApp. Please try again.'
+ not_supported: 'Reauthorization is not supported for this type of WhatsApp channel.'
inboxes:
imap:
socket_error: Please check the network connection, IMAP address and try again.
diff --git a/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb b/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb
index eca193c76..2e74817a7 100644
--- a/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb
@@ -119,19 +119,18 @@ RSpec.describe 'WhatsApp Authorization API', type: :request do
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'
+ params: {
+ code: 'test_code',
+ business_id: 'test_business_id',
+ waba_id: 'test_waba_id',
+ phone_number_id: 'test_phone_id'
+ },
+ inbox_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)
+ allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(instance_double(Whatsapp::WebhookSetupService, perform: true))
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
@@ -151,19 +150,17 @@ RSpec.describe 'WhatsApp Authorization API', type: :request do
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
+ params: {
+ code: 'test_code',
+ business_id: 'test_business_id',
+ waba_id: 'test_waba_id'
+ },
+ inbox_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)
+ allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(instance_double(Whatsapp::WebhookSetupService, perform: true))
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
@@ -300,4 +297,236 @@ RSpec.describe 'WhatsApp Authorization API', type: :request do
end
end
end
+
+ describe 'POST /api/v1/accounts/{account.id}/whatsapp/authorization with inbox_id (reauthorization)' do
+ let(:whatsapp_channel) do
+ channel = build(:channel_whatsapp, account: account, provider: 'whatsapp_cloud',
+ provider_config: {
+ 'api_key' => 'test_token',
+ 'phone_number_id' => '123456',
+ 'business_account_id' => '654321',
+ 'source' => 'embedded_signup'
+ })
+ allow(channel).to receive(:validate_provider_config).and_return(true)
+ allow(channel).to receive(:sync_templates).and_return(true)
+ allow(channel).to receive(:setup_webhooks).and_return(true)
+ channel.save!
+ # Call authorization_error! twice to reach the threshold
+ channel.authorization_error!
+ channel.authorization_error!
+ channel
+ end
+ let(:whatsapp_inbox) { create(:inbox, channel: whatsapp_channel, account: account) }
+
+ context 'when user is an administrator' do
+ let(:administrator) { create(:user, account: account, role: :administrator) }
+
+ before do
+ account.enable_features!(:whatsapp_embedded_signup)
+ end
+
+ context 'with valid parameters' do
+ let(:valid_params) do
+ {
+ code: 'auth_code_123',
+ business_id: 'business_123',
+ waba_id: 'waba_123',
+ phone_number_id: 'phone_123'
+ }
+ end
+
+ it 'reauthorizes the WhatsApp channel successfully' do
+ allow(whatsapp_channel).to receive(:reauthorization_required?).and_return(true)
+
+ embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
+ allow(Whatsapp::EmbeddedSignupService).to receive(:new).with(
+ account: account,
+ params: {
+ code: 'auth_code_123',
+ business_id: 'business_123',
+ waba_id: 'waba_123',
+ phone_number_id: 'phone_123'
+ },
+ inbox_id: whatsapp_inbox.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(whatsapp_inbox)
+
+ post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
+ params: valid_params.merge(inbox_id: whatsapp_inbox.id),
+ headers: administrator.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = response.parsed_body
+ expect(json_response['success']).to be true
+ expect(json_response['id']).to eq(whatsapp_inbox.id)
+ end
+
+ it 'handles reauthorization failure' do
+ embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
+ allow(Whatsapp::EmbeddedSignupService).to receive(:new).with(
+ account: account,
+ params: {
+ code: 'auth_code_123',
+ business_id: 'business_123',
+ waba_id: 'waba_123',
+ phone_number_id: 'phone_123'
+ },
+ inbox_id: whatsapp_inbox.id
+ ).and_return(embedded_signup_service)
+ allow(embedded_signup_service).to receive(:perform)
+ .and_raise(StandardError, 'Token exchange failed')
+
+ post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
+ params: valid_params.merge(inbox_id: whatsapp_inbox.id),
+ headers: administrator.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ json_response = response.parsed_body
+ expect(json_response['success']).to be false
+ expect(json_response['error']).to eq('Token exchange failed')
+ end
+
+ it 'handles phone number mismatch error' do
+ embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
+ allow(Whatsapp::EmbeddedSignupService).to receive(:new).with(
+ account: account,
+ params: {
+ code: 'auth_code_123',
+ business_id: 'business_123',
+ waba_id: 'waba_123',
+ phone_number_id: 'phone_123'
+ },
+ inbox_id: whatsapp_inbox.id
+ ).and_return(embedded_signup_service)
+ allow(embedded_signup_service).to receive(:perform)
+ .and_raise(StandardError, 'Phone number mismatch. The new phone number (+1234567890) does not match ' \
+ 'the existing phone number (+15551234567). Please use the same WhatsApp ' \
+ 'Business Account that was originally connected.')
+
+ post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
+ params: valid_params.merge(inbox_id: whatsapp_inbox.id),
+ headers: administrator.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ json_response = response.parsed_body
+ expect(json_response['success']).to be false
+ expect(json_response['error']).to include('Phone number mismatch')
+ end
+ end
+
+ context 'when inbox does not exist' do
+ it 'returns not found error' do
+ post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
+ params: { inbox_id: 0, code: 'test', business_id: 'test', waba_id: 'test' },
+ headers: administrator.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when reauthorization is not required' do
+ let(:fresh_channel) do
+ channel = build(:channel_whatsapp, account: account, provider: 'whatsapp_cloud',
+ provider_config: {
+ 'api_key' => 'test_token',
+ 'phone_number_id' => '123456',
+ 'business_account_id' => '654321',
+ 'source' => 'embedded_signup'
+ })
+ allow(channel).to receive(:validate_provider_config).and_return(true)
+ allow(channel).to receive(:sync_templates).and_return(true)
+ allow(channel).to receive(:setup_webhooks).and_return(true)
+ channel.save!
+ # Do NOT call authorization_error! - channel is working fine
+ channel
+ end
+ let(:fresh_inbox) { create(:inbox, channel: fresh_channel, account: account) }
+
+ it 'returns unprocessable entity error' do
+ post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
+ params: { inbox_id: fresh_inbox.id },
+ headers: administrator.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ json_response = response.parsed_body
+ expect(json_response['success']).to be false
+ end
+ end
+
+ context 'when channel is not WhatsApp' do
+ let(:facebook_channel) do
+ stub_request(:post, 'https://graph.facebook.com/v3.2/me/subscribed_apps')
+ .to_return(status: 200, body: '{}', headers: {})
+
+ channel = create(:channel_facebook_page, account: account)
+ # Call authorization_error! twice to reach the threshold
+ channel.authorization_error!
+ channel.authorization_error!
+ channel
+ end
+ let(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
+
+ it 'returns unprocessable entity error' do
+ post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
+ params: { inbox_id: facebook_inbox.id },
+ headers: administrator.create_new_auth_token,
+ as: :json
+
+ expect(response).to have_http_status(:unprocessable_entity)
+ json_response = response.parsed_body
+ expect(json_response['success']).to be false
+ end
+ end
+ end
+
+ context 'when user is an agent' do
+ let(:agent) { create(:user, account: account, role: :agent) }
+
+ before do
+ account.enable_features!(:whatsapp_embedded_signup)
+ create(:inbox_member, inbox: whatsapp_inbox, user: agent)
+ end
+
+ it 'returns unprocessable_entity error' do
+ allow(whatsapp_channel).to receive(:reauthorization_required?).and_return(true)
+
+ # Stub the embedded signup service to prevent HTTP calls
+ embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
+ allow(Whatsapp::EmbeddedSignupService).to receive(:new).with(
+ account: account,
+ params: {
+ code: 'test',
+ business_id: 'test',
+ waba_id: 'test'
+ },
+ inbox_id: whatsapp_inbox.id
+ ).and_return(embedded_signup_service)
+ allow(embedded_signup_service).to receive(:perform).and_return(whatsapp_channel)
+
+ post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
+ params: { inbox_id: whatsapp_inbox.id, code: 'test', business_id: 'test', waba_id: 'test' },
+ headers: agent.create_new_auth_token,
+ as: :json
+
+ # Agents should get unprocessable_entity since they can find the inbox but channel doesn't need reauth
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ context 'when user is not authenticated' do
+ it 'returns unauthorized error' do
+ post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
+ params: { inbox_id: whatsapp_inbox.id },
+ as: :json
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
end
diff --git a/spec/services/whatsapp/embedded_signup_service_spec.rb b/spec/services/whatsapp/embedded_signup_service_spec.rb
index 95af73523..12a4d32df 100644
--- a/spec/services/whatsapp/embedded_signup_service_spec.rb
+++ b/spec/services/whatsapp/embedded_signup_service_spec.rb
@@ -2,17 +2,18 @@ require 'rails_helper'
describe Whatsapp::EmbeddedSignupService do
let(:account) { create(:account) }
- let(:code) { 'test_authorization_code' }
- let(:business_id) { 'test_business_id' }
- let(:waba_id) { 'test_waba_id' }
- let(:phone_number_id) { 'test_phone_number_id' }
+ let(:params) do
+ {
+ code: 'test_authorization_code',
+ business_id: 'test_business_id',
+ waba_id: 'test_waba_id',
+ phone_number_id: 'test_phone_number_id'
+ }
+ end
let(:service) do
described_class.new(
account: account,
- code: code,
- business_id: business_id,
- waba_id: waba_id,
- phone_number_id: phone_number_id
+ params: params
)
end
@@ -20,37 +21,40 @@ describe Whatsapp::EmbeddedSignupService do
let(:access_token) { 'test_access_token' }
let(:phone_info) do
{
- phone_number_id: phone_number_id,
+ phone_number_id: params[:phone_number_id],
phone_number: '+1234567890',
verified: true,
business_name: 'Test Business'
}
end
let(:channel) { instance_double(Channel::Whatsapp) }
-
- let(:token_exchange_service) { instance_double(Whatsapp::TokenExchangeService) }
- 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(:service_doubles) do
+ {
+ token_exchange: instance_double(Whatsapp::TokenExchangeService),
+ phone_info: instance_double(Whatsapp::PhoneInfoService),
+ token_validation: instance_double(Whatsapp::TokenValidationService),
+ channel_creation: instance_double(Whatsapp::ChannelCreationService)
+ }
+ end
before do
allow(GlobalConfig).to receive(:clear_cache)
- allow(Whatsapp::TokenExchangeService).to receive(:new).with(code).and_return(token_exchange_service)
- allow(token_exchange_service).to receive(:perform).and_return(access_token)
+ allow(Whatsapp::TokenExchangeService).to receive(:new).with(params[:code]).and_return(service_doubles[:token_exchange])
+ allow(service_doubles[:token_exchange]).to receive(:perform).and_return(access_token)
allow(Whatsapp::PhoneInfoService).to receive(:new)
- .with(waba_id, phone_number_id, access_token).and_return(phone_info_service)
- allow(phone_info_service).to receive(:perform).and_return(phone_info)
+ .with(params[:waba_id], params[:phone_number_id], access_token).and_return(service_doubles[:phone_info])
+ allow(service_doubles[:phone_info]).to receive(:perform).and_return(phone_info)
allow(Whatsapp::TokenValidationService).to receive(:new)
- .with(access_token, waba_id).and_return(token_validation_service)
- allow(token_validation_service).to receive(:perform)
+ .with(access_token, params[:waba_id]).and_return(service_doubles[:token_validation])
+ allow(service_doubles[:token_validation]).to receive(:perform)
allow(Whatsapp::ChannelCreationService).to receive(:new)
- .with(account, { waba_id: waba_id, business_name: 'Test Business' }, phone_info, access_token)
- .and_return(channel_creation_service)
- allow(channel_creation_service).to receive(:perform).and_return(channel)
+ .with(account, { waba_id: params[:waba_id], business_name: 'Test Business' }, phone_info, access_token)
+ .and_return(service_doubles[:channel_creation])
+ allow(service_doubles[:channel_creation]).to receive(:perform).and_return(channel)
# Webhook setup is now handled in the channel after_create callback
# So we stub it at the model level
@@ -60,10 +64,10 @@ describe Whatsapp::EmbeddedSignupService do
end
it 'orchestrates all services in the correct order' do
- expect(token_exchange_service).to receive(:perform).ordered
- 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(service_doubles[:token_exchange]).to receive(:perform).ordered
+ expect(service_doubles[:phone_info]).to receive(:perform).ordered
+ expect(service_doubles[:token_validation]).to receive(:perform).ordered
+ expect(service_doubles[:channel_creation]).to receive(:perform).ordered
result = service.perform
expect(result).to eq(channel)
@@ -73,10 +77,7 @@ describe Whatsapp::EmbeddedSignupService do
it 'raises error when code is blank' do
service = described_class.new(
account: account,
- code: '',
- business_id: business_id,
- waba_id: waba_id,
- phone_number_id: phone_number_id
+ params: params.merge(code: '')
)
expect { service.perform }.to raise_error(ArgumentError, /Required parameters are missing: code/)
end
@@ -84,10 +85,7 @@ describe Whatsapp::EmbeddedSignupService do
it 'raises error when business_id is blank' do
service = described_class.new(
account: account,
- code: code,
- business_id: '',
- waba_id: waba_id,
- phone_number_id: phone_number_id
+ params: params.merge(business_id: '')
)
expect { service.perform }.to raise_error(ArgumentError, /Required parameters are missing: business_id/)
end
@@ -95,10 +93,7 @@ describe Whatsapp::EmbeddedSignupService do
it 'raises error when waba_id is blank' do
service = described_class.new(
account: account,
- code: code,
- business_id: business_id,
- waba_id: '',
- phone_number_id: phone_number_id
+ params: params.merge(waba_id: '')
)
expect { service.perform }.to raise_error(ArgumentError, /Required parameters are missing: waba_id/)
end
@@ -106,10 +101,7 @@ describe Whatsapp::EmbeddedSignupService do
it 'raises error when multiple parameters are blank' do
service = described_class.new(
account: account,
- code: '',
- business_id: '',
- waba_id: waba_id,
- phone_number_id: phone_number_id
+ params: params.merge(code: '', business_id: '')
)
expect { service.perform }.to raise_error(ArgumentError, /Required parameters are missing: code, business_id/)
end
@@ -117,11 +109,44 @@ describe Whatsapp::EmbeddedSignupService do
context 'when any service fails' do
it 'logs and re-raises the error' do
- allow(token_exchange_service).to receive(:perform).and_raise('Token error')
+ allow(service_doubles[:token_exchange]).to receive(:perform).and_raise('Token error')
expect(Rails.logger).to receive(:error).with('[WHATSAPP] Embedded signup failed: Token error')
expect { service.perform }.to raise_error('Token error')
end
end
+
+ context 'when inbox_id is provided (reauthorization flow)' do
+ let(:inbox_id) { 123 }
+ let(:reauth_service) { instance_double(Whatsapp::ReauthorizationService) }
+ let(:service_with_inbox) do
+ described_class.new(
+ account: account,
+ params: params,
+ inbox_id: inbox_id
+ )
+ end
+
+ before do
+ allow(Whatsapp::ReauthorizationService).to receive(:new).with(
+ account: account,
+ inbox_id: inbox_id,
+ phone_number_id: params[:phone_number_id],
+ business_id: params[:business_id]
+ ).and_return(reauth_service)
+ allow(reauth_service).to receive(:perform).with(access_token, phone_info).and_return(channel)
+ end
+
+ it 'uses ReauthorizationService instead of ChannelCreationService' do
+ expect(service_doubles[:token_exchange]).to receive(:perform).ordered
+ expect(service_doubles[:phone_info]).to receive(:perform).ordered
+ expect(service_doubles[:token_validation]).to receive(:perform).ordered
+ expect(reauth_service).to receive(:perform).with(access_token, phone_info).ordered
+ expect(service_doubles[:channel_creation]).not_to receive(:perform)
+
+ result = service_with_inbox.perform
+ expect(result).to eq(channel)
+ end
+ end
end
end
diff --git a/spec/services/whatsapp/webhook_teardown_service_spec.rb b/spec/services/whatsapp/webhook_teardown_service_spec.rb
index 25d2ae374..2a7ba9fd0 100644
--- a/spec/services/whatsapp/webhook_teardown_service_spec.rb
+++ b/spec/services/whatsapp/webhook_teardown_service_spec.rb
@@ -7,6 +7,9 @@ RSpec.describe Whatsapp::WebhookTeardownService do
context 'when channel is whatsapp_cloud with embedded_signup' do
before do
+ # Stub webhook setup to prevent HTTP calls during channel update
+ allow(channel).to receive(:setup_webhooks).and_return(true)
+
channel.update!(
provider: 'whatsapp_cloud',
provider_config: {
From d0824cc86a2d10894c5cb00451e26405f32e76e7 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Fri, 8 Aug 2025 08:45:33 +0530
Subject: [PATCH 15/15] fix: Fix overlap issue with filter dropdown (#12133)
# Pull Request Template
## Description
This PR increase z-index of filter dropdown to prevent overlap with
other elements.
Fixes
https://linear.app/chatwoot/issue/CW-5394/update-z-index-for-filters
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
### Screenshot