@@ -44,7 +53,7 @@
<% if ChatwootHub.pricing_plan != 'community' && User.count > ChatwootHub.pricing_plan_quantity %>
-
You have <%= User.count %> agents. Please add more licenses.
+
You have <%= User.count %> agents. Please add more licenses to add more users.
<% end %>
diff --git a/config/features.yml b/config/features.yml
index 7eabf28d8..529ad37d9 100644
--- a/config/features.yml
+++ b/config/features.yml
@@ -11,6 +11,7 @@
enabled: false
- name: disable_branding
enabled: false
+ premium: true
- name: email_continuity_on_api_channel
enabled: false
- name: help_center
@@ -55,8 +56,10 @@
enabled: false
- name: audit_logs
enabled: false
+ premium: true
- name: response_bot
enabled: false
+ premium: true
- name: message_reply_to
enabled: false
- name: insert_article_in_reply
diff --git a/config/installation_config.yml b/config/installation_config.yml
index 0778ab922..2643461e2 100644
--- a/config/installation_config.yml
+++ b/config/installation_config.yml
@@ -1,5 +1,20 @@
-# if you don't specify locked attribute, the default value will be true
-# which means the particular config will be locked
+# This file contains all the installation wide configuration which controls various settings in Chatwoot
+# This is internal config and should not be modified by the user directly in database
+# Chatwoot might override and modify these values during the upgrade process
+# Configs which can be modified by the user are available in the dashboard under appropriate UI
+#
+# name: the name of the config referenced in the code
+# value: the value of the config
+# display_title: the title of the config displayed in the dashboard UI
+# description: the description of the config displayed in the dashboard UI
+# 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
+
+
+
+# ------- Branding Related Config ------- #
- name: INSTALLATION_NAME
value: 'Chatwoot'
display_title: 'Installation Name'
@@ -41,32 +56,20 @@
display_title: 'Chatwoot Metadata'
description: 'Display default Chatwoot metadata like favicons and upgrade warnings'
type: boolean
-- name: MAILER_INBOUND_EMAIL_DOMAIN
- value:
- locked: false
-- name: MAILER_SUPPORT_EMAIL
- value:
+# ------- End of Branding Related Config ------- #
+
+
+
+# ------- Signup & Account Related Config ------- #
+- name: ENABLE_ACCOUNT_SIGNUP
+ display_title: 'Enable Account Signup'
+ value: false
+ description: 'Allow users to signup for new accounts'
locked: false
+ type: boolean
- name: CREATE_NEW_ACCOUNT_FROM_DASHBOARD
value: false
- locked: false
-- name: INSTALLATION_EVENTS_WEBHOOK_URL
- value:
- locked: false
-- name: CHATWOOT_INBOX_TOKEN
- value:
- locked: false
-- name: CHATWOOT_INBOX_HMAC_KEY
- value:
- locked: false
-- name: API_CHANNEL_NAME
- value:
-- name: API_CHANNEL_THUMBNAIL
- value:
-- name: ANALYTICS_TOKEN
- value:
-- name: DIRECT_UPLOADS_ENABLED
- value: false
+ description: 'Allow users to create new accounts from the dashboard'
locked: false
- name: HCAPTCHA_SITE_KEY
value:
@@ -74,34 +77,107 @@
- name: HCAPTCHA_SERVER_KEY
value:
locked: false
-- name: LOGOUT_REDIRECT_LINK
- value: /app/login
+- name: INSTALLATION_EVENTS_WEBHOOK_URL
+ value:
+ display_title: 'System events Webhook URL'
+ description: 'The URL to which the system events like new accounts created will be sent'
locked: false
-- name: DISABLE_USER_PROFILE_UPDATE
+- name: DIRECT_UPLOADS_ENABLED
+ type: boolean
value: false
+ description: 'Enable direct uploads to cloud storage'
+ locked: false
+# ------- End of Account Related Config ------- #
+
+
+
+# ------- Email Related Config ------- #
+- name: MAILER_INBOUND_EMAIL_DOMAIN
+ value:
+ description: 'The domain name to be used for generating conversation continuity emails (reply+id@domain.com)'
+ locked: false
+- name: MAILER_SUPPORT_EMAIL
+ value:
+ locked: false
+# ------- End of Email Related Config ------- #
+
+
+# ------- Facebook Channel Related Config ------- #
+- name: FB_APP_ID
+ display_title: 'Facebook App ID'
+ locked: false
+- name: FB_VERIFY_TOKEN
+ display_title: 'Facebook Verify Token'
+ description: 'The verify token used for Facebook Messenger Webhook'
+ locked: false
+- name: FB_APP_SECRET
+ display_title: 'Facebook App Secret'
+ locked: false
+- name: IG_VERIFY_TOKEN
+ display_title: 'Instagram Verify Token'
+ description: 'The verify token used for Instagram Webhook'
locked: false
- name: ENABLE_MESSENGER_CHANNEL_HUMAN_AGENT
+ display_title: 'Enable human agent'
value: false
locked: false
-- name: CSML_BOT_HOST
+ description: 'Enable human agent for messenger channel for longer message back period. Needs additional app approval: https://developers.facebook.com/docs/features-reference/human-agent/'
+ type: boolean
+# ------- End of Facebook Channel Related Config ------- #
+
+# ------- Chatwoot Internal Config for Cloud ----#
+- name: CHATWOOT_INBOX_TOKEN
value:
+ description: 'The Chatwoot Inbox Token for Contact Support in Cloud'
locked: false
-- name: CSML_BOT_API_KEY
+- name: CHATWOOT_INBOX_HMAC_KEY
value:
+ description: 'The Chatwoot Inbox HMAC Key for Contact Support in Cloud'
locked: false
- name: CHATWOOT_CLOUD_PLANS
value:
+ description: 'Config to store stripe plans for cloud'
- name: DEPLOYMENT_ENV
value: self-hosted
-- name: CSML_EDITOR_HOST
+ description: 'The deployment environment of the installation, to differentiate between Chatwoot cloud and self-hosted'
+- name: ANALYTICS_TOKEN
value:
+ description: 'The June.so analytics token for Chatwoot cloud'
+# ------- End of Chatwoot Internal Config for Cloud ----#
+
+
+# ------- Chatwoot Internal Config for Self Hosted ----#
- name: INSTALLATION_PRICING_PLAN
value: 'community'
+ description: 'The pricing plan for the installation, retrieved from the billing API'
- name: INSTALLATION_PRICING_PLAN_QUANTITY
value: 0
+ description: 'The number of licenses purchased for the installation, retrieved from the billing API'
- name: CHATWOOT_SUPPORT_WEBSITE_TOKEN
value:
+ description: 'The Chatwoot website token, used to identify the Chatwoot inbox and display the "Contact Support" option on the billing page'
- name: CHATWOOT_SUPPORT_SCRIPT_URL
value:
+ description: 'The Chatwoot script base URL, to display the "Contact Support" option on the billing page'
- name: CHATWOOT_SUPPORT_IDENTIFIER_HASH
value:
+ description: 'The Chatwoot identifier hash, to validate the contact in the live chat window.'
+# ------- End of Chatwoot Internal Config for Self Hosted ----#
+
+## ------ Configs added for enterprise clients ------ ##
+- name: API_CHANNEL_NAME
+ value:
+ description: 'Custom name for the API channel'
+- name: API_CHANNEL_THUMBNAIL
+ value:
+ description: 'Custom thumbnail for the API channel'
+- name: LOGOUT_REDIRECT_LINK
+ value: /app/login
+ locked: false
+ description: 'Redirect to a different link after logout'
+- name: DISABLE_USER_PROFILE_UPDATE
+ value: false
+ locked: false
+ description: 'Disable rendering profile update page for users'
+
+## ------ End of Configs added for enterprise clients ------ ##
\ No newline at end of file
diff --git a/enterprise/app/helpers/super_admin/features.yml b/enterprise/app/helpers/super_admin/features.yml
index 41e5af426..a3413a325 100644
--- a/enterprise/app/helpers/super_admin/features.yml
+++ b/enterprise/app/helpers/super_admin/features.yml
@@ -1,3 +1,5 @@
+# TODO: Move this values to features.yml itself
+# No need to replicate the same values in two places
custom_branding:
name: 'Custom Branding'
description: 'Apply your own branding to this installation.'
diff --git a/enterprise/app/jobs/enterprise/internal/check_new_versions_job.rb b/enterprise/app/jobs/enterprise/internal/check_new_versions_job.rb
new file mode 100644
index 000000000..b1f5d62ec
--- /dev/null
+++ b/enterprise/app/jobs/enterprise/internal/check_new_versions_job.rb
@@ -0,0 +1,30 @@
+module Enterprise::Internal::CheckNewVersionsJob
+ def perform
+ super
+ update_plan_info
+ reconcile_premium_config_and_features
+ end
+
+ private
+
+ def update_plan_info
+ return if @instance_info.blank?
+
+ update_installation_config(key: 'INSTALLATION_PRICING_PLAN', value: @instance_info['plan'])
+ update_installation_config(key: 'INSTALLATION_PRICING_PLAN_QUANTITY', value: @instance_info['plan_quantity'])
+ update_installation_config(key: 'CHATWOOT_SUPPORT_WEBSITE_TOKEN', value: @instance_info['chatwoot_support_website_token'])
+ update_installation_config(key: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH', value: @instance_info['chatwoot_support_identifier_hash'])
+ update_installation_config(key: 'CHATWOOT_SUPPORT_SCRIPT_URL', value: @instance_info['chatwoot_support_script_url'])
+ end
+
+ def update_installation_config(key:, value:)
+ config = InstallationConfig.find_or_initialize_by(name: key)
+ config.value = value
+ config.locked = true
+ config.save!
+ end
+
+ def reconcile_premium_config_and_features
+ Internal::ReconcilePlanConfigService.new.perform
+ end
+end
diff --git a/enterprise/app/services/internal/reconcile_plan_config_service.rb b/enterprise/app/services/internal/reconcile_plan_config_service.rb
new file mode 100644
index 000000000..04414ea68
--- /dev/null
+++ b/enterprise/app/services/internal/reconcile_plan_config_service.rb
@@ -0,0 +1,60 @@
+class Internal::ReconcilePlanConfigService
+ def perform
+ remove_premium_config_reset_warning
+ return if ChatwootHub.pricing_plan != 'community'
+
+ create_premium_config_reset_warning if premium_config_reset_required?
+
+ # We will have this enabled in the future
+ # reconcile_premium_config
+ reconcile_premium_features
+ end
+
+ private
+
+ def config_path
+ @config_path ||= Rails.root.join('enterprise/config')
+ end
+
+ def premium_config
+ @premium_config ||= YAML.safe_load(File.read("#{config_path}/premium_installation_config.yml")).freeze
+ end
+
+ def remove_premium_config_reset_warning
+ Redis::Alfred.delete(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING)
+ end
+
+ def create_premium_config_reset_warning
+ Redis::Alfred.set(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING, true)
+ end
+
+ def premium_config_reset_required?
+ premium_config.any? do |config|
+ config = config.with_indifferent_access
+ existing_config = InstallationConfig.find_by(name: config[:name])
+ existing_config&.value != config[:value] if existing_config.present?
+ end
+ end
+
+ def reconcile_premium_config
+ premium_config.each do |config|
+ new_config = config.with_indifferent_access
+ existing_config = InstallationConfig.find_by(name: new_config[:name])
+ next if existing_config&.value == new_config[:value]
+
+ existing_config&.update!(value: new_config[:value])
+ end
+ end
+
+ def premium_features
+ @premium_features ||= YAML.safe_load(File.read("#{config_path}/premium_features.yml")).freeze
+ end
+
+ def reconcile_premium_features
+ Account.find_in_batches do |accounts|
+ accounts.each do |account|
+ account.disable_features!(*premium_features)
+ end
+ end
+ end
+end
diff --git a/enterprise/config/premium_features.yml b/enterprise/config/premium_features.yml
new file mode 100644
index 000000000..9628e1da4
--- /dev/null
+++ b/enterprise/config/premium_features.yml
@@ -0,0 +1,4 @@
+# List of the premium features in EE edition
+- disable_branding
+- audit_logs
+- response_bot
diff --git a/enterprise/config/premium_installation_config.yml b/enterprise/config/premium_installation_config.yml
new file mode 100644
index 000000000..0f102b083
--- /dev/null
+++ b/enterprise/config/premium_installation_config.yml
@@ -0,0 +1,22 @@
+# ------- Branding Related Config ------- #
+- name: INSTALLATION_NAME
+ value: 'Chatwoot'
+- name: LOGO_THUMBNAIL
+ value: '/brand-assets/logo_thumbnail.svg'
+- name: LOGO
+ value: '/brand-assets/logo.svg'
+- name: LOGO_DARK
+ value: '/brand-assets/logo_dark.svg'
+- name: BRAND_URL
+ value: 'https://www.chatwoot.com'
+- name: WIDGET_BRAND_URL
+ value: 'https://www.chatwoot.com'
+- name: BRAND_NAME
+ value: 'Chatwoot'
+- name: TERMS_URL
+ value: 'https://www.chatwoot.com/terms-of-service'
+- name: PRIVACY_URL
+ value: 'https://www.chatwoot.com/privacy-policy'
+- name: DISPLAY_MANIFEST
+ value: true
+# ------- End of Branding Related Config ------- #
diff --git a/lib/redis/redis_keys.rb b/lib/redis/redis_keys.rb
index 535d3774d..f4a1520b1 100644
--- a/lib/redis/redis_keys.rb
+++ b/lib/redis/redis_keys.rb
@@ -28,6 +28,7 @@ module Redis::RedisKeys
## Internal Installation related keys
CHATWOOT_INSTALLATION_ONBOARDING = 'CHATWOOT_INSTALLATION_ONBOARDING'.freeze
+ CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING = 'CHATWOOT_CONFIG_RESET_WARNING'.freeze
LATEST_CHATWOOT_VERSION = 'LATEST_CHATWOOT_VERSION'.freeze
# Check if a message create with same source-id is in progress?
MESSAGE_SOURCE_KEY = 'MESSAGE_SOURCE_KEY::%
s'.freeze
diff --git a/spec/controllers/super_admin/app_config_controller_spec.rb b/spec/controllers/super_admin/app_config_controller_spec.rb
index 338c3e611..f517009eb 100644
--- a/spec/controllers/super_admin/app_config_controller_spec.rb
+++ b/spec/controllers/super_admin/app_config_controller_spec.rb
@@ -16,9 +16,9 @@ RSpec.describe 'Super Admin Application Config API', type: :request do
it 'shows the app_config page' do
sign_in(super_admin, scope: :super_admin)
- get '/super_admin/app_config'
+ get '/super_admin/app_config?config=facebook'
expect(response).to have_http_status(:success)
- expect(response.body).to include(config.name)
+ expect(response.body).to include(config.value)
end
end
end
@@ -34,7 +34,7 @@ RSpec.describe 'Super Admin Application Config API', type: :request do
context 'when it is an aunthenticated super admin' do
it 'shows the app_config page' do
sign_in(super_admin, scope: :super_admin)
- post '/super_admin/app_config', params: { app_config: { FB_APP_ID: 'FB_APP_ID' } }
+ post '/super_admin/app_config?config=facebook', params: { app_config: { FB_APP_ID: 'FB_APP_ID' } }
expect(response).to have_http_status(:found)
expect(response).to redirect_to(super_admin_settings_path)
diff --git a/spec/enterprise/jobs/enterprise/internal/check_new_versions_job_spec.rb b/spec/enterprise/jobs/enterprise/internal/check_new_versions_job_spec.rb
new file mode 100644
index 000000000..48664c904
--- /dev/null
+++ b/spec/enterprise/jobs/enterprise/internal/check_new_versions_job_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+RSpec.describe Internal::CheckNewVersionsJob do
+ subject(:job) { described_class.perform_now }
+
+ let(:reconsile_premium_config_service) { instance_double(Internal::ReconcilePlanConfigService) }
+
+ before do
+ allow(Internal::ReconcilePlanConfigService).to receive(:new).and_return(reconsile_premium_config_service)
+ allow(reconsile_premium_config_service).to receive(:perform)
+ allow(Rails.env).to receive(:production?).and_return(true)
+ end
+
+ it 'updates the plan info' do
+ data = { 'version' => '1.2.3', 'plan' => 'enterprise', 'plan_quantity' => 1, 'chatwoot_support_website_token' => '123',
+ 'chatwoot_support_identifier_hash' => '123', 'chatwoot_support_script_url' => '123' }
+ allow(ChatwootHub).to receive(:sync_with_hub).and_return(data)
+ job
+ expect(InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN').value).to eq 'enterprise'
+ expect(InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN_QUANTITY').value).to eq 1
+ expect(InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_WEBSITE_TOKEN').value).to eq '123'
+ expect(InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH').value).to eq '123'
+ expect(InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_SCRIPT_URL').value).to eq '123'
+ end
+
+ it 'calls Internal::ReconcilePlanConfigService' do
+ data = { 'version' => '1.2.3' }
+ allow(ChatwootHub).to receive(:sync_with_hub).and_return(data)
+ job
+ expect(reconsile_premium_config_service).to have_received(:perform)
+ end
+end
diff --git a/spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb b/spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb
new file mode 100644
index 000000000..ee8b25557
--- /dev/null
+++ b/spec/enterprise/services/internal/reconcile_plan_config_service_spec.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+RSpec.describe Internal::ReconcilePlanConfigService do
+ describe '#perform' do
+ let(:service) { described_class.new }
+
+ context 'when pricing plan is community' do
+ before do
+ allow(ChatwootHub).to receive(:pricing_plan).and_return('community')
+ end
+
+ it 'disables the premium features for accounts' do
+ account = create(:account)
+ account.enable_features!('disable_branding', 'audit_logs', 'response_bot')
+ response_bot_account = create(:account)
+ response_bot_account.enable_features!('response_bot')
+ disable_branding_account = create(:account)
+ disable_branding_account.enable_features!('disable_branding')
+ service.perform
+ expect(account.reload.enabled_features.keys).not_to include('response_bot', 'disable_branding', 'audit_logs')
+ expect(response_bot_account.reload.enabled_features.keys).not_to include('response_bot')
+ expect(disable_branding_account.reload.enabled_features.keys).not_to include('disable_branding')
+ end
+
+ it 'creates a premium config reset warning if config was modified' do
+ create(:installation_config, name: 'INSTALLATION_NAME', value: 'custom-name')
+ service.perform
+ expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING)).to eq('true')
+ end
+
+ it 'will not create a premium config reset warning if config is not modified' do
+ create(:installation_config, name: 'INSTALLATION_NAME', value: 'Chatwoot')
+ service.perform
+ expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING)).to be_nil
+ end
+
+ # To be enabled in the future when method is uncommented
+
+ # it 'updates the premium configs to default' do
+ # create(:installation_config, name: 'INSTALLATION_NAME', value: 'custom-name')
+ # create(:installation_config, name: 'LOGO', value: '/custom-path/logo.svg')
+ # service.perform
+ # expect(InstallationConfig.find_by(name: 'INSTALLATION_NAME').value).to eq('Chatwoot')
+ # expect(InstallationConfig.find_by(name: 'LOGO').value).to eq('/brand-assets/logo.svg')
+ # end
+ end
+
+ context 'when pricing plan is not community' do
+ before do
+ allow(ChatwootHub).to receive(:pricing_plan).and_return('enterprise')
+ end
+
+ it 'unset premium config warning on upgrade' do
+ Redis::Alfred.set(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING, true)
+ service.perform
+ expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_CONFIG_RESET_WARNING)).to be_nil
+ end
+
+ it 'does not disable the premium features for accounts' do
+ account = create(:account)
+ account.enable_features!('disable_branding', 'audit_logs', 'response_bot')
+ response_bot_account = create(:account)
+ response_bot_account.enable_features!('response_bot')
+ disable_branding_account = create(:account)
+ disable_branding_account.enable_features!('disable_branding')
+ service.perform
+ expect(account.reload.enabled_features.keys).to include('response_bot', 'disable_branding', 'audit_logs')
+ expect(response_bot_account.reload.enabled_features.keys).to include('response_bot')
+ expect(disable_branding_account.reload.enabled_features.keys).to include('disable_branding')
+ end
+
+ it 'does not update the LOGO config' do
+ create(:installation_config, name: 'INSTALLATION_NAME', value: 'custom-name')
+ create(:installation_config, name: 'LOGO', value: '/custom-path/logo.svg')
+ service.perform
+ expect(InstallationConfig.find_by(name: 'INSTALLATION_NAME').value).to eq('custom-name')
+ expect(InstallationConfig.find_by(name: 'LOGO').value).to eq('/custom-path/logo.svg')
+ end
+ end
+ end
+end
diff --git a/spec/jobs/internal/check_new_versions_job_spec.rb b/spec/jobs/internal/check_new_versions_job_spec.rb
index 3d36c4ba7..82424e202 100644
--- a/spec/jobs/internal/check_new_versions_job_spec.rb
+++ b/spec/jobs/internal/check_new_versions_job_spec.rb
@@ -4,7 +4,7 @@ RSpec.describe Internal::CheckNewVersionsJob do
subject(:job) { described_class.perform_now }
it 'updates the latest chatwoot version in redis' do
- data = { 'version' => '1.2.3' }.to_json
+ data = { 'version' => '1.2.3' }
allow(Rails.env).to receive(:production?).and_return(true)
allow(ChatwootHub).to receive(:sync_with_hub).and_return(data)
job