Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
556830badb | ||
|
|
86b7b4b046 | ||
|
|
b747e73f7d | ||
|
|
933bd656b0 | ||
|
|
bf37ce942b | ||
|
|
5e8e09e23d | ||
|
|
9eeacdbabe | ||
|
|
bfbbe9b754 | ||
|
|
fa2f85ccbe | ||
|
|
5af8daf4be | ||
|
|
c40a541ed6 | ||
|
|
537ea5a2a7 | ||
|
|
73420636bf | ||
|
|
672f81f3a8 | ||
|
|
1f66034750 |
@@ -20,7 +20,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
ActiveRecord::Base.transaction do
|
||||
authenticate_twilio
|
||||
build_inbox
|
||||
setup_webhooks if @twilio_channel.sms?
|
||||
setup_webhooks
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ describe('useInbox', () => {
|
||||
});
|
||||
|
||||
expect(wrapper.vm.isATwilioChannel).toBe(true);
|
||||
expect(wrapper.vm.isATwilioSMSChannel).toBe(true);
|
||||
expect(wrapper.vm.isASmsInbox).toBe(true);
|
||||
expect(wrapper.vm.isAWhatsAppChannel).toBe(false);
|
||||
});
|
||||
@@ -268,6 +269,7 @@ describe('useInbox', () => {
|
||||
'isASmsInbox',
|
||||
'isATelegramChannel',
|
||||
'isATwilioChannel',
|
||||
'isATwilioSMSChannel',
|
||||
'isAWebWidgetInbox',
|
||||
'isAWhatsAppChannel',
|
||||
'isAMicrosoftInbox',
|
||||
|
||||
@@ -150,6 +150,7 @@ export const useInbox = (inboxId = null) => {
|
||||
isASmsInbox,
|
||||
isATelegramChannel,
|
||||
isATwilioChannel,
|
||||
isATwilioSMSChannel,
|
||||
isAWebWidgetInbox,
|
||||
isAWhatsAppChannel,
|
||||
isAMicrosoftInbox,
|
||||
|
||||
@@ -134,7 +134,6 @@
|
||||
},
|
||||
"TWILIO": {
|
||||
"TITLE": "Twilio SMS/WhatsApp Channel",
|
||||
"DESC": "Integrate Twilio and start supporting your customers via SMS or WhatsApp.",
|
||||
"ACCOUNT_SID": {
|
||||
"LABEL": "Account SID",
|
||||
"PLACEHOLDER": "Please enter your Twilio Account SID",
|
||||
@@ -176,13 +175,13 @@
|
||||
"PLACEHOLDER": "Please enter the phone number from which message will be sent.",
|
||||
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "You have to configure the message callback URL in Twilio with the URL mentioned here."
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create Twilio Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to authenticate Twilio credentials, please try again"
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "You have to configure the message callback URL in Twilio with the URL mentioned here."
|
||||
}
|
||||
},
|
||||
"SMS": {
|
||||
@@ -559,6 +558,7 @@
|
||||
"MORE_SETTINGS": "More settings",
|
||||
"WEBSITE_SUCCESS": "You have successfully finished creating a website channel. Copy the code shown below and paste it on your website. Next time a customer use the live chat, the conversation will automatically appear on your inbox.",
|
||||
"WHATSAPP_QR_INSTRUCTION": "Scan the QR code above to quickly test your WhatsApp inbox",
|
||||
"SMS_QR_INSTRUCTION": "Scan the QR code above to quickly test your SMS inbox",
|
||||
"MESSENGER_QR_INSTRUCTION": "Scan the QR code above to quickly test your Facebook Messenger inbox",
|
||||
"TELEGRAM_QR_INSTRUCTION": "Scan the QR code above to quickly test your Telegram inbox"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
const qrCodes = reactive({
|
||||
sms: '',
|
||||
whatsapp: '',
|
||||
messenger: '',
|
||||
telegram: '',
|
||||
@@ -28,14 +29,13 @@ const currentInbox = computed(() =>
|
||||
// Use useInbox composable with the inbox ID
|
||||
const {
|
||||
isAWhatsAppCloudChannel,
|
||||
isATwilioChannel,
|
||||
isAWhatsAppChannel,
|
||||
isASmsInbox,
|
||||
isALineChannel,
|
||||
isAnEmailChannel,
|
||||
isAWhatsAppChannel,
|
||||
isAFacebookInbox,
|
||||
isATelegramChannel,
|
||||
isATwilioWhatsAppChannel,
|
||||
isATwilioSMSChannel,
|
||||
} = useInbox(route.params.inbox_id);
|
||||
|
||||
const hasDuplicateInstagramInbox = computed(() => {
|
||||
@@ -55,23 +55,28 @@ const shouldShowWhatsAppWebhookDetails = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const isWhatsAppEmbeddedSignup = computed(() => {
|
||||
return (
|
||||
isAWhatsAppCloudChannel.value &&
|
||||
currentInbox.value.provider_config?.source === 'embedded_signup'
|
||||
);
|
||||
const whatsappPhoneNumber = computed(() => {
|
||||
return (currentInbox.value?.phone_number || '').replace('whatsapp:', '');
|
||||
});
|
||||
|
||||
const shouldShowWhatsAppQr = computed(() => {
|
||||
return isAWhatsAppChannel.value && Boolean(whatsappPhoneNumber.value);
|
||||
});
|
||||
|
||||
const shouldShowSmsQr = computed(() => {
|
||||
return isATwilioSMSChannel.value && Boolean(currentInbox.value?.phone_number);
|
||||
});
|
||||
|
||||
const message = computed(() => {
|
||||
if (isATwilioChannel.value) {
|
||||
if (shouldShowWhatsAppQr.value) {
|
||||
return `${t('INBOX_MGMT.FINISH.MESSAGE')}. ${t(
|
||||
'INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE'
|
||||
'INBOX_MGMT.FINISH.WHATSAPP_QR_INSTRUCTION'
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (isASmsInbox.value) {
|
||||
if (shouldShowSmsQr.value) {
|
||||
return `${t('INBOX_MGMT.FINISH.MESSAGE')}. ${t(
|
||||
'INBOX_MGMT.ADD.SMS.BANDWIDTH.API_CALLBACK.SUBTITLE'
|
||||
'INBOX_MGMT.FINISH.SMS_QR_INSTRUCTION'
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -91,12 +96,6 @@ const message = computed(() => {
|
||||
return t('INBOX_MGMT.FINISH.WEBSITE_SUCCESS');
|
||||
}
|
||||
|
||||
if (isWhatsAppEmbeddedSignup.value) {
|
||||
return `${t('INBOX_MGMT.FINISH.MESSAGE')}. ${t(
|
||||
'INBOX_MGMT.FINISH.WHATSAPP_QR_INSTRUCTION'
|
||||
)}`;
|
||||
}
|
||||
|
||||
return t('INBOX_MGMT.FINISH.MESSAGE');
|
||||
});
|
||||
|
||||
@@ -109,6 +108,7 @@ async function generateQRCode(platform, identifier) {
|
||||
|
||||
try {
|
||||
const platformUrls = {
|
||||
sms: id => `sms:${id}`,
|
||||
whatsapp: id => `https://wa.me/${id}`,
|
||||
messenger: id => `https://m.me/${id}`,
|
||||
telegram: id => `https://t.me/${id}`,
|
||||
@@ -127,15 +127,13 @@ async function generateQRCode(platform, identifier) {
|
||||
async function generateQRCodes() {
|
||||
if (!currentInbox.value) return;
|
||||
|
||||
// WhatsApp (both Cloud and Twilio)
|
||||
if (currentInbox.value.phone_number && isAWhatsAppChannel.value) {
|
||||
// For Twilio WhatsApp, phone_number format is "whatsapp:+1234567890"
|
||||
// Extract just the phone number part for QR code generation
|
||||
const phoneNumber = currentInbox.value.phone_number.replace(
|
||||
'whatsapp:',
|
||||
''
|
||||
);
|
||||
await generateQRCode('whatsapp', phoneNumber);
|
||||
// WhatsApp
|
||||
if (shouldShowWhatsAppQr.value) {
|
||||
await generateQRCode('whatsapp', whatsappPhoneNumber.value);
|
||||
}
|
||||
|
||||
if (shouldShowSmsQr.value) {
|
||||
await generateQRCode('sms', currentInbox.value.phone_number);
|
||||
}
|
||||
|
||||
// Facebook Messenger
|
||||
@@ -183,13 +181,6 @@ onMounted(() => {
|
||||
:script="currentInbox.web_widget_script"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-[50%] max-w-[50%] ml-[25%]">
|
||||
<woot-code
|
||||
v-if="isATwilioWhatsAppChannel"
|
||||
lang="html"
|
||||
:script="currentInbox.callback_webhook_url"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldShowWhatsAppWebhookDetails"
|
||||
class="w-[50%] max-w-[50%] ml-[25%]"
|
||||
@@ -217,12 +208,32 @@ onMounted(() => {
|
||||
:script="currentInbox.callback_webhook_url"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-[50%] max-w-[50%] ml-[25%]">
|
||||
<woot-code
|
||||
v-if="isASmsInbox"
|
||||
lang="html"
|
||||
:script="currentInbox.callback_webhook_url"
|
||||
/>
|
||||
<div
|
||||
v-if="isASmsInbox && !isATwilioSMSChannel"
|
||||
class="w-[50%] max-w-[50%] ml-[25%]"
|
||||
>
|
||||
<p class="mt-8 font-medium text-n-slate-11">
|
||||
{{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_CALLBACK.TITLE') }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-n-slate-9">
|
||||
{{ $t('INBOX_MGMT.ADD.SMS.BANDWIDTH.API_CALLBACK.SUBTITLE') }}
|
||||
</p>
|
||||
<woot-code lang="html" :script="currentInbox.callback_webhook_url" />
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldShowSmsQr && qrCodes.sms"
|
||||
class="flex flex-col gap-3 items-center mt-8"
|
||||
>
|
||||
<p class="mt-2 text-sm text-n-slate-9">
|
||||
{{ $t('INBOX_MGMT.FINISH.SMS_QR_INSTRUCTION') }}
|
||||
</p>
|
||||
<div class="rounded-lg shadow outline-1 outline-n-strong outline">
|
||||
<img
|
||||
:src="qrCodes.sms"
|
||||
alt="SMS QR Code"
|
||||
class="rounded-lg size-48 dark:invert"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<EmailInboxFinish
|
||||
v-if="isAnEmailChannel && !currentInbox.provider"
|
||||
@@ -230,7 +241,7 @@ onMounted(() => {
|
||||
:inbox-id="$route.params.inbox_id"
|
||||
/>
|
||||
<div
|
||||
v-if="isAWhatsAppChannel && qrCodes.whatsapp"
|
||||
v-if="shouldShowWhatsAppQr && qrCodes.whatsapp"
|
||||
class="flex flex-col gap-3 items-center mt-8"
|
||||
>
|
||||
<p class="mt-2 text-sm text-n-slate-9">
|
||||
|
||||
@@ -33,12 +33,16 @@ class Twilio::WebhookSetupService
|
||||
end
|
||||
end
|
||||
|
||||
def twilio_phone_number
|
||||
channel.phone_number.delete_prefix('whatsapp:')
|
||||
end
|
||||
|
||||
def phonenumber_sid
|
||||
phone_numbers.first.sid
|
||||
end
|
||||
|
||||
def phone_numbers
|
||||
@phone_numbers ||= twilio_client.incoming_phone_numbers.list(phone_number: channel.phone_number)
|
||||
@phone_numbers ||= twilio_client.incoming_phone_numbers.list(phone_number: twilio_phone_number)
|
||||
end
|
||||
|
||||
def channel
|
||||
@@ -46,6 +50,14 @@ class Twilio::WebhookSetupService
|
||||
end
|
||||
|
||||
def twilio_client
|
||||
@twilio_client ||= ::Twilio::REST::Client.new(channel.account_sid, channel.auth_token)
|
||||
@twilio_client ||= if channel.api_key_sid.present?
|
||||
::Twilio::REST::Client.new(
|
||||
channel.api_key_sid,
|
||||
channel.auth_token,
|
||||
channel.account_sid
|
||||
)
|
||||
else
|
||||
::Twilio::REST::Client.new(channel.account_sid, channel.auth_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,6 +38,8 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
|
||||
context 'when user is logged in' do
|
||||
context 'with user as administrator' do
|
||||
it 'creates inbox and returns inbox object' do
|
||||
expect(Twilio::WebhookSetupService).to receive(:new).and_return(twilio_webhook_setup_service)
|
||||
expect(twilio_webhook_setup_service).to receive(:perform)
|
||||
allow(twilio_client).to receive(:messages).and_return(message_double)
|
||||
allow(message_double).to receive(:list).and_return([])
|
||||
|
||||
@@ -53,6 +55,8 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
|
||||
end
|
||||
|
||||
it 'creates inbox with blank phone number and returns inbox object' do
|
||||
expect(Twilio::WebhookSetupService).to receive(:new).and_return(twilio_webhook_setup_service)
|
||||
expect(twilio_webhook_setup_service).to receive(:perform)
|
||||
params = {
|
||||
twilio_channel: {
|
||||
account_sid: 'sid-1',
|
||||
@@ -131,6 +135,8 @@ RSpec.describe '/api/v1/accounts/{account.id}/channels/twilio_channel', type: :r
|
||||
end
|
||||
|
||||
it 'return error if Twilio tokens are incorrect' do
|
||||
expect(twilio_webhook_setup_service).not_to receive(:perform)
|
||||
expect(Twilio::WebhookSetupService).not_to receive(:new)
|
||||
allow(twilio_client).to receive(:messages).and_return(message_double)
|
||||
allow(message_double).to receive(:list).and_raise(Twilio::REST::TwilioError)
|
||||
|
||||
|
||||
@@ -10,8 +10,40 @@ describe Twilio::WebhookSetupService do
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'with api key authentication' do
|
||||
let(:channel_twilio_sms) do
|
||||
create(
|
||||
:channel_twilio_sms,
|
||||
api_key_sid: 'SK1234567890abcdef',
|
||||
account_sid: 'AC1234567890abcdef',
|
||||
auth_token: 'api-key-token',
|
||||
messaging_service_sid: 'MG1234567890abcdef'
|
||||
)
|
||||
end
|
||||
|
||||
let(:messaging) { instance_double(Twilio::REST::Messaging) }
|
||||
let(:services) { instance_double(Twilio::REST::Messaging::V1::ServiceContext) }
|
||||
|
||||
before do
|
||||
allow(Twilio::REST::Client).to receive(:new).with(
|
||||
'SK1234567890abcdef',
|
||||
'api-key-token',
|
||||
'AC1234567890abcdef'
|
||||
).and_return(twilio_client)
|
||||
allow(twilio_client).to receive(:messaging).and_return(messaging)
|
||||
allow(messaging).to receive(:services).with(channel_twilio_sms.messaging_service_sid).and_return(services)
|
||||
allow(services).to receive(:update)
|
||||
end
|
||||
|
||||
it 'uses API key credentials to initialize twilio client' do
|
||||
described_class.new(inbox: channel_twilio_sms.inbox).perform
|
||||
|
||||
expect(services).to have_received(:update)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a messaging service sid' do
|
||||
let(:channel_twilio_sms) { create(:channel_twilio_sms) }
|
||||
let(:channel_twilio_sms) { create(:channel_twilio_sms, :whatsapp) }
|
||||
|
||||
let(:messaging) { instance_double(Twilio::REST::Messaging) }
|
||||
let(:services) { instance_double(Twilio::REST::Messaging::V1::ServiceContext) }
|
||||
@@ -20,12 +52,14 @@ describe Twilio::WebhookSetupService do
|
||||
allow(twilio_client).to receive(:messaging).and_return(messaging)
|
||||
allow(messaging).to receive(:services).with(channel_twilio_sms.messaging_service_sid).and_return(services)
|
||||
allow(services).to receive(:update)
|
||||
allow(twilio_client).to receive(:incoming_phone_numbers)
|
||||
end
|
||||
|
||||
it 'updates the messaging service' do
|
||||
it 'updates the messaging service webhook and skips phone number lookup' do
|
||||
described_class.new(inbox: channel_twilio_sms.inbox).perform
|
||||
|
||||
expect(services).to have_received(:update)
|
||||
expect(twilio_client).not_to have_received(:incoming_phone_numbers)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,6 +94,26 @@ describe Twilio::WebhookSetupService do
|
||||
sms_url: twilio_callback_index_url
|
||||
)
|
||||
end
|
||||
|
||||
it 'strips whatsapp prefix before looking up phone number' do
|
||||
phone_channel = create(
|
||||
:channel_twilio_sms,
|
||||
:with_phone_number,
|
||||
:whatsapp,
|
||||
phone_number: 'whatsapp:+1234567890',
|
||||
messaging_service_sid: nil
|
||||
)
|
||||
|
||||
allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double)
|
||||
allow(phone_double).to receive(:list).and_return([phone_record_double])
|
||||
allow(phone_record_double).to receive(:sid).and_return('1234')
|
||||
|
||||
described_class.new(inbox: phone_channel.inbox).perform
|
||||
|
||||
expect(phone_double).to have_received(:list).with(
|
||||
phone_number: '+1234567890'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user