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 {