From 361b28cb03811fd588bb422ab639dff659da9726 Mon Sep 17 00:00:00 2001 From: Muhsin <12408980+muhsin-k@users.noreply.github.com> Date: Fri, 22 May 2026 11:56:21 +0400 Subject: [PATCH] chore: Use per-account feature flag for App Store channel --- .../api/v1/accounts/inboxes_controller.rb | 6 +----- app/controllers/dashboard_controller.rb | 1 - .../components/widgets/ChannelItem.vue | 6 +----- .../fetch_app_store_review_inboxes_job.rb | 9 +-------- app/jobs/inboxes/fetch_app_store_reviews_job.rb | 8 +------- .../app_store/send_on_app_store_service.rb | 2 +- config/features.yml | 6 +++--- config/installation_config.yml | 5 ----- ...ssage_reply_to_flag_for_channel_app_store.rb | 11 +++++++++++ db/schema.rb | 2 +- .../api/v1/accounts/inboxes_controller_spec.rb | 6 +----- .../fetch_app_store_review_inboxes_job_spec.rb | 6 +++--- .../inboxes/fetch_app_store_reviews_job_spec.rb | 5 ++--- spec/models/account_spec.rb | 17 +++++++++++++++++ .../app_store/send_on_app_store_service_spec.rb | 8 ++++---- 15 files changed, 47 insertions(+), 51 deletions(-) create mode 100644 db/migrate/20260522080000_repurpose_message_reply_to_flag_for_channel_app_store.rb diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 5c56c0d92..7d93af25a 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -31,7 +31,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController end def create - return render_app_store_feature_disabled if app_store_channel_requested? && !app_store_reviews_enabled? + return render_app_store_feature_disabled if app_store_channel_requested? && !Current.account.feature_enabled?(:channel_app_store) ActiveRecord::Base.transaction do channel = create_channel @@ -110,10 +110,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController render json: { message: 'App Store Reviews channel is not enabled for this account' }, status: :forbidden end - def app_store_reviews_enabled? - GlobalConfigService.load('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').to_s == 'true' - end - def update_inbox_working_hours @inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours] end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 3b0e981fc..b6df015f7 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -74,7 +74,6 @@ class DashboardController < ActionController::Base FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''), INSTAGRAM_APP_ID: GlobalConfigService.load('INSTAGRAM_APP_ID', ''), TIKTOK_APP_ID: GlobalConfigService.load('TIKTOK_APP_ID', ''), - ENABLE_APP_STORE_REVIEWS_CHANNEL: GlobalConfigService.load('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false'), FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v18.0'), WHATSAPP_APP_ID: GlobalConfigService.load('WHATSAPP_APP_ID', ''), WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''), diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index 07028378e..a8327a466 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -27,10 +27,6 @@ const hasTiktokConfigured = computed(() => { return window.chatwootConfig?.tiktokAppId; }); -const isAppStoreReviewsEnabled = computed(() => { - return window.chatwootConfig?.enableAppStoreReviewsChannel === 'true'; -}); - const isActive = computed(() => { const { key } = props.channel; if (Object.keys(props.enabledFeatures).length === 0) { @@ -57,7 +53,7 @@ const isActive = computed(() => { } if (key === 'app_store') { - return isAppStoreReviewsEnabled.value; + return props.enabledFeatures.channel_app_store; } if (key === 'voice') { diff --git a/app/jobs/inboxes/fetch_app_store_review_inboxes_job.rb b/app/jobs/inboxes/fetch_app_store_review_inboxes_job.rb index 2507e2956..fc7b079d3 100644 --- a/app/jobs/inboxes/fetch_app_store_review_inboxes_job.rb +++ b/app/jobs/inboxes/fetch_app_store_review_inboxes_job.rb @@ -2,19 +2,12 @@ class Inboxes::FetchAppStoreReviewInboxesJob < ApplicationJob queue_as :scheduled_jobs def perform - return unless app_store_reviews_enabled? - Inbox.where(channel_type: 'Channel::AppStore').find_each(batch_size: 100) do |inbox| next if inbox.account.suspended? + next unless inbox.account.feature_enabled?(:channel_app_store) next unless inbox.channel.sync_due? ::Inboxes::FetchAppStoreReviewsJob.perform_later(inbox.channel) end end - - private - - def app_store_reviews_enabled? - GlobalConfigService.load('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').to_s == 'true' - end end diff --git a/app/jobs/inboxes/fetch_app_store_reviews_job.rb b/app/jobs/inboxes/fetch_app_store_reviews_job.rb index dedd4e6aa..d90fc05d4 100644 --- a/app/jobs/inboxes/fetch_app_store_reviews_job.rb +++ b/app/jobs/inboxes/fetch_app_store_reviews_job.rb @@ -2,7 +2,7 @@ class Inboxes::FetchAppStoreReviewsJob < ApplicationJob queue_as :scheduled_jobs def perform(channel) - return unless app_store_reviews_enabled? + return unless channel.account.feature_enabled?(:channel_app_store) channel.fetch_reviews.each do |review_payload| ::AppStore::ReviewBuilder.new(review_payload: review_payload, channel: channel).perform @@ -14,10 +14,4 @@ class Inboxes::FetchAppStoreReviewsJob < ApplicationJob rescue StandardError => e ChatwootExceptionTracker.new(e, account: channel.account).capture_exception end - - private - - def app_store_reviews_enabled? - GlobalConfigService.load('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').to_s == 'true' - end end diff --git a/app/services/app_store/send_on_app_store_service.rb b/app/services/app_store/send_on_app_store_service.rb index 3545f83e5..c3f6a7f6b 100644 --- a/app/services/app_store/send_on_app_store_service.rb +++ b/app/services/app_store/send_on_app_store_service.rb @@ -21,7 +21,7 @@ class AppStore::SendOnAppStoreService < Base::SendOnChannelService end def validate_feature_enabled! - return if GlobalConfigService.load('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').to_s == 'true' + return if message.account.feature_enabled?(:channel_app_store) raise 'App Store Reviews channel is not enabled for this account.' end diff --git a/config/features.yml b/config/features.yml index 03105588b..231f402bd 100644 --- a/config/features.yml +++ b/config/features.yml @@ -108,10 +108,10 @@ display_name: Custom Tools enabled: false premium: true -- name: message_reply_to - display_name: Message Reply To +- name: channel_app_store + display_name: App Store Reviews Channel enabled: false - deprecated: true + chatwoot_internal: true - name: insert_article_in_reply display_name: Insert Article in Reply enabled: false diff --git a/config/installation_config.yml b/config/installation_config.yml index 6c5de1a06..e374d0948 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -277,11 +277,6 @@ display_title: 'Inactive WhatsApp Numbers' description: 'Comma-separated list of WhatsApp numbers that should be rejected with a 422 error' type: code -- name: ENABLE_APP_STORE_REVIEWS_CHANNEL - value: false - display_title: 'Enable App Store Reviews Channel' - description: 'Enable the App Store Reviews inbox channel' - type: boolean # ------- End of Chatwoot Internal Config for Cloud ----# # ------- Chatwoot Internal Config for Self Hosted ----# diff --git a/db/migrate/20260522080000_repurpose_message_reply_to_flag_for_channel_app_store.rb b/db/migrate/20260522080000_repurpose_message_reply_to_flag_for_channel_app_store.rb new file mode 100644 index 000000000..ca17582be --- /dev/null +++ b/db/migrate/20260522080000_repurpose_message_reply_to_flag_for_channel_app_store.rb @@ -0,0 +1,11 @@ +class RepurposeMessageReplyToFlagForChannelAppStore < ActiveRecord::Migration[7.1] + def up + # The message_reply_to flag (deprecated) has been renamed to channel_app_store. + # Disable it on any accounts that had message_reply_to enabled so the repurposed + # flag starts in its intended default-off state. + Account.feature_channel_app_store.find_each(batch_size: 100) do |account| + account.disable_features(:channel_app_store) + account.save!(validate: false) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 35afe0fb4..f3bae1192 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2026_05_20_090000) do +ActiveRecord::Schema[7.1].define(version: 2026_05_22_080000) do # These extensions should be enabled to support this database enable_extension "pg_stat_statements" enable_extension "pg_trgm" diff --git a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index ed8fea076..ed5871be9 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -435,9 +435,6 @@ RSpec.describe 'Inboxes API', type: :request do end it 'does not create an app store inbox when the feature is disabled' do - allow(GlobalConfigService).to receive(:load).and_call_original - allow(GlobalConfigService).to receive(:load).with('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').and_return('false') - post "/api/v1/accounts/#{account.id}/inboxes", headers: admin.create_new_auth_token, params: { name: 'App Store Reviews', @@ -452,8 +449,7 @@ RSpec.describe 'Inboxes API', type: :request do it 'creates an app store inbox when the feature is enabled' do app_store_client = instance_double(AppStoreConnect::Client) - allow(GlobalConfigService).to receive(:load).and_call_original - allow(GlobalConfigService).to receive(:load).with('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').and_return('true') + account.enable_features!(:channel_app_store) allow(AppStoreConnect::Client).to receive(:new).and_return(app_store_client) allow(app_store_client).to receive(:fetch_app).and_return( { diff --git a/spec/jobs/inboxes/fetch_app_store_review_inboxes_job_spec.rb b/spec/jobs/inboxes/fetch_app_store_review_inboxes_job_spec.rb index 410c1d1d0..894d4ca2a 100644 --- a/spec/jobs/inboxes/fetch_app_store_review_inboxes_job_spec.rb +++ b/spec/jobs/inboxes/fetch_app_store_review_inboxes_job_spec.rb @@ -10,8 +10,8 @@ RSpec.describe Inboxes::FetchAppStoreReviewInboxesJob do let(:suspended_channel) { create(:channel_app_store, account: suspended_account, last_synced_at: 2.hours.ago) } before do - allow(GlobalConfigService).to receive(:load).and_call_original - allow(GlobalConfigService).to receive(:load).with('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').and_return('true') + account.enable_features!(:channel_app_store) + suspended_account.enable_features!(:channel_app_store) end it 'enqueues the job' do @@ -32,7 +32,7 @@ RSpec.describe Inboxes::FetchAppStoreReviewInboxesJob do end it 'skips channels when the feature is disabled for the account' do - allow(GlobalConfigService).to receive(:load).with('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').and_return('false') + account.disable_features!(:channel_app_store) due_channel expect(Inboxes::FetchAppStoreReviewsJob).not_to receive(:perform_later) diff --git a/spec/jobs/inboxes/fetch_app_store_reviews_job_spec.rb b/spec/jobs/inboxes/fetch_app_store_reviews_job_spec.rb index 00bafc46f..e67c0d403 100644 --- a/spec/jobs/inboxes/fetch_app_store_reviews_job_spec.rb +++ b/spec/jobs/inboxes/fetch_app_store_reviews_job_spec.rb @@ -8,8 +8,7 @@ RSpec.describe Inboxes::FetchAppStoreReviewsJob do let(:review_builder) { instance_double(AppStore::ReviewBuilder, perform: true) } before do - allow(GlobalConfigService).to receive(:load).and_call_original - allow(GlobalConfigService).to receive(:load).with('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').and_return('true') + channel.account.enable_features!(:channel_app_store) end it 'enqueues the job' do @@ -43,7 +42,7 @@ RSpec.describe Inboxes::FetchAppStoreReviewsJob do end it 'does not fetch reviews when the feature is disabled for the account' do - allow(GlobalConfigService).to receive(:load).with('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').and_return('false') + channel.account.disable_features!(:channel_app_store) expect(channel).not_to receive(:fetch_reviews) diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 38ca9694a..82da9f961 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -50,6 +50,23 @@ RSpec.describe Account do end end + describe 'app store channel feature flag' do + let(:account) { create(:account) } + + it 'stores app store channel feature state in feature flags' do + expect(account.feature_enabled?(:channel_app_store)).to be false + + account.enable_features!(:channel_app_store) + + expect(account.reload.feature_enabled?(:channel_app_store)).to be true + expect(account.enabled_features).to include('channel_app_store' => true) + + account.disable_features!(:channel_app_store) + + expect(account.reload.feature_enabled?(:channel_app_store)).to be false + end + end + describe 'conversation unread counts feature flag' do let(:account) { create(:account) } let(:inbox) { create(:inbox, account: account) } diff --git a/spec/services/app_store/send_on_app_store_service_spec.rb b/spec/services/app_store/send_on_app_store_service_spec.rb index 821c4843f..854aa04e3 100644 --- a/spec/services/app_store/send_on_app_store_service_spec.rb +++ b/spec/services/app_store/send_on_app_store_service_spec.rb @@ -3,7 +3,8 @@ require 'rails_helper' RSpec.describe AppStore::SendOnAppStoreService do - let(:channel) { create(:channel_app_store) } + let(:account) { create(:account) } + let(:channel) { create(:channel_app_store, account: account) } let(:inbox) { channel.inbox } let(:contact) { create(:contact, account: inbox.account) } let(:contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: 'review-1') } @@ -12,8 +13,7 @@ RSpec.describe AppStore::SendOnAppStoreService do let(:exception_tracker) { instance_double(ChatwootExceptionTracker, capture_exception: true) } before do - allow(GlobalConfigService).to receive(:load).and_call_original - allow(GlobalConfigService).to receive(:load).with('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').and_return('true') + account.enable_features!(:channel_app_store) allow(Messages::StatusUpdateService).to receive(:new).and_return(status_update_service) allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker) end @@ -58,7 +58,7 @@ RSpec.describe AppStore::SendOnAppStoreService do end it 'marks the message as failed when the feature is disabled' do - allow(GlobalConfigService).to receive(:load).with('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').and_return('false') + account.disable_features!(:channel_app_store) message = create(:message, message_type: :outgoing, inbox: inbox, conversation: conversation, account: inbox.account, content: 'Thanks') described_class.new(message: message).perform