Merge branch 'feat/whatsapp-embedded-signup' of https://github.com/chatwoot/chatwoot into feat/whatsapp-embedded-signup

This commit is contained in:
Muhsin Keloth
2025-06-11 14:22:58 +05:30
7 changed files with 55 additions and 61 deletions
@@ -40,7 +40,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET],
'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET],
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT],
'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID]
'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION]
}
@allowed_configs = mapping.fetch(@config, %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS])
@@ -293,7 +293,7 @@ export function useWhatsappEmbeddedSignup() {
appId: window.chatwootConfig?.whatsappAppId,
status: true,
xfbml: true,
version: window.chatwootConfig?.fbApiVersion || 'v21.0',
version: window.chatwootConfig?.whatsappApiVersion || 'v22.0',
});
fbSdkLoaded.value = true;
};
@@ -35,9 +35,13 @@ class Whatsapp::EmbeddedSignupService
private
def whatsapp_api_version
@whatsapp_api_version ||= GlobalConfigService.load('WHATSAPP_API_VERSION', 'v22.0')
end
def exchange_code_for_token
response = Faraday.get(
'https://graph.facebook.com/v21.0/oauth/access_token',
"https://graph.facebook.com/#{whatsapp_api_version}/oauth/access_token",
{
client_id: GlobalConfigService.load('WHATSAPP_APP_ID', ''),
client_secret: GlobalConfigService.load('WHATSAPP_APP_SECRET', ''),
@@ -56,7 +60,7 @@ class Whatsapp::EmbeddedSignupService
def fetch_phone_info_via_waba(waba_id, phone_number_id, access_token)
# Get all phone numbers for the WABA
response = Faraday.get(
"https://graph.facebook.com/v21.0/#{waba_id}/phone_numbers",
"https://graph.facebook.com/#{whatsapp_api_version}/#{waba_id}/phone_numbers",
{ access_token: access_token }
)
@@ -64,11 +68,7 @@ class Whatsapp::EmbeddedSignupService
data = JSON.parse(response.body)
phone_numbers = data['data']
# Find the specific phone number we're looking for
phone_data = phone_numbers.find { |phone| phone['id'] == phone_number_id }
phone_data = phone_numbers.first if phone_data.nil?
phone_data = phone_numbers.find { |phone| phone['id'] == phone_number_id } || phone_numbers.first
raise "No phone numbers found for WABA #{waba_id}" if phone_data.nil?
@@ -98,22 +98,16 @@ class Whatsapp::EmbeddedSignupService
def register_phone_number(phone_number_id, access_token)
HTTParty.post(
"https://graph.facebook.com/v21.0/#{phone_number_id}/register",
"https://graph.facebook.com/#{whatsapp_api_version}/#{phone_number_id}/register",
{
headers: { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' },
body: {
messaging_product: 'whatsapp',
pin: '212834' # TODO: figure out the correct value of this field
}.to_json
body: { messaging_product: 'whatsapp', pin: '212834' }.to_json
}
)
end
def find_existing_channel(phone_number)
Channel::Whatsapp.find_by(
account: @account,
phone_number: phone_number
)
Channel::Whatsapp.find_by(account: @account, phone_number: phone_number)
end
def build_channel_attributes(waba_info, phone_info, access_token)
@@ -130,29 +124,20 @@ class Whatsapp::EmbeddedSignupService
end
def create_new_channel(attributes, phone_info)
channel = Channel::Whatsapp.create!(
account: @account,
**attributes
)
channel = Channel::Whatsapp.create!(account: @account, **attributes)
create_inbox_for_channel(channel, phone_info)
channel.reload
channel
end
def create_inbox_for_channel(channel, phone_info)
inbox_name = generate_inbox_name(phone_info)
Inbox.create!(
account: @account,
name: inbox_name,
name: "#{phone_info[:business_name]} WhatsApp",
channel: channel
)
end
def generate_inbox_name(phone_info)
"#{phone_info[:business_name]} WhatsApp"
end
def sanitize_phone_number(phone_number)
return phone_number if phone_number.blank?
@@ -167,7 +152,7 @@ class Whatsapp::EmbeddedSignupService
def fetch_token_debug_data(access_token)
response = Faraday.get(
'https://graph.facebook.com/v21.0/debug_token',
"https://graph.facebook.com/#{whatsapp_api_version}/debug_token",
{
input_token: access_token,
access_token: build_app_access_token
@@ -207,7 +192,7 @@ class Whatsapp::EmbeddedSignupService
verify_token = channel.provider_config['webhook_verify_token']
response = HTTParty.post(
"https://graph.facebook.com/v21.0/#{waba_id}/subscribed_apps",
"https://graph.facebook.com/#{whatsapp_api_version}/#{waba_id}/subscribed_apps",
{
headers: {
'Authorization' => "Bearer #{access_token}",
+1
View File
@@ -41,6 +41,7 @@
fbApiVersion: '<%= @global_config['FACEBOOK_API_VERSION'] %>',
whatsappAppId: '<%= @global_config['WHATSAPP_APP_ID'] %>',
whatsappConfigurationId: '<%= @global_config['WHATSAPP_CONFIGURATION_ID'] %>',
whatsappApiVersion: '<%= @global_config['WHATSAPP_API_VERSION'] %>',
signupEnabled: '<%= @global_config['ENABLE_ACCOUNT_SIGNUP'] %>',
isEnterprise: '<%= @global_config['IS_ENTERPRISE'] %>',
<% if @global_config['IS_ENTERPRISE'] %>
+5
View File
@@ -139,6 +139,11 @@
display_title: 'WhatsApp App Secret'
description: 'The App Secret for WhatsApp Embedded Signup flow (required for embedded signup)'
locked: false
- name: WHATSAPP_API_VERSION
display_title: 'WhatsApp API Version'
description: 'Configure this if you want to use a different WhatsApp API version. Make sure its prefixed with `v`'
value: 'v22.0'
locked: false
# ------- End of WhatsApp Channel Related Config ------- #
# MARK: Microsoft Email Channel Config
@@ -9,6 +9,7 @@ RSpec.describe 'WhatsApp Embedded API', type: :request do
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_APP_ID', '').and_return('test_app_id')
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_CONFIGURATION_ID', '').and_return('test_config_id')
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_APP_SECRET', '').and_return('test_app_secret')
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_API_VERSION', '').and_return('v22.0')
end
context 'when user is authenticated' do
@@ -9,6 +9,7 @@ describe Whatsapp::EmbeddedSignupService do
let(:access_token) { 'test_access_token' }
let(:app_id) { 'test_app_id' }
let(:app_secret) { 'test_app_secret' }
let(:api_version) { 'v22.0' }
let(:service) do
described_class.new(
@@ -24,6 +25,7 @@ describe Whatsapp::EmbeddedSignupService do
# Mock global configuration
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_APP_ID', '').and_return(app_id)
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_APP_SECRET', '').and_return(app_secret)
allow(GlobalConfigService).to receive(:load).with('WHATSAPP_API_VERSION', 'v22.0').and_return(api_version)
allow(GlobalConfig).to receive(:clear_cache)
# Mock environment variables - allow any calls to ENV.fetch
@@ -41,7 +43,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when all parameters are valid' do
before do
# Stub the token exchange
stub_request(:get, 'https://graph.facebook.com/v21.0/oauth/access_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: hash_including(
'client_id' => app_id,
'client_secret' => app_secret,
@@ -54,7 +56,7 @@ describe Whatsapp::EmbeddedSignupService do
)
# Stub the phone numbers fetch
stub_request(:get, "https://graph.facebook.com/v21.0/#{waba_id}/phone_numbers")
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: hash_including('access_token' => access_token))
.to_return(
status: 200,
@@ -72,7 +74,7 @@ describe Whatsapp::EmbeddedSignupService do
)
# Stub the token validation
stub_request(:get, 'https://graph.facebook.com/v21.0/debug_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: hash_including(
'input_token' => access_token,
'access_token' => "#{app_id}|#{app_secret}"
@@ -102,7 +104,7 @@ describe Whatsapp::EmbeddedSignupService do
)
# Stub the phone number registration
stub_request(:post, "https://graph.facebook.com/v21.0/#{phone_number_id}/register")
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}/register")
.to_return(
status: 200,
body: { success: true }.to_json,
@@ -110,7 +112,7 @@ describe Whatsapp::EmbeddedSignupService do
)
# Stub the webhook subscription
stub_request(:post, "https://graph.facebook.com/v21.0/#{waba_id}/subscribed_apps")
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.to_return(
status: 200,
body: { success: true }.to_json,
@@ -142,7 +144,7 @@ describe Whatsapp::EmbeddedSignupService do
it 'registers the phone number' do
service.perform
expect(WebMock).to have_requested(:post, "https://graph.facebook.com/v21.0/#{phone_number_id}/register")
expect(WebMock).to have_requested(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}/register")
.with(
body: {
messaging_product: 'whatsapp',
@@ -157,7 +159,7 @@ describe Whatsapp::EmbeddedSignupService do
channel = Channel::Whatsapp.find_by(account: account, phone_number: '+1234567890')
callback_url = "https://app.chatwoot.com/webhooks/whatsapp/#{channel.phone_number}"
expect(WebMock).to have_requested(:post, "https://graph.facebook.com/v21.0/#{waba_id}/subscribed_apps")
expect(WebMock).to have_requested(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.with(
body: hash_including(
override_callback_uri: callback_url,
@@ -220,7 +222,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when channel already exists' do
before do
# Stub all the required requests for successful flow
stub_request(:get, 'https://graph.facebook.com/v21.0/oauth/access_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: hash_including(
'client_id' => app_id,
'client_secret' => app_secret,
@@ -232,7 +234,7 @@ describe Whatsapp::EmbeddedSignupService do
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, "https://graph.facebook.com/v21.0/#{waba_id}/phone_numbers")
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: hash_including('access_token' => access_token))
.to_return(
status: 200,
@@ -249,7 +251,7 @@ describe Whatsapp::EmbeddedSignupService do
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, 'https://graph.facebook.com/v21.0/debug_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: hash_including(
'input_token' => access_token,
'access_token' => "#{app_id}|#{app_secret}"
@@ -295,7 +297,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when token exchange fails' do
before do
stub_request(:get, 'https://graph.facebook.com/v21.0/oauth/access_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: hash_including(
'client_id' => app_id,
'client_secret' => app_secret,
@@ -311,7 +313,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when token has no access to WABA' do
before do
stub_request(:get, 'https://graph.facebook.com/v21.0/oauth/access_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: hash_including(
'client_id' => app_id,
'client_secret' => app_secret,
@@ -323,7 +325,7 @@ describe Whatsapp::EmbeddedSignupService do
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, "https://graph.facebook.com/v21.0/#{waba_id}/phone_numbers")
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: hash_including('access_token' => access_token))
.to_return(
status: 200,
@@ -340,7 +342,7 @@ describe Whatsapp::EmbeddedSignupService do
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, 'https://graph.facebook.com/v21.0/debug_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: hash_including(
'input_token' => access_token,
'access_token' => "#{app_id}|#{app_secret}"
@@ -368,7 +370,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when phone numbers fetch fails' do
before do
stub_request(:get, 'https://graph.facebook.com/v21.0/oauth/access_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: hash_including(
'client_id' => app_id,
'client_secret' => app_secret,
@@ -380,7 +382,7 @@ describe Whatsapp::EmbeddedSignupService do
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, "https://graph.facebook.com/v21.0/#{waba_id}/phone_numbers")
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: hash_including('access_token' => access_token))
.to_return(status: 400, body: { error: 'Phone numbers fetch failed' }.to_json)
end
@@ -393,7 +395,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when webhook override fails' do
before do
# Stub all the successful requests
stub_request(:get, 'https://graph.facebook.com/v21.0/oauth/access_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: hash_including(
'client_id' => app_id,
'client_secret' => app_secret,
@@ -405,7 +407,7 @@ describe Whatsapp::EmbeddedSignupService do
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, "https://graph.facebook.com/v21.0/#{waba_id}/phone_numbers")
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: hash_including('access_token' => access_token))
.to_return(
status: 200,
@@ -422,7 +424,7 @@ describe Whatsapp::EmbeddedSignupService do
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, 'https://graph.facebook.com/v21.0/debug_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: hash_including(
'input_token' => access_token,
'access_token' => "#{app_id}|#{app_secret}"
@@ -450,7 +452,7 @@ describe Whatsapp::EmbeddedSignupService do
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:post, "https://graph.facebook.com/v21.0/#{phone_number_id}/register")
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{phone_number_id}/register")
.to_return(
status: 200,
body: { success: true }.to_json,
@@ -458,7 +460,7 @@ describe Whatsapp::EmbeddedSignupService do
)
# Stub the failing webhook request
stub_request(:post, "https://graph.facebook.com/v21.0/#{waba_id}/subscribed_apps")
stub_request(:post, "https://graph.facebook.com/#{api_version}/#{waba_id}/subscribed_apps")
.to_return(status: 400, body: { error: 'Webhook failed' }.to_json)
end
@@ -472,7 +474,7 @@ describe Whatsapp::EmbeddedSignupService do
describe '#exchange_code_for_token' do
context 'when token exchange is successful' do
before do
stub_request(:get, 'https://graph.facebook.com/v21.0/oauth/access_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: hash_including(
'client_id' => app_id,
'client_secret' => app_secret,
@@ -493,7 +495,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when response has no access token' do
before do
stub_request(:get, 'https://graph.facebook.com/v21.0/oauth/access_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/oauth/access_token")
.with(query: hash_including(
'client_id' => app_id,
'client_secret' => app_secret,
@@ -514,7 +516,7 @@ describe Whatsapp::EmbeddedSignupService do
describe '#fetch_phone_info_via_waba' do
before do
stub_request(:get, "https://graph.facebook.com/v21.0/#{waba_id}/phone_numbers")
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: hash_including('access_token' => access_token))
.to_return(
status: 200,
@@ -544,7 +546,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when specific phone number is not found' do
before do
stub_request(:get, "https://graph.facebook.com/v21.0/#{waba_id}/phone_numbers")
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: hash_including('access_token' => access_token))
.to_return(
status: 200,
@@ -571,7 +573,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when no phone numbers are available' do
before do
stub_request(:get, "https://graph.facebook.com/v21.0/#{waba_id}/phone_numbers")
stub_request(:get, "https://graph.facebook.com/#{api_version}/#{waba_id}/phone_numbers")
.with(query: hash_including('access_token' => access_token))
.to_return(
status: 200,
@@ -591,7 +593,7 @@ describe Whatsapp::EmbeddedSignupService do
describe '#validate_token_waba_access' do
context 'when token has access to WABA' do
before do
stub_request(:get, 'https://graph.facebook.com/v21.0/debug_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: hash_including(
'input_token' => access_token,
'access_token' => "#{app_id}|#{app_secret}"
@@ -619,7 +621,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when token validation fails' do
before do
stub_request(:get, 'https://graph.facebook.com/v21.0/debug_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: hash_including(
'input_token' => access_token,
'access_token' => "#{app_id}|#{app_secret}"
@@ -636,7 +638,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when token does not have access to WABA' do
before do
stub_request(:get, 'https://graph.facebook.com/v21.0/debug_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: hash_including(
'input_token' => access_token,
'access_token' => "#{app_id}|#{app_secret}"
@@ -666,7 +668,7 @@ describe Whatsapp::EmbeddedSignupService do
context 'when no WABA scope is found' do
before do
stub_request(:get, 'https://graph.facebook.com/v21.0/debug_token')
stub_request(:get, "https://graph.facebook.com/#{api_version}/debug_token")
.with(query: hash_including(
'input_token' => access_token,
'access_token' => "#{app_id}|#{app_secret}"