feat: Add voice calling as a capability on Twilio SMS channel(Enterprise) (#13963)

Voice calling is now a capability on the existing TwilioSms rather than
a separate Voice model. A single Twilio phone number handles both SMS
and voice calls through one inbox.
Fixes
https://linear.app/chatwoot/issue/CW-6683/add-voice-calling-as-a-capability-on-twilio-sms-channel
and https://linear.app/chatwoot/issue/PLA-120/add-the-support-for-sms

**What changed**
- Replaced Channel::Voice with voice_enabled flag on Channel::TwilioSms
- Added voice_enabled, twiml_app_sid, api_key_secret columns to
channel_twilio_sms table
  - Dropped channel_voice table (no production data)
- All voice logic lives in Enterprise layer via
prepend_mod_with('Channel::TwilioSms')
- Added Voice settings tab on Twilio SMS inbox settings to
enable/disable voice
  - Validates Twilio number voice capability before provisioning
- Teardown service cleans up TwiML app and credentials when voice is
disabled
- Frontend voice detection uses isVoiceCallEnabled() /
getVoiceCallProvider() helpers — extensible to future providers
  - Gated by channel_voice feature flag

**How to test**

1. Enable feature flag:
Account.find(<id>).enable_features('channel_voice')
2. Create voice inbox: Inboxes → Voice tile → enter Twilio credentials →
verify incoming/outgoing calls and SMS work
3. Enable voice on existing SMS inbox: Inboxes → select Twilio SMS inbox
→ Voice tab → toggle on → provide API key credentials → verify calls
work
4. Disable voice: Voice tab → toggle off → verify TwiML app is deleted,
credentials cleared, SMS still works
5. Re-enable voice: Toggle on again → must provide api_key_secret again
→ new TwiML app provisioned

---------

Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
This commit is contained in:
Muhsin Keloth
2026-04-29 11:32:19 +04:00
committed by GitHub
co-authored by Muhsin
parent f8f0caf443
commit 0e122188e9
50 changed files with 667 additions and 418 deletions
@@ -3,7 +3,7 @@ import { computed, ref, useAttrs } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useMapGetter, useStore } from 'dashboard/composables/store';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
import { isVoiceCallEnabled } from 'dashboard/helper/inbox';
import { useAlert } from 'dashboard/composables';
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper';
import { useCallsStore } from 'dashboard/stores/calls';
@@ -34,9 +34,7 @@ const inboxesList = useMapGetter('inboxes/getInboxes');
const contactsUiFlags = useMapGetter('contacts/getUIFlags');
const voiceInboxes = computed(() =>
(inboxesList.value || []).filter(
inbox => inbox.channel_type === INBOX_TYPES.VOICE
)
(inboxesList.value || []).filter(isVoiceCallEnabled)
);
const hasVoiceInboxes = computed(() => voiceInboxes.value.length > 0);
@@ -8,8 +8,6 @@ import { useEventListener } from '@vueuse/core';
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import FileUpload from 'vue-upload-component';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
import Button from 'dashboard/components-next/button/Button.vue';
import WhatsAppOptions from './WhatsAppOptions.vue';
import ContentTemplateSelector from './ContentTemplateSelector.vue';
@@ -30,6 +28,7 @@ const props = defineProps({
isDropdownActive: { type: Boolean, default: false },
messageSignature: { type: String, default: '' },
inboxId: { type: Number, default: null },
voiceEnabled: { type: Boolean, default: false },
});
const emit = defineEmits([
@@ -82,11 +81,9 @@ const isRegularMessageMode = computed(() => {
return !props.isWhatsappInbox && !props.isTwilioWhatsAppInbox;
});
const isVoiceInbox = computed(() => props.channelType === INBOX_TYPES.VOICE);
const shouldShowSignatureButton = computed(() => {
return (
props.hasSelectedInbox && isRegularMessageMode.value && !isVoiceInbox.value
props.hasSelectedInbox && isRegularMessageMode.value && !props.voiceEnabled
);
});
@@ -111,7 +108,7 @@ watch(
() => props.hasSelectedInbox,
newValue => {
nextTick(() => {
if (newValue && !isVoiceInbox.value) setSignature();
if (newValue && !props.voiceEnabled) setSignature();
});
},
{ immediate: true }
@@ -2,7 +2,7 @@
import { ref, computed } from 'vue';
import { useVuelidate } from '@vuelidate/core';
import { required, requiredIf } from '@vuelidate/validators';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
import { INBOX_TYPES, isVoiceCallEnabled } from 'dashboard/helper/inbox';
import {
appendSignature,
removeSignature,
@@ -100,6 +100,8 @@ const inboxChannelType = computed(() => props.targetInbox?.channelType || '');
const inboxMedium = computed(() => props.targetInbox?.medium || '');
const voiceCallEnabled = computed(() => isVoiceCallEnabled(props.targetInbox));
const effectiveChannelType = computed(() =>
getEffectiveChannelType(inboxChannelType.value, inboxMedium.value)
);
@@ -442,6 +444,7 @@ useKeyboardEvents({
:is-twilio-whats-app-inbox="inboxTypes.isTwilioWhatsapp"
:message-templates="whatsappMessageTemplates"
:channel-type="inboxChannelType"
:voice-enabled="voiceCallEnabled"
:is-loading="isCreating"
:disable-send-button="isCreating"
:has-selected-inbox="!!targetInbox"
@@ -1,4 +1,5 @@
import { computed } from 'vue';
import { isVoiceCallEnabled } from 'dashboard/helper/inbox';
export function useChannelIcon(inbox) {
const channelTypeIconMap = {
@@ -14,7 +15,6 @@ export function useChannelIcon(inbox) {
'Channel::Whatsapp': 'i-woot-whatsapp',
'Channel::Instagram': 'i-woot-instagram',
'Channel::Tiktok': 'i-woot-tiktok',
'Channel::Voice': 'i-woot-voice',
};
const providerIconMap = {
@@ -38,6 +38,11 @@ export function useChannelIcon(inbox) {
icon = 'i-woot-whatsapp';
}
// Special case for voice-enabled inboxes (Twilio, WhatsApp, etc.)
if (isVoiceCallEnabled(inboxDetails)) {
icon = 'i-woot-voice';
}
return icon ?? 'i-ri-global-fill';
});
@@ -19,8 +19,11 @@ describe('useChannelIcon', () => {
expect(icon).toBe('i-woot-whatsapp');
});
it('returns correct icon for Voice channel', () => {
const inbox = { channel_type: 'Channel::Voice' };
it('returns correct icon for voice-enabled Twilio channel', () => {
const inbox = {
channel_type: 'Channel::TwilioSms',
voice_enabled: true,
};
const { value: icon } = useChannelIcon(inbox);
expect(icon).toBe('i-woot-voice');
});
@@ -47,7 +47,11 @@ const mockStore = createStore({
11: { id: 11, channel_type: INBOX_TYPES.API },
12: { id: 12, channel_type: INBOX_TYPES.SMS },
13: { id: 13, channel_type: INBOX_TYPES.INSTAGRAM },
14: { id: 14, channel_type: INBOX_TYPES.VOICE },
14: {
id: 14,
channel_type: INBOX_TYPES.TWILIO,
voice_enabled: true,
},
15: { id: 15, channel_type: INBOX_TYPES.TIKTOK },
};
return inboxes[id] || null;
@@ -211,11 +215,11 @@ describe('useInbox', () => {
});
expect(wrapper.vm.isAnInstagramChannel).toBe(true);
// Test Voice
// Test Voice (Twilio with voice_enabled)
wrapper = mount(createTestComponent(14), {
global: { plugins: [mockStore] },
});
expect(wrapper.vm.isAVoiceChannel).toBe(true);
expect(wrapper.vm.voiceCallEnabled).toBe(true);
// Test Tiktok
wrapper = mount(createTestComponent(15), {
@@ -274,7 +278,8 @@ describe('useInbox', () => {
'isAnEmailChannel',
'isAnInstagramChannel',
'isATiktokChannel',
'isAVoiceChannel',
'voiceCallEnabled',
'voiceCallProvider',
];
expectedProperties.forEach(prop => {
@@ -1,7 +1,11 @@
import { computed } from 'vue';
import { useMapGetter } from 'dashboard/composables/store';
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
import {
INBOX_TYPES,
isVoiceCallEnabled,
getVoiceCallProvider,
} from 'dashboard/helper/inbox';
export const INBOX_FEATURES = {
REPLY_TO: 'replyTo',
@@ -134,9 +138,9 @@ export const useInbox = (inboxId = null) => {
return channelType.value === INBOX_TYPES.TIKTOK;
});
const isAVoiceChannel = computed(() => {
return channelType.value === INBOX_TYPES.VOICE;
});
const voiceCallEnabled = computed(() => isVoiceCallEnabled(inbox.value));
const voiceCallProvider = computed(() => getVoiceCallProvider(inbox.value));
return {
inbox,
@@ -156,6 +160,7 @@ export const useInbox = (inboxId = null) => {
isAnEmailChannel,
isAnInstagramChannel,
isATiktokChannel,
isAVoiceChannel,
voiceCallEnabled,
voiceCallProvider,
};
};
@@ -109,11 +109,6 @@ export const FORMATTING = {
'redo',
],
},
'Channel::Voice': {
marks: [],
nodes: [],
menu: [],
},
'Channel::Tiktok': {
marks: [],
nodes: [],
+1
View File
@@ -36,6 +36,7 @@ export const FEATURE_FLAGS = {
CHATWOOT_V4: 'chatwoot_v4',
CHANNEL_INSTAGRAM: 'channel_instagram',
CHANNEL_TIKTOK: 'channel_tiktok',
CHANNEL_VOICE: 'channel_voice',
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
CAPTAIN_CUSTOM_TOOLS: 'custom_tools',
CAPTAIN_V2: 'captain_integration_v2',
+21 -10
View File
@@ -11,9 +11,29 @@ export const INBOX_TYPES = {
SMS: 'Channel::Sms',
INSTAGRAM: 'Channel::Instagram',
TIKTOK: 'Channel::Tiktok',
VOICE: 'Channel::Voice',
};
// Add providers here as they gain voice capability (e.g., WhatsApp Cloud, Twilio WhatsApp)
export const VOICE_CALL_PROVIDERS = {
TWILIO: 'twilio',
};
export const getVoiceCallProvider = inbox => {
if (!inbox) return null;
// Callers pass either snake_case (raw API) or camelCase (after camelcaseKeys) shapes.
const channelType = inbox.channel_type || inbox.channelType;
const voiceEnabled = inbox.voice_enabled || inbox.voiceEnabled;
if (channelType === INBOX_TYPES.TWILIO && voiceEnabled) {
return VOICE_CALL_PROVIDERS.TWILIO;
}
return null;
};
export const isVoiceCallEnabled = inbox => getVoiceCallProvider(inbox) !== null;
export const TWILIO_CHANNEL_MEDIUM = {
WHATSAPP: 'whatsapp',
SMS: 'sms',
@@ -30,7 +50,6 @@ const INBOX_ICON_MAP_FILL = {
[INBOX_TYPES.LINE]: 'i-ri-line-fill',
[INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-fill',
[INBOX_TYPES.TIKTOK]: 'i-ri-tiktok-fill',
[INBOX_TYPES.VOICE]: 'i-ri-phone-fill',
};
const DEFAULT_ICON_FILL = 'i-ri-chat-1-fill';
@@ -45,7 +64,6 @@ const INBOX_ICON_MAP_LINE = {
[INBOX_TYPES.TELEGRAM]: 'i-woot-telegram',
[INBOX_TYPES.LINE]: 'i-woot-line',
[INBOX_TYPES.INSTAGRAM]: 'i-woot-instagram',
[INBOX_TYPES.VOICE]: 'i-woot-voice',
[INBOX_TYPES.TIKTOK]: 'i-woot-tiktok',
};
@@ -58,7 +76,6 @@ export const getInboxSource = (type, phoneNumber, inbox) => {
case INBOX_TYPES.TWILIO:
case INBOX_TYPES.WHATSAPP:
case INBOX_TYPES.VOICE:
return phoneNumber || '';
case INBOX_TYPES.EMAIL:
@@ -97,9 +114,6 @@ export const getReadableInboxByType = (type, phoneNumber) => {
case INBOX_TYPES.LINE:
return 'line';
case INBOX_TYPES.VOICE:
return 'voice';
default:
return 'chat';
}
@@ -142,9 +156,6 @@ export const getInboxClassByType = (type, phoneNumber) => {
case INBOX_TYPES.TIKTOK:
return 'brand-tiktok';
case INBOX_TYPES.VOICE:
return 'phone';
default:
return 'chat';
}
@@ -636,7 +636,17 @@
"WIDGET_BUILDER": "Widget Builder",
"BOT_CONFIGURATION": "Bot Configuration",
"ACCOUNT_HEALTH": "Account Health",
"CSAT": "CSAT"
"CSAT": "CSAT",
"VOICE": "Voice"
},
"VOICE_CONFIGURATION": {
"ENABLE_VOICE": {
"LABEL": "Enable Voice Calling",
"DESCRIPTION": "Enable voice calling on this inbox. Agents will be able to make and receive phone calls."
},
"CREDENTIALS": {
"DESCRIPTION": "Voice calling requires Twilio API Key credentials. These are used to generate tokens for agent voice connections."
}
},
"CHANNEL_PREFERENCES": "Channel Preferences",
"WIDGET_FEATURES": "Widget features",
@@ -145,6 +145,7 @@ const openDelete = inbox => {
<ChannelName
:channel-type="inbox.channel_type"
:medium="inbox.medium"
:voice-enabled="inbox.voice_enabled"
class="text-body-main text-n-slate-11"
/>
</div>
@@ -21,6 +21,7 @@ import PreChatFormSettings from './PreChatForm/Settings.vue';
import WeeklyAvailability from './components/WeeklyAvailability.vue';
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
import ConfigurationPage from './settingsPage/ConfigurationPage.vue';
import VoiceConfigurationPage from './settingsPage/VoiceConfigurationPage.vue';
import CustomerSatisfactionPage from './settingsPage/CustomerSatisfactionPage.vue';
import CollaboratorsPage from './settingsPage/CollaboratorsPage.vue';
import BotConfiguration from './components/BotConfiguration.vue';
@@ -46,6 +47,7 @@ export default {
BotConfiguration,
CollaboratorsPage,
ConfigurationPage,
VoiceConfigurationPage,
CustomerSatisfactionPage,
FacebookReauthorize,
GreetingsEditor,
@@ -169,19 +171,17 @@ export default {
},
];
if (!this.isAVoiceChannel) {
visibleToAllChannelTabs = [
...visibleToAllChannelTabs,
{
key: 'business-hours',
name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
},
{
key: 'csat',
name: this.$t('INBOX_MGMT.TABS.CSAT'),
},
];
}
visibleToAllChannelTabs = [
...visibleToAllChannelTabs,
{
key: 'business-hours',
name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
},
{
key: 'csat',
name: this.$t('INBOX_MGMT.TABS.CSAT'),
},
];
if (this.isAWebWidgetInbox) {
visibleToAllChannelTabs = [
@@ -197,7 +197,6 @@ export default {
this.isATwilioChannel ||
this.isALineChannel ||
this.isAPIInbox ||
this.isAVoiceChannel ||
(this.isAnEmailChannel && !this.inbox.provider) ||
this.shouldShowWhatsAppConfiguration ||
this.isAWebWidgetInbox
@@ -232,6 +231,24 @@ export default {
];
}
if (
this.isATwilioChannel &&
this.inbox.phone_number &&
this.inbox.medium === 'sms' &&
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.CHANNEL_VOICE
)
) {
visibleToAllChannelTabs = [
...visibleToAllChannelTabs,
{
key: 'voice-configuration',
name: this.$t('INBOX_MGMT.TABS.VOICE'),
},
];
}
return visibleToAllChannelTabs;
},
currentInboxId() {
@@ -812,7 +829,6 @@ export default {
</SettingsFieldSection>
<SettingsFieldSection
v-if="!isAVoiceChannel"
:label="$t('INBOX_MGMT.HELP_CENTER.LABEL')"
:help-text="$t('INBOX_MGMT.HELP_CENTER.SUB_TEXT')"
>
@@ -1240,6 +1256,12 @@ export default {
>
<ConfigurationPage :inbox="inbox" />
</div>
<div
v-if="selectedTabKey === 'voice-configuration'"
class="mx-6 max-w-4xl"
>
<VoiceConfigurationPage :inbox="inbox" />
</div>
<div v-if="selectedTabKey === 'csat'">
<CustomerSatisfactionPage :inbox="inbox" />
</div>
@@ -12,6 +12,10 @@ const props = defineProps({
type: String,
default: '',
},
voiceEnabled: {
type: Boolean,
default: false,
},
});
const getters = useStoreGetters();
const { t } = useI18n();
@@ -30,7 +34,6 @@ const i18nMap = {
'Channel::Api': 'API',
'Channel::Instagram': 'INSTAGRAM',
'Channel::Tiktok': 'TIKTOK',
'Channel::Voice': 'VOICE',
};
const twilioChannelName = () => {
@@ -45,6 +48,9 @@ const readableChannelName = computed(() => {
return globalConfig.value.apiChannelName || t('INBOX_MGMT.CHANNELS.API');
}
if (props.channelType === 'Channel::TwilioSms') {
if (props.voiceEnabled) {
return t('INBOX_MGMT.CHANNELS.VOICE');
}
return twilioChannelName();
}
return t(`INBOX_MGMT.CHANNELS.${i18nMap[props.channelType]}`);
@@ -208,24 +208,6 @@ export default {
</NextButton>
</SettingsFieldSection>
</div>
<div v-else-if="isAVoiceChannel">
<SettingsFieldSection
:label="$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_VOICE_URL_TITLE')"
:help-text="
$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_VOICE_URL_SUBTITLE')
"
>
<woot-code :script="inbox.voice_call_webhook_url" lang="html" />
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_STATUS_URL_TITLE')"
:help-text="
$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_STATUS_URL_SUBTITLE')
"
>
<woot-code :script="inbox.voice_status_webhook_url" lang="html" />
</SettingsFieldSection>
</div>
<div v-else-if="isALineChannel">
<SettingsFieldSection
@@ -0,0 +1,156 @@
<script>
import { useAlert } from 'dashboard/composables';
import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFieldSection.vue';
import SettingsToggleSection from 'dashboard/components-next/Settings/SettingsToggleSection.vue';
import NextInput from 'dashboard/components-next/input/Input.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
export default {
components: {
SettingsFieldSection,
SettingsToggleSection,
NextInput,
NextButton,
},
props: {
inbox: {
type: Object,
default: () => ({}),
},
},
data() {
return {
voiceEnabled: this.inbox.voice_enabled || false,
apiKeySid: this.inbox.api_key_sid || '',
apiKeySecret: '',
isUpdating: false,
};
},
computed: {
isVoiceConfigured() {
return !!this.inbox.voice_configured;
},
hasApiKeySid() {
return !!this.inbox.api_key_sid;
},
hasExistingCredentials() {
return this.hasApiKeySid && !!this.inbox.has_api_key_secret;
},
needsCredentials() {
return (
this.voiceEnabled &&
!this.isVoiceConfigured &&
!this.hasExistingCredentials
);
},
needsApiKeySid() {
return this.needsCredentials && !this.hasApiKeySid;
},
isSubmitDisabled() {
if (!this.voiceEnabled) return false;
if (this.needsCredentials) {
if (this.needsApiKeySid && !this.apiKeySid) return true;
return !this.apiKeySecret;
}
return false;
},
},
watch: {
'inbox.voice_enabled'(val) {
this.voiceEnabled = val || false;
},
'inbox.api_key_sid'(val) {
this.apiKeySid = val || '';
},
},
methods: {
async updateVoiceSettings() {
this.isUpdating = true;
try {
const channelPayload = { voice_enabled: this.voiceEnabled };
if (this.needsCredentials) {
if (this.needsApiKeySid) {
channelPayload.api_key_sid = this.apiKeySid;
}
channelPayload.api_key_secret = this.apiKeySecret;
}
await this.$store.dispatch('inboxes/updateInbox', {
id: this.inbox.id,
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;
}
},
},
};
</script>
<template>
<div class="flex flex-col gap-6">
<SettingsToggleSection
v-model="voiceEnabled"
:header="$t('INBOX_MGMT.VOICE_CONFIGURATION.ENABLE_VOICE.LABEL')"
:description="
$t('INBOX_MGMT.VOICE_CONFIGURATION.ENABLE_VOICE.DESCRIPTION')
"
/>
<div v-if="voiceEnabled && needsCredentials" class="flex flex-col gap-4">
<p class="text-sm text-n-slate-11">
{{ $t('INBOX_MGMT.VOICE_CONFIGURATION.CREDENTIALS.DESCRIPTION') }}
</p>
<NextInput
v-if="needsApiKeySid"
v-model="apiKeySid"
:label="$t('INBOX_MGMT.ADD.VOICE.TWILIO.API_KEY_SID.LABEL')"
:placeholder="$t('INBOX_MGMT.ADD.VOICE.TWILIO.API_KEY_SID.PLACEHOLDER')"
/>
<NextInput
v-model="apiKeySecret"
type="password"
:label="$t('INBOX_MGMT.ADD.VOICE.TWILIO.API_KEY_SECRET.LABEL')"
:placeholder="
$t('INBOX_MGMT.ADD.VOICE.TWILIO.API_KEY_SECRET.PLACEHOLDER')
"
/>
</div>
<div v-if="inbox.voice_enabled && inbox.voice_call_webhook_url">
<SettingsFieldSection
:label="$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_VOICE_URL_TITLE')"
:help-text="
$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_VOICE_URL_SUBTITLE')
"
>
<woot-code :script="inbox.voice_call_webhook_url" lang="html" />
</SettingsFieldSection>
<SettingsFieldSection
:label="
$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_STATUS_URL_TITLE')
"
:help-text="
$t('INBOX_MGMT.ADD.VOICE.CONFIGURATION.TWILIO_STATUS_URL_SUBTITLE')
"
>
<woot-code :script="inbox.voice_status_webhook_url" lang="html" />
</SettingsFieldSection>
</div>
<div>
<NextButton
:disabled="isSubmitDisabled"
:is-loading="isUpdating"
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
@click="updateVoiceSettings"
/>
</div>
</div>
</template>
+3 -3
View File
@@ -1,4 +1,4 @@
import { INBOX_TYPES } from 'dashboard/helper/inbox';
import { INBOX_TYPES, isVoiceCallEnabled } from 'dashboard/helper/inbox';
export const INBOX_FEATURES = {
REPLY_TO: 'replyTo',
@@ -59,8 +59,8 @@ export default {
isALineChannel() {
return this.channelType === INBOX_TYPES.LINE;
},
isAVoiceChannel() {
return this.channelType === INBOX_TYPES.VOICE;
voiceCallEnabled() {
return isVoiceCallEnabled(this.inbox);
},
isAnEmailChannel() {
return this.channelType === INBOX_TYPES.EMAIL;