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