Compare commits

...
Author SHA1 Message Date
Muhsin KelothandGitHub 556830badb Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-06-12 10:30:04 +04:00
Muhsin KelothandGitHub 86b7b4b046 Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-06-04 10:48:39 +04:00
Muhsin KelothandGitHub b747e73f7d Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-04-21 13:51:36 +04:00
Muhsin KelothandGitHub 933bd656b0 Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-03-24 11:19:08 +04:00
Muhsin KelothandGitHub bf37ce942b Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-03-23 23:37:39 +04:00
Muhsin KelothandGitHub 5e8e09e23d Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-03-18 11:27:47 +04:00
Sojan Jose 9eeacdbabe fix(inboxes): gate Twilio SMS QR guidance on phone number 2026-03-17 18:08:00 -07:00
Muhsin KelothandGitHub bfbbe9b754 Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-03-17 19:35:53 +04:00
Muhsin fa2f85ccbe chore: fix issues 2026-03-17 16:24:39 +04:00
Muhsin KelothandGitHub 5af8daf4be Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-03-17 15:58:48 +04:00
Sojan Jose c40a541ed6 fix(inboxes): restore whatsapp qr flow and tighten twilio webhook coverage 2026-02-13 18:08:37 -08:00
Sojan Jose 537ea5a2a7 fix(inboxes): fix twilio webhook and finish setup edge cases 2026-02-13 17:33:19 -08:00
Sojan JoseandGitHub 73420636bf Merge branch 'develop' into codex/fix-inbox-finish-setup-guidance 2026-02-13 16:47:57 -08:00
Sojan Jose 672f81f3a8 test(inboxes): cover webhook setup automation 2026-02-13 16:47:23 -08:00
Sojan Jose 1f66034750 feat(inboxes): improve channel finish setup guidance 2026-02-13 16:43:13 -08:00
8 changed files with 137 additions and 51 deletions
@@ -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">
+14 -2
View File
@@ -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