feat: add support to embedded whatsapp coexistence method (#12108)

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 <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com>
This commit is contained in:
Petterson
2025-08-08 09:58:50 -03:00
committed by GitHub
parent b5f5c5c1bc
commit fd28ed8d83
8 changed files with 120 additions and 33 deletions

View File

@@ -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...",

View File

@@ -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(() => {
</div>
</div>
<div class="flex flex-col gap-2 mb-6">
<span class="text-sm text-n-slate-11">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.LEARN_MORE.TEXT') }}
{{ ' ' }}
<a
:href="
$t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.LEARN_MORE.LINK_URL')
"
target="_blank"
rel="noopener noreferrer"
class="underline text-primary"
>
{{
$t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.LEARN_MORE.LINK_TEXT')
}}
</a>
</span>
</div>
<div class="flex mt-4">
<NextButton
:disabled="isAuthenticating"

View File

@@ -74,7 +74,7 @@ export const initWhatsAppEmbeddedSignup = configId => {
override_default_response_type: true,
extras: {
setup: {},
featureType: '',
featureType: 'whatsapp_business_app_onboarding',
sessionInfoVersion: '3',
},
}

View File

@@ -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",

View File

@@ -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

View File

@@ -53,9 +53,15 @@
</svg>
</button>
</div>
<% 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? %>
<p class="pt-2 text-xs italic text-slate-400">
<%= @installation_configs[key]&.dig('description') %>

View File

@@ -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

View File

@@ -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!)