fix: Setup webhooks for manual WhatsApp Cloud channel creation (#13278)

Fixes https://github.com/chatwoot/chatwoot/issues/13097

### Problem
The PR #12176 removed the `before_save :setup_webhooks` callback to fix
a race condition where Meta's webhook verification request arrived
before the channel was saved to the database. This change broke manual
WhatsApp Cloud channel setup. While embedded signup explicitly calls
`channel.setup_webhooks` in `EmbeddedSignupService`, manual setup had no
equivalent call - meaning the `subscribed_apps` endpoint was never
invoked and Meta never sent webhook events to Chatwoot.


### Solution
Added an `after_commit` callback that triggers webhook setup for manual
WhatsApp Cloud channels
This commit is contained in:
Muhsin Keloth
2026-01-19 14:12:36 +04:00
committed by GitHub
parent b2ffad1998
commit 7e4d93f649
5 changed files with 65 additions and 13 deletions

View File

@@ -34,6 +34,7 @@ class Channel::Whatsapp < ApplicationRecord
after_create :sync_templates after_create :sync_templates
before_destroy :teardown_webhooks before_destroy :teardown_webhooks
after_commit :setup_webhooks, on: :create, if: :should_auto_setup_webhooks?
def name def name
'Whatsapp' 'Whatsapp'
@@ -86,4 +87,10 @@ class Channel::Whatsapp < ApplicationRecord
def teardown_webhooks def teardown_webhooks
Whatsapp::WebhookTeardownService.new(self).perform Whatsapp::WebhookTeardownService.new(self).perform
end end
def should_auto_setup_webhooks?
# Only auto-setup webhooks for whatsapp_cloud provider with manual setup
# Embedded signup calls setup_webhooks explicitly in EmbeddedSignupService
provider == 'whatsapp_cloud' && provider_config['source'] != 'embedded_signup'
end
end end

View File

@@ -16,6 +16,10 @@ class Whatsapp::EmbeddedSignupService
validate_token_access(access_token) validate_token_access(access_token)
channel = create_or_reauthorize_channel(access_token, phone_info) channel = create_or_reauthorize_channel(access_token, phone_info)
# NOTE: We call setup_webhooks explicitly here instead of relying on after_commit callback because:
# 1. Reauthorization flow updates an existing channel (not a create), so after_commit on: :create won't trigger
# 2. We need to run check_channel_health_and_prompt_reauth after webhook setup completes
# 3. The channel is marked with source: 'embedded_signup' to skip the after_commit callback
channel.setup_webhooks channel.setup_webhooks
check_channel_health_and_prompt_reauth(channel) check_channel_health_and_prompt_reauth(channel)
channel channel

View File

@@ -96,8 +96,16 @@ FactoryBot.define do
channel_whatsapp.define_singleton_method(:sync_templates) { nil } unless options.sync_templates channel_whatsapp.define_singleton_method(:sync_templates) { nil } unless options.sync_templates
channel_whatsapp.define_singleton_method(:validate_provider_config) { nil } unless options.validate_provider_config channel_whatsapp.define_singleton_method(:validate_provider_config) { nil } unless options.validate_provider_config
if channel_whatsapp.provider == 'whatsapp_cloud' if channel_whatsapp.provider == 'whatsapp_cloud'
channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'phone_number_id' => '123456789', # Add 'source' => 'embedded_signup' to skip after_commit :setup_webhooks callback in tests
'business_account_id' => '123456789' }) # The callback is for manual setup flow; embedded signup handles webhook setup explicitly
# Only set source if not already provided (allows tests to override)
default_config = {
'api_key' => 'test_key',
'phone_number_id' => '123456789',
'business_account_id' => '123456789'
}
default_config['source'] = 'embedded_signup' unless channel_whatsapp.provider_config.key?('source')
channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge(default_config)
end end
end end

View File

