Merge branch 'feat/app-store-reviews' of github.com:chatwoot/chatwoot into feat/app-store-reviews
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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', ''),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ----#
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user