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:
@@ -34,6 +34,7 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
|
||||
after_create :sync_templates
|
||||
before_destroy :teardown_webhooks
|
||||
after_commit :setup_webhooks, on: :create, if: :should_auto_setup_webhooks?
|
||||
|
||||
def name
|
||||
'Whatsapp'
|
||||
@@ -86,4 +87,10 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
def teardown_webhooks
|
||||
Whatsapp::WebhookTeardownService.new(self).perform
|
||||
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
|
||||
|
||||
@@ -16,6 +16,10 @@ class Whatsapp::EmbeddedSignupService
|
||||
validate_token_access(access_token)
|
||||
|
||||
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
|
||||
check_channel_health_and_prompt_reauth(channel)
|
||||
channel
|
||||
|
||||
@@ -96,8 +96,16 @@ FactoryBot.define do
|
||||
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
|
||||
if channel_whatsapp.provider == 'whatsapp_cloud'
|
||||
channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'phone_number_id' => '123456789',
|
||||
'business_account_id' => '123456789' })
|
||||
# Add 'source' => 'embedded_signup' to skip after_commit :setup_webhooks callback in tests
|
||||
# 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
|
||||
|
||||
|
||||
@@ -47,16 +47,39 @@ RSpec.describe Channel::Whatsapp do
|
||||
end
|
||||
|
||||
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
|
||||
channel = create(:channel_whatsapp, provider_config: { webhook_verify_token: nil }, provider: 'whatsapp_cloud', account: create(:account),
|
||||
validate_provider_config: false, sync_templates: false)
|
||||
channel = create(:channel_whatsapp,
|
||||
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
|
||||
end
|
||||
|
||||
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),
|
||||
validate_provider_config: false, sync_templates: false)
|
||||
channel = create(:channel_whatsapp,
|
||||
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'
|
||||
end
|
||||
@@ -91,15 +114,18 @@ RSpec.describe Channel::Whatsapp do
|
||||
end
|
||||
|
||||
context 'when channel is created through manual setup' do
|
||||
it 'does not setup webhooks' do
|
||||
expect(Whatsapp::WebhookSetupService).not_to receive(:new)
|
||||
it 'setups webhooks via after_commit callback' do
|
||||
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,
|
||||
account: account,
|
||||
provider: 'whatsapp_cloud',
|
||||
provider_config: {
|
||||
'business_account_id' => 'test_waba_id',
|
||||
'api_key' => 'test_access_token'
|
||||
'api_key' => 'test_access_token',
|
||||
'source' => nil
|
||||
},
|
||||
validate_provider_config: false,
|
||||
sync_templates: false)
|
||||
@@ -157,12 +183,17 @@ RSpec.describe Channel::Whatsapp do
|
||||
end
|
||||
|
||||
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,
|
||||
account: account,
|
||||
provider: 'whatsapp_cloud',
|
||||
provider_config: {
|
||||
'source' => 'manual',
|
||||
'business_account_id' => 'test_waba_id',
|
||||
'api_key' => 'test_access_token'
|
||||
},
|
||||
validate_provider_config: false,
|
||||
|
||||
@@ -6,7 +6,8 @@ describe Whatsapp::WebhookSetupService do
|
||||
phone_number: '+1234567890',
|
||||
provider_config: {
|
||||
'phone_number_id' => '123456789',
|
||||
'webhook_verify_token' => 'test_verify_token'
|
||||
'webhook_verify_token' => 'test_verify_token',
|
||||
'source' => 'embedded_signup'
|
||||
},
|
||||
provider: 'whatsapp_cloud',
|
||||
sync_templates: false,
|
||||
@@ -261,7 +262,8 @@ describe Whatsapp::WebhookSetupService do
|
||||
'phone_number_id' => '123456789',
|
||||
'webhook_verify_token' => 'existing_verify_token',
|
||||
'business_id' => 'existing_business_id',
|
||||
'waba_id' => 'existing_waba_id'
|
||||
'waba_id' => 'existing_waba_id',
|
||||
'source' => 'embedded_signup'
|
||||
},
|
||||
provider: 'whatsapp_cloud',
|
||||
sync_templates: false,
|
||||
|
||||
Reference in New Issue
Block a user