From fd28ed8d83499aab42b1f7dcd1f4dc1204a9803d Mon Sep 17 00:00:00 2001 From: Petterson <58094725+hahuma@users.noreply.github.com> Date: Fri, 8 Aug 2025 09:58:50 -0300 Subject: [PATCH] feat: add support to embedded whatsapp coexistence method (#12108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This update adds support to the coexistence method to Embedded Whatsapp, allowing users to add their existing whatsapp business number in order to use it in both places(chatwoot and whatsapp business) at the same time. This update require some changes in the permissions for the Meta App, as described in the Meta Oficial Docs, I'll leave this listed below: - **history** — describes past messages the business customer has sent/received - **smb_app_state_sync** — describes the business customer's current and new contacts - **smb_message_echoes** — describes any new messages the business customer sends with the WhatsApp Business app after having been onboarded Co-authored-by: Muhsin Keloth Co-authored-by: Sojan Jose Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> --- .../dashboard/i18n/locale/en/inboxMgmt.json | 5 ++ .../inbox/channels/WhatsappEmbeddedSignup.vue | 26 +++++- .../settings/inbox/channels/whatsapp/utils.js | 2 +- app/services/whatsapp/facebook_api_client.rb | 10 +++ .../whatsapp/webhook_setup_service.rb | 12 ++- .../super_admin/app_configs/show.html.erb | 12 ++- config/installation_config.yml | 3 +- .../whatsapp/webhook_setup_service_spec.rb | 83 +++++++++++++------ 8 files changed, 120 insertions(+), 33 deletions(-) diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 821e2ecbd..4006212a3 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -280,6 +280,11 @@ "SECURE_AUTH": "Secure OAuth based authentication", "AUTO_CONFIG": "Automatic webhook and phone number configuration" }, + "LEARN_MORE": { + "TEXT": "To learn more about integrated signup, pricing, and limitations, visit", + "LINK_TEXT": "this link.", + "LINK_URL": "https://developers.facebook.com/docs/whatsapp/embedded-signup/custom-flows/onboarding-business-app-users#limitations" + }, "SUBMIT_BUTTON": "Connect with WhatsApp Business", "AUTH_PROCESSING": "Authenticating with Meta", "WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue index 384082b71..583ca2413 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue @@ -107,7 +107,7 @@ const completeSignupFlow = async businessDataParam => { code: authCode.value, business_id: businessDataParam.business_id, waba_id: businessDataParam.waba_id, - phone_number_id: businessDataParam.phone_number_id, + phone_number_id: businessDataParam?.phone_number_id || '', }; const responseData = await store.dispatch( @@ -127,7 +127,10 @@ const completeSignupFlow = async businessDataParam => { // Message handling const handleEmbeddedSignupData = async data => { - if (data.event === 'FINISH') { + if ( + data.event === 'FINISH' || + data.event === 'FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING' + ) { const businessDataLocal = data.data; if (isValidBusinessData(businessDataLocal)) { @@ -262,6 +265,25 @@ onBeforeUnmount(() => { +
+ + {{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.LEARN_MORE.TEXT') }} + {{ ' ' }} + + {{ + $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.LEARN_MORE.LINK_TEXT') + }} + + +
+
{ override_default_response_type: true, extras: { setup: {}, - featureType: '', + featureType: 'whatsapp_business_app_onboarding', sessionInfoVersion: '3', }, } diff --git a/app/services/whatsapp/facebook_api_client.rb b/app/services/whatsapp/facebook_api_client.rb index 441e41b1a..55ce4e698 100644 --- a/app/services/whatsapp/facebook_api_client.rb +++ b/app/services/whatsapp/facebook_api_client.rb @@ -50,6 +50,16 @@ class Whatsapp::FacebookApiClient handle_response(response, 'Phone registration failed') end + def phone_number_verified?(phone_number_id) + response = HTTParty.get( + "#{BASE_URI}/#{@api_version}/#{phone_number_id}", + headers: request_headers + ) + + data = handle_response(response, 'Phone status check failed') + data['code_verification_status'] == 'VERIFIED' + end + def subscribe_waba_webhook(waba_id, callback_url, verify_token) response = HTTParty.post( "#{BASE_URI}/#{@api_version}/#{waba_id}/subscribed_apps", diff --git a/app/services/whatsapp/webhook_setup_service.rb b/app/services/whatsapp/webhook_setup_service.rb index 6aff531f7..a3faaaa56 100644 --- a/app/services/whatsapp/webhook_setup_service.rb +++ b/app/services/whatsapp/webhook_setup_service.rb @@ -8,7 +8,8 @@ class Whatsapp::WebhookSetupService def perform validate_parameters! - register_phone_number + # Since coexistence method does not need to register, we check it + register_phone_number unless phone_number_verified? setup_webhook end @@ -64,4 +65,13 @@ class Whatsapp::WebhookSetupService "#{frontend_url}/webhooks/whatsapp/#{phone_number}" end + + def phone_number_verified? + phone_number_id = @channel.provider_config['phone_number_id'] + + @api_client.phone_number_verified?(phone_number_id) + rescue StandardError => e + Rails.logger.error("[WHATSAPP] Phone registration status check failed, but continuing: #{e.message}") + false + end end diff --git a/app/views/super_admin/app_configs/show.html.erb b/app/views/super_admin/app_configs/show.html.erb index 6f95a6418..8bf513597 100644 --- a/app/views/super_admin/app_configs/show.html.erb +++ b/app/views/super_admin/app_configs/show.html.erb @@ -53,9 +53,15 @@
- <% else %> - <%= form.text_field "app_config[#{key}]", value: @app_config[key] %> - <% end %> + <% elsif @installation_configs[key]&.dig('type') == 'select' && @installation_configs[key]&.dig('options').present? %> + <%= form.select "app_config[#{key}]", + @installation_configs[key]['options'].map { |val, label| [label, val] }, + { selected: @app_config[key] }, + class: "mt-2 border border-slate-100 p-1 rounded-md" + %> + <% else %> + <%= form.text_field "app_config[#{key}]", value: @app_config[key] %> + <% end %> <%if @installation_configs[key]&.dig('description').present? %>

<%= @installation_configs[key]&.dig('description') %> diff --git a/config/installation_config.yml b/config/installation_config.yml index 53251fc3c..01799e830 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -10,7 +10,8 @@ # locked: if you don't specify locked attribute in yaml, the default value will be true, # which means the particular config will be locked and won't be available in `super_admin/installation_configs` # premium: These values get overwritten unless the user is on a premium plan -# type: The type of the config. Default is text, boolean is also supported +# type: The type of the config. Default is text, select and boolean are also supported +# options: For select types, its required to have options for the select in the following pattern: "option_value":"Human readable option" # ------- Branding Related Config ------- # - name: INSTALLATION_NAME diff --git a/spec/services/whatsapp/webhook_setup_service_spec.rb b/spec/services/whatsapp/webhook_setup_service_spec.rb index 89beca922..7cee115c2 100644 --- a/spec/services/whatsapp/webhook_setup_service_spec.rb +++ b/spec/services/whatsapp/webhook_setup_service_spec.rb @@ -24,25 +24,19 @@ describe Whatsapp::WebhookSetupService do end describe '#perform' do - context 'when all operations succeed' do + context 'when phone number is NOT verified (should register)' do before do + allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false) allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456) allow(api_client).to receive(:register_phone_number).with('123456789', 223_456) allow(api_client).to receive(:subscribe_waba_webhook) - .with(waba_id, anything, 'test_verify_token') - .and_return({ 'success' => true }) + .with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true }) allow(channel).to receive(:save!) end - it 'registers the phone number' do + it 'registers the phone number and sets up webhook' do with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do expect(api_client).to receive(:register_phone_number).with('123456789', 223_456) - service.perform - end - end - - it 'sets up webhook subscription' do - with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do expect(api_client).to receive(:subscribe_waba_webhook) .with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token') service.perform @@ -50,33 +44,71 @@ describe Whatsapp::WebhookSetupService do end end - context 'when phone registration fails' do + context 'when phone number IS verified (should NOT register)' do before do - allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456) - allow(api_client).to receive(:register_phone_number) - .and_raise('Registration failed') + allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(true) allow(api_client).to receive(:subscribe_waba_webhook) - .and_return({ 'success' => true }) + .with(waba_id, anything, 'test_verify_token').and_return({ 'success' => true }) end - it 'continues with webhook setup' do + it 'does NOT register phone, but sets up webhook' do with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do + expect(api_client).not_to receive(:register_phone_number) + expect(api_client).to receive(:subscribe_waba_webhook) + .with(waba_id, 'https://app.chatwoot.com/webhooks/whatsapp/+1234567890', 'test_verify_token') + service.perform + end + end + end + + context 'when phone_number_verified? raises error' do + before do + allow(api_client).to receive(:phone_number_verified?).with('123456789').and_raise('API down') + allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456) + allow(api_client).to receive(:register_phone_number) + allow(api_client).to receive(:subscribe_waba_webhook).and_return({ 'success' => true }) + allow(channel).to receive(:save!) + end + + it 'tries to register phone and proceeds with webhook setup' do + with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do + expect(api_client).to receive(:register_phone_number) expect(api_client).to receive(:subscribe_waba_webhook) expect { service.perform }.not_to raise_error end end end - context 'when webhook setup fails' do + context 'when phone registration fails (not blocking)' do before do + allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false) + allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456) + allow(api_client).to receive(:register_phone_number).and_raise('Registration failed') + allow(api_client).to receive(:subscribe_waba_webhook).and_return({ 'success' => true }) + allow(channel).to receive(:save!) + end + + it 'continues with webhook setup even if registration fails' do + with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do + expect(api_client).to receive(:register_phone_number) + expect(api_client).to receive(:subscribe_waba_webhook) + expect { service.perform }.not_to raise_error + end + end + end + + context 'when webhook setup fails (should raise)' do + before do + allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false) allow(SecureRandom).to receive(:random_number).with(900_000).and_return(123_456) allow(api_client).to receive(:register_phone_number) - allow(api_client).to receive(:subscribe_waba_webhook) - .and_raise('Webhook failed') + allow(api_client).to receive(:subscribe_waba_webhook).and_raise('Webhook failed') end it 'raises an error' do with_modified_env FRONTEND_URL: 'https://app.chatwoot.com' do + expect(api_client).to receive(:register_phone_number) + expect(api_client).to receive(:subscribe_waba_webhook) expect { service.perform }.to raise_error(/Webhook setup failed/) end end @@ -84,24 +116,25 @@ describe Whatsapp::WebhookSetupService do context 'when required parameters are missing' do it 'raises error when channel is nil' do - service = described_class.new(nil, waba_id, access_token) - expect { service.perform }.to raise_error(ArgumentError, 'Channel is required') + service_invalid = described_class.new(nil, waba_id, access_token) + expect { service_invalid.perform }.to raise_error(ArgumentError, 'Channel is required') end it 'raises error when waba_id is blank' do - service = described_class.new(channel, '', access_token) - expect { service.perform }.to raise_error(ArgumentError, 'WABA ID is required') + service_invalid = described_class.new(channel, '', access_token) + expect { service_invalid.perform }.to raise_error(ArgumentError, 'WABA ID is required') end it 'raises error when access_token is blank' do - service = described_class.new(channel, waba_id, '') - expect { service.perform }.to raise_error(ArgumentError, 'Access token is required') + service_invalid = described_class.new(channel, waba_id, '') + expect { service_invalid.perform }.to raise_error(ArgumentError, 'Access token is required') end end context 'when PIN already exists' do before do channel.provider_config['verification_pin'] = 123_456 + allow(api_client).to receive(:phone_number_verified?).with('123456789').and_return(false) allow(api_client).to receive(:register_phone_number) allow(api_client).to receive(:subscribe_waba_webhook).and_return({ 'success' => true }) allow(channel).to receive(:save!)