From 61d10044a04f84252e038c1f5a1cce287063435e Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:37:06 +0700 Subject: [PATCH] feat: Whatsapp embedded signup (#11612) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: iamsivin Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Sojan Jose --- .rubocop.yml | 2 +- .../integrations/notion_controller.rb | 2 +- .../notion/authorizations_controller.rb | 2 +- .../whatsapp/authorizations_controller.rb | 64 ++++ app/controllers/dashboard_controller.rb | 2 + .../notion/callbacks_controller.rb | 2 +- .../super_admin/app_configs_controller.rb | 5 +- .../dashboard/api/channel/whatsappChannel.js | 14 + app/javascript/dashboard/featureFlags.js | 1 + .../dashboard/i18n/locale/en/inboxMgmt.json | 29 ++ .../dashboard/settings/inbox/FinishSetup.vue | 16 +- .../dashboard/settings/inbox/Settings.vue | 14 +- .../settings/inbox/channels/Whatsapp.vue | 172 +++++++-- .../inbox/channels/WhatsappEmbeddedSignup.vue | 327 ++++++++++++++++++ .../dashboard/store/modules/inboxes.js | 14 + app/models/channel/whatsapp.rb | 38 ++ app/presenters/message_content_presenter.rb | 2 +- .../whatsapp/channel_creation_service.rb | 75 ++++ .../whatsapp/embedded_signup_service.rb | 44 +++ app/services/whatsapp/facebook_api_client.rb | 86 +++++ app/services/whatsapp/phone_info_service.rb | 57 +++ .../whatsapp/token_exchange_service.rb | 26 ++ .../whatsapp/token_validation_service.rb | 42 +++ .../whatsapp/webhook_setup_service.rb | 67 ++++ .../v1/accounts/search/articles.json.jbuilder | 2 +- .../callbacks/embedded_signup.json.jbuilder | 1 + app/views/layouts/vueapp.html.erb | 3 + config/features.yml | 3 + config/installation_config.yml | 20 ++ config/routes.rb | 4 + .../20250620120000_create_channel_voice.rb | 2 +- .../accounts/captain/scenarios_controller.rb | 2 +- .../app/helpers/super_admin/features.yml | 6 + enterprise/app/models/channel/voice.rb | 1 - .../app/policies/captain/scenario_policy.rb | 2 +- .../open_ai_message_builder_service.rb | 2 +- .../captain/scenarios/create.json.jbuilder | 2 +- .../captain/scenarios/index.json.jbuilder | 2 +- .../captain/scenarios/show.json.jbuilder | 2 +- .../captain/scenarios/update.json.jbuilder | 2 +- .../v1/models/captain/_scenario.json.jbuilder | 2 +- .../images/dashboard/channels/whatsapp.png | Bin 46629 -> 13525 bytes .../notion/authorization_controller_spec.rb | 2 +- .../authorizations_controller_spec.rb | 303 ++++++++++++++++ .../captain/scenarios_controller_spec.rb | 2 +- .../models/captain/scenario_spec.rb | 2 +- spec/models/channel/whatsapp_spec.rb | 61 ++++ spec/models/concerns/featurable_spec.rb | 57 +++ .../message_content_presenter_spec.rb | 2 +- spec/services/csat_survey_service_spec.rb | 2 +- .../linear/activity_message_service_spec.rb | 2 +- .../whatsapp/channel_creation_service_spec.rb | 119 +++++++ .../whatsapp/embedded_signup_service_spec.rb | 127 +++++++ .../whatsapp/facebook_api_client_spec.rb | 197 +++++++++++ .../whatsapp/phone_info_service_spec.rb | 147 ++++++++ .../whatsapp/token_exchange_service_spec.rb | 45 +++ .../whatsapp/token_validation_service_spec.rb | 99 ++++++ .../whatsapp/webhook_setup_service_spec.rb | 119 +++++++ 58 files changed, 2384 insertions(+), 63 deletions(-) create mode 100644 app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb create mode 100644 app/javascript/dashboard/api/channel/whatsappChannel.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/inbox/channels/WhatsappEmbeddedSignup.vue create mode 100644 app/services/whatsapp/channel_creation_service.rb create mode 100644 app/services/whatsapp/embedded_signup_service.rb create mode 100644 app/services/whatsapp/facebook_api_client.rb create mode 100644 app/services/whatsapp/phone_info_service.rb create mode 100644 app/services/whatsapp/token_exchange_service.rb create mode 100644 app/services/whatsapp/token_validation_service.rb create mode 100644 app/services/whatsapp/webhook_setup_service.rb create mode 100644 app/views/api/v1/accounts/whatsapp/callbacks/embedded_signup.json.jbuilder create mode 100644 spec/controllers/api/v1/accounts/whatsapp/authorizations_controller_spec.rb create mode 100644 spec/models/concerns/featurable_spec.rb create mode 100644 spec/services/whatsapp/channel_creation_service_spec.rb create mode 100644 spec/services/whatsapp/embedded_signup_service_spec.rb create mode 100644 spec/services/whatsapp/facebook_api_client_spec.rb create mode 100644 spec/services/whatsapp/phone_info_service_spec.rb create mode 100644 spec/services/whatsapp/token_exchange_service_spec.rb create mode 100644 spec/services/whatsapp/token_validation_service_spec.rb create mode 100644 spec/services/whatsapp/webhook_setup_service_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index 12e756af6..e30a71ee9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -283,7 +283,7 @@ Rails/RedundantActiveRecordAllMethod: Enabled: false Layout/TrailingEmptyLines: - Enabled: false + Enabled: true Style/SafeNavigationChainLength: Enabled: false diff --git a/app/controllers/api/v1/accounts/integrations/notion_controller.rb b/app/controllers/api/v1/accounts/integrations/notion_controller.rb index dff6ccece..ecf6bae6e 100644 --- a/app/controllers/api/v1/accounts/integrations/notion_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/notion_controller.rb @@ -11,4 +11,4 @@ class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::Bas def fetch_hook @hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'notion') end -end \ No newline at end of file +end diff --git a/app/controllers/api/v1/accounts/notion/authorizations_controller.rb b/app/controllers/api/v1/accounts/notion/authorizations_controller.rb index bb9b2f858..3e0f6586a 100644 --- a/app/controllers/api/v1/accounts/notion/authorizations_controller.rb +++ b/app/controllers/api/v1/accounts/notion/authorizations_controller.rb @@ -18,4 +18,4 @@ class Api::V1::Accounts::Notion::AuthorizationsController < Api::V1::Accounts::O render json: { success: false }, status: :unprocessable_entity end end -end \ No newline at end of file +end diff --git a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb new file mode 100644 index 000000000..e7a1f3fa6 --- /dev/null +++ b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb @@ -0,0 +1,64 @@ +class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController + before_action :validate_feature_enabled! + + # POST /api/v1/accounts/:account_id/whatsapp/authorization + # Handles the embedded signup callback data from the Facebook SDK + def create + validate_embedded_signup_params! + channel = process_embedded_signup + render_success_response(channel.inbox) + rescue StandardError => e + render_error_response(e) + end + + private + + def process_embedded_signup + service = Whatsapp::EmbeddedSignupService.new( + account: Current.account, + code: params[:code], + business_id: params[:business_id], + waba_id: params[:waba_id], + phone_number_id: params[:phone_number_id] + ) + service.perform + end + + def render_success_response(inbox) + render json: { + success: true, + id: inbox.id, + name: inbox.name, + channel_type: 'whatsapp' + } + end + + def render_error_response(error) + Rails.logger.error "[WHATSAPP AUTHORIZATION] Embedded signup error: #{error.message}" + Rails.logger.error error.backtrace.join("\n") + render json: { + success: false, + error: error.message + }, status: :unprocessable_entity + end + + def validate_feature_enabled! + return if Current.account.feature_whatsapp_embedded_signup? + + render json: { + success: false, + error: 'WhatsApp embedded signup is not enabled for this account' + }, status: :forbidden + end + + def validate_embedded_signup_params! + missing_params = [] + missing_params << 'code' if params[:code].blank? + missing_params << 'business_id' if params[:business_id].blank? + missing_params << 'waba_id' if params[:waba_id].blank? + + return if missing_params.empty? + + raise ArgumentError, "Required parameters are missing: #{missing_params.join(', ')}" + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 6a4ce2461..4a2df5ee5 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -67,6 +67,8 @@ class DashboardController < ActionController::Base FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), INSTAGRAM_APP_ID: GlobalConfigService.load('INSTAGRAM_APP_ID', ''), FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v17.0'), + WHATSAPP_APP_ID: GlobalConfigService.load('WHATSAPP_APP_ID', ''), + WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''), IS_ENTERPRISE: ChatwootApp.enterprise?, AZURE_APP_ID: GlobalConfigService.load('AZURE_APP_ID', ''), GIT_SHA: GIT_HASH diff --git a/app/controllers/notion/callbacks_controller.rb b/app/controllers/notion/callbacks_controller.rb index 94030fc8e..22dcf6d30 100644 --- a/app/controllers/notion/callbacks_controller.rb +++ b/app/controllers/notion/callbacks_controller.rb @@ -33,4 +33,4 @@ class Notion::CallbacksController < OauthCallbackController def notion_redirect_uri "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/settings/integrations/notion" end -end \ No newline at end of file +end diff --git a/app/controllers/super_admin/app_configs_controller.rb b/app/controllers/super_admin/app_configs_controller.rb index 771f9f28c..3972d5a28 100644 --- a/app/controllers/super_admin/app_configs_controller.rb +++ b/app/controllers/super_admin/app_configs_controller.rb @@ -39,8 +39,9 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController 'email' => ['MAILER_INBOUND_EMAIL_DOMAIN'], 'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET], 'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET], - 'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET], - 'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT] + '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_API_VERSION], + 'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET] } @allowed_configs = mapping.fetch(@config, %w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]) diff --git a/app/javascript/dashboard/api/channel/whatsappChannel.js b/app/javascript/dashboard/api/channel/whatsappChannel.js new file mode 100644 index 000000000..e1003b123 --- /dev/null +++ b/app/javascript/dashboard/api/channel/whatsappChannel.js @@ -0,0 +1,14 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class WhatsappChannel extends ApiClient { + constructor() { + super('whatsapp', { accountScoped: true }); + } + + createEmbeddedSignup(params) { + return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params); + } +} + +export default new WhatsappChannel(); diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index 98234539b..78109e82c 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -36,6 +36,7 @@ export const FEATURE_FLAGS = { REPORT_V4: 'report_v4', CHANNEL_INSTAGRAM: 'channel_instagram', CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team', + WHATSAPP_EMBEDDED_SIGNUP: 'whatsapp_embedded_signup', CAPTAIN_V2: 'captain_integration_v2', }; diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index be7a6f2f9..eb999a0e5 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -222,10 +222,17 @@ "DESC": "Start supporting your customers via WhatsApp.", "PROVIDERS": { "LABEL": "API Provider", + "WHATSAPP_EMBEDDED": "WhatsApp Business", "TWILIO": "Twilio", "WHATSAPP_CLOUD": "WhatsApp Cloud", + "WHATSAPP_CLOUD_DESC": "Quick setup through Meta", + "TWILIO_DESC": "Connect via Twilio credentials", "360_DIALOG": "360Dialog" }, + "SELECT_PROVIDER": { + "TITLE": "Select your API provider", + "DESCRIPTION": "Choose your WhatsApp provider. You can connect directly through Meta which requires no setup, or connect through Twilio using your account credentials." + }, "INBOX_NAME": { "LABEL": "Inbox Name", "PLACEHOLDER": "Please enter an inbox name", @@ -264,6 +271,28 @@ "WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token" }, "SUBMIT_BUTTON": "Create WhatsApp Channel", + "EMBEDDED_SIGNUP": { + "TITLE": "Quick Setup with Meta", + "DESC": "You will be redirected to Meta to log into your WhatsApp Business account. Having admin access will help make the setup smooth and easy.", + "BENEFITS": { + "TITLE": "Benefits of Embedded Signup:", + "EASY_SETUP": "No manual configuration required", + "SECURE_AUTH": "Secure OAuth based authentication", + "AUTO_CONFIG": "Automatic webhook and phone number configuration" + }, + "SUBMIT_BUTTON": "Connect with WhatsApp Business", + "AUTH_PROCESSING": "Authenticating with Meta", + "WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...", + "PROCESSING": "Setting up your WhatsApp Business Account", + "LOADING_SDK": "Loading Facebook SDK...", + "CANCELLED": "WhatsApp Signup was cancelled", + "SUCCESS_TITLE": "WhatsApp Business Account Connected!", + "WAITING_FOR_AUTH": "Waiting for authentication...", + "INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.", + "SIGNUP_ERROR": "Signup error occurred", + "AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.", + "SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured" + }, "API": { "ERROR_MESSAGE": "We were not able to save the WhatsApp channel" } diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue index f6d0707af..ac09448f6 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/FinishSetup.vue @@ -47,6 +47,13 @@ export default { this.currentInbox.provider === 'whatsapp_cloud' ); }, + // If the inbox is a whatsapp cloud inbox and the source is not embedded signup, then show the webhook details + shouldShowWhatsAppWebhookDetails() { + return ( + this.isWhatsAppCloudInbox && + this.currentInbox.provider_config?.source !== 'embedded_signup' + ); + }, message() { if (this.isATwilioInbox) { return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t( @@ -66,7 +73,7 @@ export default { )}`; } - if (this.isWhatsAppCloudInbox) { + if (this.isWhatsAppCloudInbox && this.shouldShowWhatsAppWebhookDetails) { return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t( 'INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.SUBTITLE' )}`; @@ -113,8 +120,11 @@ export default { :script="currentInbox.callback_webhook_url" /> -
-

+

+

{{ $t('INBOX_MGMT.ADD.WHATSAPP.API_CALLBACK.WEBHOOK_URL') }}

diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index a3f60c6d5..420d7b7b9 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -86,6 +86,12 @@ export default { selectedTabKey() { return this.tabs[this.selectedTabIndex]?.key; }, + shouldShowWhatsAppConfiguration() { + return !!( + this.isAWhatsAppCloudChannel && + this.inbox.provider_config?.source !== 'embedded_signup' + ); + }, whatsAppAPIProviderName() { if (this.isAWhatsAppCloudChannel) { return this.$t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WHATSAPP_CLOUD'); @@ -137,7 +143,7 @@ export default { this.isALineChannel || this.isAPIInbox || (this.isAnEmailChannel && !this.inbox.provider) || - this.isAWhatsAppChannel || + this.shouldShowWhatsAppConfiguration || this.isAWebWidgetInbox ) { visibleToAllChannelTabs = [ @@ -383,7 +389,7 @@ export default {