@@ -47,16 +47,39 @@ RSpec.describe Channel::Whatsapp do
end end
describe 'webhook_verify_token' do describe 'webhook_verify_token' do
before do
# Stub webhook setup to prevent HTTP calls during channel creation
setup_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(setup_service)
allow(setup_service).to receive(:perform)
end
it 'generates webhook_verify_token if not present' do it 'generates webhook_verify_token if not present' do
channel = create(:channel_whatsapp, provider_config: { webhook_verify_token: nil }, provider: 'whatsapp_cloud', account: create(:account), channel = create(:channel_whatsapp,
validate_provider_config: false, sync_templates: false) provider_config: {
'webhook_verify_token' => nil,
'api_key' => 'test_key',
'business_account_id' => '123456789'
},
provider: 'whatsapp_cloud',
account: create(:account),
validate_provider_config: false,
sync_templates: false)
expect(channel.provider_config['webhook_verify_token']).not_to be_nil expect(channel.provider_config['webhook_verify_token']).not_to be_nil
end end
it 'does not generate webhook_verify_token if present' do it 'does not generate webhook_verify_token if present' do
channel = create(:channel_whatsapp, provider: 'whatsapp_cloud', provider_config: { webhook_verify_token: '123' }, account: create(:account), channel = create(:channel_whatsapp,
validate_provider_config: false, sync_templates: false) provider: 'whatsapp_cloud',
provider_config: {
'webhook_verify_token' => '123',
'api_key' => 'test_key',
'business_account_id' => '123456789'
},
account: create(:account),
validate_provider_config: false,
sync_templates: false)
expect(channel.provider_config['webhook_verify_token']).to eq '123' expect(channel.provider_config['webhook_verify_token']).to eq '123'
end end
@@ -91,15 +114,18 @@ RSpec.describe Channel::Whatsapp do
end end
context 'when channel is created through manual setup' do context 'when channel is created through manual setup' do
it 'does not setup webhooks' do it 'setups webhooks via after_commit callback' do
expect(Whatsapp::WebhookSetupService).not_to receive(:new) expect(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
expect(webhook_service).to receive(:perform)
# Explicitly set source to nil to test manual setup behavior (not embedded_signup)
create(:channel_whatsapp, create(:channel_whatsapp,
account: account, account: account,
provider: 'whatsapp_cloud', provider: 'whatsapp_cloud',
provider_config: { provider_config: {
'business_account_id' => 'test_waba_id', 'business_account_id' => 'test_waba_id',
'api_key' => 'test_access_token' 'api_key' => 'test_access_token',
'source' => nil
}, },
validate_provider_config: false, validate_provider_config: false,
sync_templates: false) sync_templates: false)
@@ -157,12 +183,17 @@ RSpec.describe Channel::Whatsapp do
end end
context 'when channel is not embedded_signup' do context 'when channel is not embedded_signup' do
it 'does not call WebhookTeardownService on destroy' do it 'calls WebhookTeardownService on destroy' do
# Mock the setup service to prevent HTTP calls during creation
setup_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(setup_service)
allow(setup_service).to receive(:perform)
channel = create(:channel_whatsapp, channel = create(:channel_whatsapp,
account: account, account: account,
provider: 'whatsapp_cloud', provider: 'whatsapp_cloud',
provider_config: { provider_config: {
'source' => 'manual', 'business_account_id' => 'test_waba_id',
'api_key' => 'test_access_token' 'api_key' => 'test_access_token'
}, },
validate_provider_config: false, validate_provider_config: false,

View File

@@ -6,7 +6,8 @@ describe Whatsapp::WebhookSetupService do
phone_number: '+1234567890', phone_number: '+1234567890',
provider_config: { provider_config: {
'phone_number_id' => '123456789', 'phone_number_id' => '123456789',
'webhook_verify_token' => 'test_verify_token' 'webhook_verify_token' => 'test_verify_token',
'source' => 'embedded_signup'
}, },
provider: 'whatsapp_cloud', provider: 'whatsapp_cloud',
sync_templates: false, sync_templates: false,
@@ -261,7 +262,8 @@ describe Whatsapp::WebhookSetupService do
'phone_number_id' => '123456789', 'phone_number_id' => '123456789',
'webhook_verify_token' => 'existing_verify_token', 'webhook_verify_token' => 'existing_verify_token',
'business_id' => 'existing_business_id', 'business_id' => 'existing_business_id',
'waba_id' => 'existing_waba_id' 'waba_id' => 'existing_waba_id',
'source' => 'embedded_signup'
}, },
provider: 'whatsapp_cloud', provider: 'whatsapp_cloud',
sync_templates: false, sync_templates: false,