feat: Whatsapp embedded signup (#11612)

## Description

This PR introduces WhatsApp Embedded Signup functionality, enabling
users to connect their WhatsApp Business accounts through Meta's
streamlined OAuth flow without manual webhook configuration. This
significantly improves the user experience by automating the entire
setup process.

**Key Features:**

- Embedded signup flow using Facebook SDK and Meta's OAuth 2.0
- Automatic webhook registration and phone number configuration
- Enhanced provider selection UI with card-based design
- Real-time progress tracking during signup process
- Comprehensive error handling and user feedback


## Required Configuration

The following environment variables must be configured by administrators
before this feature can be used:
Super Admin Configuration (via
super_admin/app_config?config=whatsapp_embedded)

- `WHATSAPP_APP_ID`: The Facebook App ID for WhatsApp Business API
integration
- `WHATSAPP_CONFIGURATION_ID`: The Configuration ID for WhatsApp
Embedded Signup flow (obtained from Meta Developer Portal)
- `WHATSAPP_APP_SECRET`: The App Secret for WhatsApp Embedded Signup
flow (required for token exchange)
![Screenshot 2025-06-09 at 11 21
08 AM](https://github.com/user-attachments/assets/1615fb0d-27fc-4d9e-b193-9be7894ea93a)


## How Has This Been Tested?

#### Backend Tests (RSpec):

- Authentication validation for embedded signup endpoints
- Authorization code validation and error handling
- Missing business parameter validation
- Proper response format for configuration endpoint
- Unauthorized access prevention

#### Manual Test Cases:

- Complete embedded signup flow (happy path)
- Provider selection UI navigation
- Facebook authentication popup handling
- Error scenarios (cancelled auth, invalid business data, API failures)
- Configuration presence/absence behavior

## Related Screenshots:

![Screenshot 2025-06-09 at 7 48
18 PM](https://github.com/user-attachments/assets/34001425-df11-4d78-9424-334461e3178f)
![Screenshot 2025-06-09 at 7 48
22 PM](https://github.com/user-attachments/assets/c09f4964-3aba-4c39-9285-d1e8e37d0e33)
![Screenshot 2025-06-09 at 7 48
32 PM](https://github.com/user-attachments/assets/a34d5382-7a91-4e1c-906e-dc2d570c864a)
![Screenshot 2025-06-09 at 10 43
05 AM](https://github.com/user-attachments/assets/a15840d8-8223-4513-82e4-b08f23c95927)
![Screenshot 2025-06-09 at 10 42
56 AM](https://github.com/user-attachments/assets/8c345022-38b5-44c4-aba2-0cda81389c69)


Fixes
https://linear.app/chatwoot/issue/CW-2131/spec-for-whatsapp-cloud-channels-sign-in-with-facebook

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
Tanmay Deep Sharma
2025-07-15 11:37:06 +07:00
committed by GitHub
parent 4378506a35
commit 61d10044a0
58 changed files with 2384 additions and 63 deletions

View File

@@ -50,4 +50,4 @@ RSpec.describe 'Notion Authorization API', type: :request do
end
end
end
end
end

View File

@@ -0,0 +1,303 @@
require 'rails_helper'
RSpec.describe 'WhatsApp Authorization API', type: :request do
let(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/whatsapp/authorization' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
context 'when feature is not enabled' do
before do
account.disable_features!(:whatsapp_embedded_signup)
end
it 'returns forbidden' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:forbidden)
expect(response.parsed_body['error']).to eq('WhatsApp embedded signup is not enabled for this account')
end
end
context 'when feature is enabled' do
before do
account.enable_features!(:whatsapp_embedded_signup)
end
it 'returns unprocessable entity when code is missing' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to include('code')
end
it 'returns unprocessable entity when business_id is missing' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to include('business_id')
end
it 'returns unprocessable entity when waba_id is missing' do
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to include('waba_id')
end
it 'creates whatsapp channel successfully' do
whatsapp_channel = create(:channel_whatsapp, account: account, validate_provider_config: false, sync_templates: false)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).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 to prevent HTTP calls
webhook_service = instance_double(Whatsapp::WebhookSetupService)
allow(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
allow(webhook_service).to receive(:perform)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id',
phone_number_id: 'test_phone_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data['success']).to be true
expect(response_data['id']).to eq(inbox.id)
expect(response_data['name']).to eq(inbox.name)
expect(response_data['channel_type']).to eq('whatsapp')
end
it 'calls the embedded signup service with correct parameters' do
whatsapp_channel = create(:channel_whatsapp, account: account, validate_provider_config: false, sync_templates: false)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
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'
).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)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id',
phone_number_id: 'test_phone_id'
},
headers: agent.create_new_auth_token,
as: :json
end
it 'accepts phone_number_id as optional parameter' do
whatsapp_channel = create(:channel_whatsapp, account: account, validate_provider_config: false, sync_templates: false)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
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
).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)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
it 'returns unprocessable entity when service fails' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error')
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
response_data = response.parsed_body
expect(response_data['success']).to be false
expect(response_data['error']).to eq('Service error')
end
it 'logs error when service fails' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new).and_raise(StandardError, 'Service error')
expect(Rails.logger).to receive(:error).with(/\[WHATSAPP AUTHORIZATION\] Embedded signup error: Service error/)
expect(Rails.logger).to receive(:error).with(/authorizations_controller/)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
end
it 'handles token exchange errors' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new)
.and_raise(StandardError, 'Invalid authorization code')
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'invalid_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Invalid authorization code')
end
it 'handles channel already exists error' do
allow(Whatsapp::EmbeddedSignupService).to receive(:new)
.and_raise(StandardError, 'Channel already exists')
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(response.parsed_body['error']).to eq('Channel already exists')
end
end
context 'when user is not authorized for the account' do
let(:other_account) { create(:account) }
before do
account.enable_features!(:whatsapp_embedded_signup)
end
it 'returns unauthorized' do
post "/api/v1/accounts/#{other_account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when user is an administrator' do
before do
account.enable_features!(:whatsapp_embedded_signup)
end
it 'allows channel creation' do
embedded_signup_service = instance_double(Whatsapp::EmbeddedSignupService)
whatsapp_channel = create(:channel_whatsapp, account: account, validate_provider_config: false, sync_templates: false)
inbox = create(:inbox, account: account, channel: whatsapp_channel)
allow(Whatsapp::EmbeddedSignupService).to receive(:new).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)
post "/api/v1/accounts/#{account.id}/whatsapp/authorization",
params: {
code: 'test_code',
business_id: 'test_business_id',
waba_id: 'test_waba_id'
},
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
end
end