diff --git a/.gitignore b/.gitignore
index c64fb5c1b..bb0df62a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,3 +94,4 @@ yarn-debug.log*
.vscode
.claude/settings.local.json
.cursor
+CLAUDE.local.md
diff --git a/AGENTS.md b/AGENTS.md
index ad374799c..e3b022a2e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -55,4 +55,21 @@
## Ruby Best Practices
-- Use compact `module/class` definitions; avoid nested styles
\ No newline at end of file
+- Use compact `module/class` definitions; avoid nested styles
+
+## Enterprise Edition Notes
+
+- Chatwoot has an Enterprise overlay under `enterprise/` that extends/overrides OSS code.
+- When you add or modify core functionality, always check for corresponding files in `enterprise/` and keep behavior compatible.
+- Follow the Enterprise development practices documented here:
+ - https://chatwoot.help/hc/handbook/articles/developing-enterprise-edition-features-38
+
+Practical checklist for any change impacting core logic or public APIs
+- Search for related files in both trees before editing (e.g., `rg -n "FooService|ControllerName|ModelName" app enterprise`).
+- If adding new endpoints, services, or models, consider whether Enterprise needs:
+ - An override (e.g., `enterprise/app/...`), or
+ - An extension point (e.g., `prepend_mod_with`, hooks, configuration) to avoid hard forks.
+- Avoid hardcoding instance- or plan-specific behavior in OSS; prefer configuration, feature flags, or extension points consumed by Enterprise.
+- Keep request/response contracts stable across OSS and Enterprise; update both sets of routes/controllers when introducing new APIs.
+- When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift.
+- Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable.
diff --git a/Gemfile b/Gemfile
index 269614d8b..a3b3c0ae8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -108,7 +108,7 @@ gem 'google-cloud-translate-v3', '>= 0.7.0'
##-- apm and error monitoring ---#
# loaded only when environment variables are set.
# ref application.rb
-gem 'ddtrace', require: false
+gem 'datadog', '~> 2.0', require: false
gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
@@ -179,7 +179,10 @@ gem 'reverse_markdown'
gem 'iso-639'
gem 'ruby-openai'
-gem 'ai-agents', '>= 0.2.1'
+gem 'ai-agents', '>= 0.4.3'
+
+# TODO: Move this gem as a dependency of ai-agents
+gem 'ruby_llm-schema'
gem 'shopify_api'
diff --git a/Gemfile.lock b/Gemfile.lock
index 405300a7c..be258f524 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -25,35 +25,35 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (7.1.5.1)
- actionpack (= 7.1.5.1)
- activesupport (= 7.1.5.1)
+ actioncable (7.1.5.2)
+ actionpack (= 7.1.5.2)
+ activesupport (= 7.1.5.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
- actionmailbox (7.1.5.1)
- actionpack (= 7.1.5.1)
- activejob (= 7.1.5.1)
- activerecord (= 7.1.5.1)
- activestorage (= 7.1.5.1)
- activesupport (= 7.1.5.1)
+ actionmailbox (7.1.5.2)
+ actionpack (= 7.1.5.2)
+ activejob (= 7.1.5.2)
+ activerecord (= 7.1.5.2)
+ activestorage (= 7.1.5.2)
+ activesupport (= 7.1.5.2)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
- actionmailer (7.1.5.1)
- actionpack (= 7.1.5.1)
- actionview (= 7.1.5.1)
- activejob (= 7.1.5.1)
- activesupport (= 7.1.5.1)
+ actionmailer (7.1.5.2)
+ actionpack (= 7.1.5.2)
+ actionview (= 7.1.5.2)
+ activejob (= 7.1.5.2)
+ activesupport (= 7.1.5.2)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2)
- actionpack (7.1.5.1)
- actionview (= 7.1.5.1)
- activesupport (= 7.1.5.1)
+ actionpack (7.1.5.2)
+ actionview (= 7.1.5.2)
+ activesupport (= 7.1.5.2)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
@@ -61,38 +61,38 @@ GEM
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- actiontext (7.1.5.1)
- actionpack (= 7.1.5.1)
- activerecord (= 7.1.5.1)
- activestorage (= 7.1.5.1)
- activesupport (= 7.1.5.1)
+ actiontext (7.1.5.2)
+ actionpack (= 7.1.5.2)
+ activerecord (= 7.1.5.2)
+ activestorage (= 7.1.5.2)
+ activesupport (= 7.1.5.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.1.5.1)
- activesupport (= 7.1.5.1)
+ actionview (7.1.5.2)
+ activesupport (= 7.1.5.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
active_record_query_trace (1.8)
- activejob (7.1.5.1)
- activesupport (= 7.1.5.1)
+ activejob (7.1.5.2)
+ activesupport (= 7.1.5.2)
globalid (>= 0.3.6)
- activemodel (7.1.5.1)
- activesupport (= 7.1.5.1)
- activerecord (7.1.5.1)
- activemodel (= 7.1.5.1)
- activesupport (= 7.1.5.1)
+ activemodel (7.1.5.2)
+ activesupport (= 7.1.5.2)
+ activerecord (7.1.5.2)
+ activemodel (= 7.1.5.2)
+ activesupport (= 7.1.5.2)
timeout (>= 0.4.0)
activerecord-import (2.1.0)
activerecord (>= 4.2)
- activestorage (7.1.5.1)
- actionpack (= 7.1.5.1)
- activejob (= 7.1.5.1)
- activerecord (= 7.1.5.1)
- activesupport (= 7.1.5.1)
+ activestorage (7.1.5.2)
+ actionpack (= 7.1.5.2)
+ activejob (= 7.1.5.2)
+ activerecord (= 7.1.5.2)
+ activesupport (= 7.1.5.2)
marcel (~> 1.0)
- activesupport (7.1.5.1)
+ activesupport (7.1.5.2)
base64
benchmark (>= 0.3)
bigdecimal
@@ -126,7 +126,7 @@ GEM
jbuilder (~> 2)
rails (>= 4.2, < 7.2)
selectize-rails (~> 0.6)
- ai-agents (0.2.1)
+ ai-agents (0.4.3)
ruby_llm (~> 1.3)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
@@ -155,10 +155,10 @@ GEM
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
- base64 (0.2.0)
+ base64 (0.3.0)
bcrypt (3.1.20)
- benchmark (0.4.0)
- bigdecimal (3.1.9)
+ benchmark (0.4.1)
+ bigdecimal (3.2.2)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
@@ -194,10 +194,14 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
- date (3.4.1)
- ddtrace (0.48.0)
- ffi (~> 1.0)
+ datadog (2.19.0)
+ datadog-ruby_core_source (~> 3.4, >= 3.4.1)
+ libdatadog (~> 18.1.0.1.0)
+ libddwaf (~> 1.24.1.0.3)
+ logger
msgpack
+ datadog-ruby_core_source (3.4.1)
+ date (3.4.1)
debug (1.8.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
@@ -444,6 +448,16 @@ GEM
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
+ libdatadog (18.1.0.1.0)
+ libdatadog (18.1.0.1.0-x86_64-linux)
+ libddwaf (1.24.1.0.3)
+ ffi (~> 1.0)
+ libddwaf (1.24.1.0.3-arm64-darwin)
+ ffi (~> 1.0)
+ libddwaf (1.24.1.0.3-x86_64-darwin)
+ ffi (~> 1.0)
+ libddwaf (1.24.1.0.3-x86_64-linux)
+ ffi (~> 1.0)
line-bot-api (1.28.0)
lint_roller (1.1.0)
liquid (5.4.0)
@@ -599,20 +613,20 @@ GEM
rackup (1.0.1)
rack (< 3)
webrick
- rails (7.1.5.1)
- actioncable (= 7.1.5.1)
- actionmailbox (= 7.1.5.1)
- actionmailer (= 7.1.5.1)
- actionpack (= 7.1.5.1)
- actiontext (= 7.1.5.1)
- actionview (= 7.1.5.1)
- activejob (= 7.1.5.1)
- activemodel (= 7.1.5.1)
- activerecord (= 7.1.5.1)
- activestorage (= 7.1.5.1)
- activesupport (= 7.1.5.1)
+ rails (7.1.5.2)
+ actioncable (= 7.1.5.2)
+ actionmailbox (= 7.1.5.2)
+ actionmailer (= 7.1.5.2)
+ actionpack (= 7.1.5.2)
+ actiontext (= 7.1.5.2)
+ actionview (= 7.1.5.2)
+ activejob (= 7.1.5.2)
+ activemodel (= 7.1.5.2)
+ activerecord (= 7.1.5.2)
+ activestorage (= 7.1.5.2)
+ activesupport (= 7.1.5.2)
bundler (>= 1.15.0)
- railties (= 7.1.5.1)
+ railties (= 7.1.5.2)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -620,9 +634,9 @@ GEM
rails-html-sanitizer (1.6.1)
loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
- railties (7.1.5.1)
- actionpack (= 7.1.5.1)
- activesupport (= 7.1.5.1)
+ railties (7.1.5.2)
+ actionpack (= 7.1.5.2)
+ activesupport (= 7.1.5.2)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -720,7 +734,7 @@ GEM
ruby2ruby (2.5.0)
ruby_parser (~> 3.1)
sexp_processor (~> 4.6)
- ruby_llm (1.3.1)
+ ruby_llm (1.5.1)
base64
event_stream_parser (~> 1)
faraday (>= 1.10.0)
@@ -729,6 +743,7 @@ GEM
faraday-retry (>= 1)
marcel (~> 1.0)
zeitwerk (~> 2)
+ ruby_llm-schema (0.1.0)
ruby_parser (3.20.0)
sexp_processor (~> 4.16)
sass (3.7.4)
@@ -910,7 +925,7 @@ DEPENDENCIES
administrate (>= 0.20.1)
administrate-field-active_storage (>= 1.0.3)
administrate-field-belongs_to_search (>= 0.9.0)
- ai-agents (>= 0.2.1)
+ ai-agents (>= 0.4.3)
annotate
attr_extras
audited (~> 5.4, >= 5.4.1)
@@ -927,7 +942,7 @@ DEPENDENCIES
commonmarker
csv-safe
database_cleaner
- ddtrace
+ datadog (~> 2.0)
debug (~> 1.8)
devise (>= 4.9.4)
devise-secure_password!
@@ -1004,6 +1019,7 @@ DEPENDENCIES
rubocop-rails
rubocop-rspec
ruby-openai
+ ruby_llm-schema
scout_apm
scss_lint
seed_dump
diff --git a/app/controllers/api/v1/accounts/assignment_policies/inboxes_controller.rb b/app/controllers/api/v1/accounts/assignment_policies/inboxes_controller.rb
new file mode 100644
index 000000000..ac1d0a712
--- /dev/null
+++ b/app/controllers/api/v1/accounts/assignment_policies/inboxes_controller.rb
@@ -0,0 +1,20 @@
+class Api::V1::Accounts::AssignmentPolicies::InboxesController < Api::V1::Accounts::BaseController
+ before_action :fetch_assignment_policy
+ before_action -> { check_authorization(AssignmentPolicy) }
+
+ def index
+ @inboxes = @assignment_policy.inboxes
+ end
+
+ private
+
+ def fetch_assignment_policy
+ @assignment_policy = Current.account.assignment_policies.find(
+ params[:assignment_policy_id]
+ )
+ end
+
+ def permitted_params
+ params.permit(:assignment_policy_id)
+ end
+end
diff --git a/app/controllers/api/v1/accounts/assignment_policies_controller.rb b/app/controllers/api/v1/accounts/assignment_policies_controller.rb
new file mode 100644
index 000000000..1807d6afb
--- /dev/null
+++ b/app/controllers/api/v1/accounts/assignment_policies_controller.rb
@@ -0,0 +1,36 @@
+class Api::V1::Accounts::AssignmentPoliciesController < Api::V1::Accounts::BaseController
+ before_action :fetch_assignment_policy, only: [:show, :update, :destroy]
+ before_action :check_authorization
+
+ def index
+ @assignment_policies = Current.account.assignment_policies
+ end
+
+ def show; end
+
+ def create
+ @assignment_policy = Current.account.assignment_policies.create!(assignment_policy_params)
+ end
+
+ def update
+ @assignment_policy.update!(assignment_policy_params)
+ end
+
+ def destroy
+ @assignment_policy.destroy!
+ head :ok
+ end
+
+ private
+
+ def fetch_assignment_policy
+ @assignment_policy = Current.account.assignment_policies.find(params[:id])
+ end
+
+ def assignment_policy_params
+ params.require(:assignment_policy).permit(
+ :name, :description, :assignment_order, :conversation_priority,
+ :fair_distribution_limit, :fair_distribution_window, :enabled
+ )
+ end
+end
diff --git a/app/controllers/api/v1/accounts/inboxes/assignment_policies_controller.rb b/app/controllers/api/v1/accounts/inboxes/assignment_policies_controller.rb
new file mode 100644
index 000000000..cf52951a5
--- /dev/null
+++ b/app/controllers/api/v1/accounts/inboxes/assignment_policies_controller.rb
@@ -0,0 +1,46 @@
+class Api::V1::Accounts::Inboxes::AssignmentPoliciesController < Api::V1::Accounts::BaseController
+ before_action :fetch_inbox
+ before_action :fetch_assignment_policy, only: [:create]
+ before_action -> { check_authorization(AssignmentPolicy) }
+ before_action :validate_assignment_policy, only: [:show, :destroy]
+
+ def show
+ @assignment_policy = @inbox.assignment_policy
+ end
+
+ def create
+ # There should be only one assignment policy for an inbox.
+ # If there is a new request to add an assignment policy, we will
+ # delete the old one and attach the new policy
+ remove_inbox_assignment_policy
+ @inbox_assignment_policy = @inbox.create_inbox_assignment_policy!(assignment_policy: @assignment_policy)
+ @assignment_policy = @inbox.assignment_policy
+ end
+
+ def destroy
+ remove_inbox_assignment_policy
+ head :ok
+ end
+
+ private
+
+ def remove_inbox_assignment_policy
+ @inbox.inbox_assignment_policy&.destroy
+ end
+
+ def fetch_inbox
+ @inbox = Current.account.inboxes.find(permitted_params[:inbox_id])
+ end
+
+ def fetch_assignment_policy
+ @assignment_policy = Current.account.assignment_policies.find(permitted_params[:assignment_policy_id])
+ end
+
+ def permitted_params
+ params.permit(:assignment_policy_id, :inbox_id)
+ end
+
+ def validate_assignment_policy
+ return render_not_found_error(I18n.t('errors.assignment_policy.not_found')) unless @inbox.assignment_policy
+ end
+end
diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb
index 18538e842..af96441f8 100644
--- a/app/controllers/api/v1/accounts/portals_controller.rb
+++ b/app/controllers/api/v1/accounts/portals_controller.rb
@@ -26,9 +26,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
@portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present?
# @portal.custom_domain = parsed_custom_domain
process_attached_logo if params[:blob_id].present?
- rescue StandardError => e
- Rails.logger.error e
- render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
+ rescue ActiveRecord::RecordInvalid => e
+ render_record_invalid(e)
end
end
diff --git a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb
index e7a1f3fa6..3e7d876c3 100644
--- a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb
+++ b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb
@@ -1,8 +1,10 @@
class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController
before_action :validate_feature_enabled!
+ before_action :fetch_and_validate_inbox, if: -> { params[:inbox_id].present? }
# POST /api/v1/accounts/:account_id/whatsapp/authorization
- # Handles the embedded signup callback data from the Facebook SDK
+ # Handles both initial authorization and reauthorization
+ # If inbox_id is present in params, it performs reauthorization
def create
validate_embedded_signup_params!
channel = process_embedded_signup
@@ -16,21 +18,42 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts:
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]
+ params: params.permit(:code, :business_id, :waba_id, :phone_number_id).to_h.symbolize_keys,
+ inbox_id: params[:inbox_id]
)
service.perform
end
- def render_success_response(inbox)
+ def fetch_and_validate_inbox
+ @inbox = Current.account.inboxes.find(params[:inbox_id])
+ validate_reauthorization_required
+ end
+
+ def validate_reauthorization_required
+ return if @inbox.channel.reauthorization_required? || can_upgrade_to_embedded_signup?
+
render json: {
+ success: false,
+ message: I18n.t('inbox.reauthorization.not_required')
+ }, status: :unprocessable_entity
+ end
+
+ def can_upgrade_to_embedded_signup?
+ channel = @inbox.channel
+ return false unless channel.provider == 'whatsapp_cloud'
+
+ true
+ end
+
+ def render_success_response(inbox)
+ response = {
success: true,
id: inbox.id,
name: inbox.name,
channel_type: 'whatsapp'
}
+ response[:message] = I18n.t('inbox.reauthorization.success') if params[:inbox_id].present?
+ render json: response
end
def render_error_response(error)
diff --git a/app/controllers/platform/api/v1/accounts_controller.rb b/app/controllers/platform/api/v1/accounts_controller.rb
index e11cf9d4a..4521930a6 100644
--- a/app/controllers/platform/api/v1/accounts_controller.rb
+++ b/app/controllers/platform/api/v1/accounts_controller.rb
@@ -1,4 +1,11 @@
class Platform::Api::V1::AccountsController < PlatformController
+ def index
+ @resources = @platform_app.platform_app_permissibles
+ .where(permissible_type: 'Account')
+ .includes(:permissible)
+ .map(&:permissible)
+ end
+
def show; end
def create
diff --git a/app/controllers/slack_uploads_controller.rb b/app/controllers/slack_uploads_controller.rb
index 127e77649..6f157c7d5 100644
--- a/app/controllers/slack_uploads_controller.rb
+++ b/app/controllers/slack_uploads_controller.rb
@@ -17,7 +17,12 @@ class SlackUploadsController < ApplicationController
end
def blob_url
- url_for(@blob.representation(resize_to_fill: [250, nil]))
+ # Only generate representations for images
+ if @blob.content_type.start_with?('image/')
+ url_for(@blob.representation(resize_to_fill: [250, nil]))
+ else
+ url_for(@blob)
+ end
end
def avatar_url
diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb
index 455828228..d607ba151 100644
--- a/app/controllers/twilio/callback_controller.rb
+++ b/app/controllers/twilio/callback_controller.rb
@@ -30,7 +30,8 @@ class Twilio::CallbackController < ApplicationController
:NumMedia,
:Latitude,
:Longitude,
- :MessageType
+ :MessageType,
+ :ProfileName
)
end
end
diff --git a/app/finders/notification_finder.rb b/app/finders/notification_finder.rb
index ccfe470a0..e1958827a 100644
--- a/app/finders/notification_finder.rb
+++ b/app/finders/notification_finder.rb
@@ -15,7 +15,13 @@ class NotificationFinder
end
def unread_count
- @notifications.where(read_at: nil).count
+ if type_included?('read')
+ # If we're including read notifications, filter to unread
+ @notifications.where(read_at: nil).count
+ else
+ # Already filtered to unread notifications, just count
+ @notifications.count
+ end
end
def count
@@ -27,7 +33,7 @@ class NotificationFinder
def set_up
find_all_notifications
filter_snoozed_notifications
- fitler_read_notifications
+ filter_read_notifications
end
def find_all_notifications
@@ -38,7 +44,7 @@ class NotificationFinder
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
end
- def fitler_read_notifications
+ def filter_read_notifications
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
end
diff --git a/app/helpers/reporting_event_helper.rb b/app/helpers/reporting_event_helper.rb
index e08b5691c..f0a419cc8 100644
--- a/app/helpers/reporting_event_helper.rb
+++ b/app/helpers/reporting_event_helper.rb
@@ -18,12 +18,25 @@ module ReportingEventHelper
end
def last_non_human_activity(conversation)
- # check if a handoff event already exists
- handoff_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_handoff').last
+ # Try to get either a handoff or reopened event first
+ # These will always take precedence over any other activity
+ # Also, any of these events can happen at any time in the course of a conversation lifecycle.
+ # So we pick the latest event
+ event = ReportingEvent.where(
+ conversation_id: conversation.id,
+ name: %w[conversation_bot_handoff conversation_opened]
+ ).order(event_end_time: :desc).first
- # if a handoff exists, last non human activity is when the handoff ended,
- # otherwise it's when the conversation was created
- handoff_event&.event_end_time || conversation.created_at
+ return event.event_end_time if event&.event_end_time
+
+ # Fallback to bot resolved event
+ # Because this will be closest to the most accurate activity instead of conversation.created_at
+ bot_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_resolved').last
+
+ return bot_event.event_end_time if bot_event&.event_end_time
+
+ # If no events found, return conversation creation time
+ conversation.created_at
end
private
diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue
index 0fbb20ea9..8da7e7476 100644
--- a/app/javascript/dashboard/App.vue
+++ b/app/javascript/dashboard/App.vue
@@ -137,7 +137,6 @@ export default {
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
id="app"
class="flex flex-col w-full h-screen min-h-0"
- :class="{ 'app-rtl--wrapper': isRTL }"
:dir="isRTL ? 'rtl' : 'ltr'"
>
diff --git a/app/javascript/dashboard/api/channel/whatsappChannel.js b/app/javascript/dashboard/api/channel/whatsappChannel.js
index e1003b123..8f51f4878 100644
--- a/app/javascript/dashboard/api/channel/whatsappChannel.js
+++ b/app/javascript/dashboard/api/channel/whatsappChannel.js
@@ -9,6 +9,13 @@ class WhatsappChannel extends ApiClient {
createEmbeddedSignup(params) {
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params);
}
+
+ reauthorizeWhatsApp({ inboxId, ...params }) {
+ return axios.post(`${this.baseUrl()}/whatsapp/authorization`, {
+ ...params,
+ inbox_id: inboxId,
+ });
+ }
}
export default new WhatsappChannel();
diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
index 304e55347..05507fc89 100644
--- a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
+++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
@@ -76,8 +76,8 @@ const campaignStatus = computed(() => {
const inboxName = computed(() => props.inbox?.name || '');
const inboxIcon = computed(() => {
- const { phone_number: phoneNumber, channel_type: type } = props.inbox;
- return getInboxIconByType(type, phoneNumber);
+ const { medium, channel_type: type } = props.inbox;
+ return getInboxIconByType(type, medium);
});
diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue
index 12a789fee..f4301375d 100644
--- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue
+++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue
@@ -38,11 +38,13 @@ const handleClose = () => emit('close');
-
- {{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
-
-
+
+
+ {{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue
index df76ae901..0babd11ec 100644
--- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue
+++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue
@@ -9,6 +9,7 @@ import Input from 'dashboard/components-next/input/Input.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
import TagMultiSelectComboBox from 'dashboard/components-next/combobox/TagMultiSelectComboBox.vue';
+import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
const emit = defineEmits(['submit', 'cancel']);
@@ -18,7 +19,9 @@ const formState = {
uiFlags: useMapGetter('campaigns/getUIFlags'),
labels: useMapGetter('labels/getLabels'),
inboxes: useMapGetter('inboxes/getWhatsAppInboxes'),
- getWhatsAppTemplates: useMapGetter('inboxes/getWhatsAppTemplates'),
+ getFilteredWhatsAppTemplates: useMapGetter(
+ 'inboxes/getFilteredWhatsAppTemplates'
+ ),
};
const initialState = {
@@ -30,7 +33,7 @@ const initialState = {
};
const state = reactive({ ...initialState });
-const processedParams = ref({});
+const templateParserRef = ref(null);
const rules = {
title: { required, minLength: minLength(1) },
@@ -67,7 +70,7 @@ const inboxOptions = computed(() =>
const templateOptions = computed(() => {
if (!state.inboxId) return [];
- const templates = formState.getWhatsAppTemplates.value(state.inboxId);
+ const templates = formState.getFilteredWhatsAppTemplates.value(state.inboxId);
return templates.map(template => {
// Create a more user-friendly label from template name
const friendlyName = template.name
@@ -88,26 +91,6 @@ const selectedTemplate = computed(() => {
?.template;
});
-const templateString = computed(() => {
- if (!selectedTemplate.value) return '';
- try {
- return (
- selectedTemplate.value.components?.find(
- component => component.type === 'BODY'
- )?.text || ''
- );
- } catch (error) {
- return '';
- }
-});
-
-const processedString = computed(() => {
- if (!templateString.value) return '';
- return templateString.value.replace(/{{([^}]+)}}/g, (match, variable) => {
- return processedParams.value[variable] || `{{${variable}}}`;
- });
-});
-
const getErrorMessage = (field, errorKey) => {
const baseKey = 'CAMPAIGN.WHATSAPP.CREATE.FORM';
return v$.value[field].$error ? t(`${baseKey}.${errorKey}.ERROR`) : '';
@@ -122,8 +105,7 @@ const formErrors = computed(() => ({
}));
const hasRequiredTemplateParams = computed(() => {
- const params = Object.values(processedParams.value);
- return params.length === 0 || params.every(param => param.trim() !== '');
+ return templateParserRef.value?.v$?.$invalid === false || true;
});
const isSubmitDisabled = computed(
@@ -135,32 +117,18 @@ const formatToUTCString = localDateTime =>
const resetState = () => {
Object.assign(state, initialState);
- processedParams.value = {};
v$.value.$reset();
};
const handleCancel = () => emit('cancel');
-const generateVariables = () => {
- const matchedVariables = templateString.value.match(/{{([^}]+)}}/g);
- if (!matchedVariables) {
- processedParams.value = {};
- return;
- }
-
- const finalVars = matchedVariables.map(match => match.replace(/{{|}}/g, ''));
- processedParams.value = finalVars.reduce((acc, variable) => {
- acc[variable] = processedParams.value[variable] || '';
- return acc;
- }, {});
-};
-
const prepareCampaignDetails = () => {
// Find the selected template to get its content
const currentTemplate = selectedTemplate.value;
+ const parserData = templateParserRef.value;
// Extract template content - this should be the template message body
- const templateContent = templateString.value;
+ const templateContent = parserData?.renderedTemplate || '';
// Prepare template_params object with the same structure as used in contacts
const templateParams = {
@@ -168,7 +136,7 @@ const prepareCampaignDetails = () => {
namespace: currentTemplate?.namespace || '',
category: currentTemplate?.category || 'UTILITY',
language: currentTemplate?.language || 'en_US',
- processed_params: processedParams.value,
+ processed_params: parserData?.processedParams || {},
};
return {
@@ -198,15 +166,6 @@ watch(
() => state.inboxId,
() => {
state.templateId = null;
- processedParams.value = {};
- }
-);
-
-// Generate variables when template changes
-watch(
- () => state.templateId,
- () => {
- generateVariables();
}
);
@@ -254,62 +213,12 @@ watch(
-
-
+
-
-
- {{ selectedTemplate.name }}
-
-
- {{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.LANGUAGE') }}:
- {{ selectedTemplate.language || 'en' }}
-
-
-
-
-
-
- {{ processedString }}
-
-
-
-
-
- {{ t('CAMPAIGN.WHATSAPP.CREATE.FORM.TEMPLATE.CATEGORY') }}:
- {{ selectedTemplate.category || 'UTILITY' }}
-
-
-
-
-
-
-
-
+ ref="templateParserRef"
+ :template="selectedTemplate"
+ />