diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 9b8f7b747..5c56c0d92 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -31,6 +31,8 @@ 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? + ActiveRecord::Base.transaction do channel = create_channel @inbox = Current.account.inboxes.build( @@ -100,6 +102,18 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController %w[web_widget api email line telegram whatsapp sms app_store] end + def app_store_channel_requested? + permitted_params.dig(:channel, :type) == 'app_store' + end + + def render_app_store_feature_disabled + 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 b6df015f7..3b0e981fc 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -74,6 +74,7 @@ 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 54c9c707d..07028378e 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -27,6 +27,10 @@ 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) { @@ -52,6 +56,10 @@ const isActive = computed(() => { return props.enabledFeatures.channel_tiktok && hasTiktokConfigured.value; } + if (key === 'app_store') { + return isAppStoreReviewsEnabled.value; + } + if (key === 'voice') { return props.enabledFeatures.channel_voice; } @@ -66,7 +74,6 @@ const isActive = computed(() => { 'line', 'instagram', 'tiktok', - 'app_store', 'voice', ].includes(key); }); 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 d056c63b8..2507e2956 100644 --- a/app/jobs/inboxes/fetch_app_store_review_inboxes_job.rb +++ b/app/jobs/inboxes/fetch_app_store_review_inboxes_job.rb @@ -2,6 +2,8 @@ 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.channel.sync_due? @@ -9,4 +11,10 @@ class Inboxes::FetchAppStoreReviewInboxesJob < ApplicationJob ::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 bd1fa79a7..dedd4e6aa 100644 --- a/app/jobs/inboxes/fetch_app_store_reviews_job.rb +++ b/app/jobs/inboxes/fetch_app_store_reviews_job.rb @@ -2,6 +2,8 @@ class Inboxes::FetchAppStoreReviewsJob < ApplicationJob queue_as :scheduled_jobs def perform(channel) + return unless app_store_reviews_enabled? + channel.fetch_reviews.each do |review_payload| ::AppStore::ReviewBuilder.new(review_payload: review_payload, channel: channel).perform rescue StandardError => e @@ -12,4 +14,10 @@ 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 3949a2b08..3545f83e5 100644 --- a/app/services/app_store/send_on_app_store_service.rb +++ b/app/services/app_store/send_on_app_store_service.rb @@ -6,6 +6,7 @@ class AppStore::SendOnAppStoreService < Base::SendOnChannelService end def perform_reply + validate_feature_enabled! validate_message_support! source_id = channel.reply_to_review(review_id, reply_content, response_id: existing_response_id) message.update!(source_id: source_id) if source_id.present? @@ -19,6 +20,12 @@ class AppStore::SendOnAppStoreService < Base::SendOnChannelService raise 'Sending attachments is not supported for App Store reviews.' if message.attachments.any? end + def validate_feature_enabled! + return if GlobalConfigService.load('ENABLE_APP_STORE_REVIEWS_CHANNEL', 'false').to_s == 'true' + + raise 'App Store Reviews channel is not enabled for this account.' + end + def review_id message.conversation.contact_inbox.source_id end diff --git a/config/installation_config.yml b/config/installation_config.yml index e374d0948..6c5de1a06 100644 --- a/config/installation_config.yml +++ b/config/installation_config.yml @@ -277,6 +277,11 @@ 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/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb index 9e03e2587..ed8fea076 100644 --- a/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/inboxes_controller_spec.rb @@ -434,6 +434,48 @@ RSpec.describe 'Inboxes API', type: :request do expect(response.body).to include('+123456789') 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', + channel: { type: 'app_store', app_id: '123456789', issuer_id: SecureRandom.uuid, key_id: 'KEY123', + private_key: OpenSSL::PKey::EC.generate('prime256v1').to_pem } }, + as: :json + + expect(response).to have_http_status(:forbidden) + expect(response.parsed_body['message']).to eq('App Store Reviews channel is not enabled for this account') + end + + 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') + allow(AppStoreConnect::Client).to receive(:new).and_return(app_store_client) + allow(app_store_client).to receive(:fetch_app).and_return( + { + 'attributes' => { + 'name' => 'Chatwoot', + 'bundleId' => 'com.chatwoot.app' + } + } + ) + + post "/api/v1/accounts/#{account.id}/inboxes", + headers: admin.create_new_auth_token, + params: { name: 'App Store Reviews', + channel: { type: 'app_store', app_id: '123456789', issuer_id: SecureRandom.uuid, key_id: 'KEY123', + private_key: OpenSSL::PKey::EC.generate('prime256v1').to_pem } }, + as: :json + + expect(response).to have_http_status(:success) + expect(response.body).to include('App Store Reviews') + expect(Channel::AppStore.last.app_name).to eq('Chatwoot') + end + it 'creates the webwidget inbox that allow messages after conversation is resolved' do post "/api/v1/accounts/#{account.id}/inboxes", headers: admin.create_new_auth_token, 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 6eaadd34f..410c1d1d0 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 @@ -9,6 +9,11 @@ RSpec.describe Inboxes::FetchAppStoreReviewInboxesJob do let(:fresh_channel) { create(:channel_app_store, account: account, last_synced_at: 10.minutes.ago) } 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') + end + it 'enqueues the job' do expect { described_class.perform_later }.to have_enqueued_job(described_class) .on_queue('scheduled_jobs') @@ -25,4 +30,13 @@ RSpec.describe Inboxes::FetchAppStoreReviewInboxesJob do described_class.perform_now 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') + due_channel + + expect(Inboxes::FetchAppStoreReviewsJob).not_to receive(:perform_later) + + described_class.perform_now + end end 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 065a494dd..00bafc46f 100644 --- a/spec/jobs/inboxes/fetch_app_store_reviews_job_spec.rb +++ b/spec/jobs/inboxes/fetch_app_store_reviews_job_spec.rb @@ -7,6 +7,11 @@ RSpec.describe Inboxes::FetchAppStoreReviewsJob do let(:review_payload) { { 'review' => { 'id' => 'review-1' }, 'response' => nil } } 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') + end + it 'enqueues the job' do expect { described_class.perform_later(channel) }.to have_enqueued_job(described_class) .with(channel) @@ -36,4 +41,12 @@ RSpec.describe Inboxes::FetchAppStoreReviewsJob do expect(exception_tracker).to have_received(:capture_exception) expect(channel.reload.last_synced_at).to be_present 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') + + expect(channel).not_to receive(:fetch_reviews) + + described_class.perform_now(channel) + end end 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 1124633e2..821c4843f 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 @@ -12,6 +12,8 @@ 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') allow(Messages::StatusUpdateService).to receive(:new).and_return(status_update_service) allow(ChatwootExceptionTracker).to receive(:new).and_return(exception_tracker) end @@ -54,5 +56,19 @@ RSpec.describe AppStore::SendOnAppStoreService do ) expect(exception_tracker).to have_received(:capture_exception) 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') + message = create(:message, message_type: :outgoing, inbox: inbox, conversation: conversation, account: inbox.account, content: 'Thanks') + + described_class.new(message: message).perform + + expect(Messages::StatusUpdateService).to have_received(:new).with( + message, + 'failed', + 'App Store Reviews channel is not enabled for this account.' + ) + expect(exception_tracker).to have_received(:capture_exception) + end end end