diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/VoiceConfigurationPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/VoiceConfigurationPage.vue
index c8bc3124d..cf954c353 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/VoiceConfigurationPage.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/VoiceConfigurationPage.vue
@@ -23,6 +23,7 @@ export default {
voiceEnabled: this.inbox.voice_enabled || false,
apiKeySid: this.inbox.api_key_sid || '',
apiKeySecret: '',
+ isUpdating: false,
};
},
computed: {
@@ -57,6 +58,7 @@ export default {
},
methods: {
async updateVoiceSettings() {
+ this.isUpdating = true;
try {
const channelPayload = { voice_enabled: this.voiceEnabled };
@@ -72,9 +74,12 @@ export default {
formData: false,
channel: channelPayload,
});
+ this.apiKeySecret = '';
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
+ } finally {
+ this.isUpdating = false;
}
},
},
@@ -135,6 +140,7 @@ export default {
diff --git a/enterprise/app/models/enterprise/channel/twilio_sms.rb b/enterprise/app/models/enterprise/channel/twilio_sms.rb
index d58a87bf9..07b6a9d04 100644
--- a/enterprise/app/models/enterprise/channel/twilio_sms.rb
+++ b/enterprise/app/models/enterprise/channel/twilio_sms.rb
@@ -8,6 +8,7 @@ module Enterprise::Channel::TwilioSms
validate :voice_requires_phone_number, if: :voice_enabled?
before_validation :provision_twiml_app, on: :create, if: :voice_enabled?
before_validation :provision_twiml_app_on_update, on: :update, if: :voice_enabled_changed_to_true?
+ after_update :teardown_voice, if: :voice_disabled?
end
end
@@ -56,6 +57,23 @@ module Enterprise::Channel::TwilioSms
voice_enabled? && voice_enabled_changed?
end
+ def voice_disabled?
+ !voice_enabled? && voice_enabled_previously_changed?
+ end
+
+ def teardown_voice
+ delete_twiml_app if twiml_app_sid.present?
+ rescue StandardError => e
+ Rails.logger.error("TWILIO_VOICE_TEARDOWN_ERROR: #{e.class} #{e.message} phone=#{phone_number} account=#{account_id}")
+ ensure
+ update_columns(twiml_app_sid: nil, api_key_secret: nil)
+ end
+
+ def delete_twiml_app
+ twilio_client = Twilio::REST::Client.new(account_sid, auth_token)
+ twilio_client.applications(twiml_app_sid).delete
+ end
+
def provision_twiml_app
service = ::Twilio::VoiceWebhookSetupService.new(channel: self)
self.twiml_app_sid = service.perform
diff --git a/spec/enterprise/models/channel/twilio_sms_voice_spec.rb b/spec/enterprise/models/channel/twilio_sms_voice_spec.rb
index 55134ab59..50b484e58 100644
--- a/spec/enterprise/models/channel/twilio_sms_voice_spec.rb
+++ b/spec/enterprise/models/channel/twilio_sms_voice_spec.rb
@@ -60,4 +60,39 @@ RSpec.describe Channel::TwilioSms do
expect(channel.twiml_app_sid).to eq(twiml_app_sid)
end
end
+
+ describe 'teardown on disable' do
+ let(:channel) { create(:channel_twilio_sms, :with_voice, account: account) }
+ let(:app_context) { double('app_context') }
+ let(:twilio_client) { instance_double(Twilio::REST::Client) }
+
+ before do
+ allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
+ allow(twilio_client).to receive(:applications).with(channel.twiml_app_sid).and_return(app_context)
+ allow(app_context).to receive(:delete)
+ end
+
+ it 'deletes the TwiML app and clears voice credentials' do
+ original_twiml_sid = channel.twiml_app_sid
+ channel.update!(voice_enabled: false)
+
+ expect(twilio_client).to have_received(:applications).with(original_twiml_sid)
+ expect(app_context).to have_received(:delete)
+ expect(channel.reload.twiml_app_sid).to be_nil
+ expect(channel.reload.api_key_secret).to be_nil
+ end
+
+ it 'preserves api_key_sid' do
+ channel.update!(voice_enabled: false)
+ expect(channel.reload.api_key_sid).to be_present
+ end
+
+ it 'does not fail if Twilio API errors' do
+ allow(app_context).to receive(:delete).and_raise(Twilio::REST::RestError.new('Not found', double(status_code: 404, body: {})))
+
+ expect { channel.update!(voice_enabled: false) }.not_to raise_error
+ expect(channel.reload.twiml_app_sid).to be_nil
+ expect(channel.reload.api_key_secret).to be_nil
+ end
+ end
end