From 83873d49ae8c90cc21d8d1732e799c8d83dfba6a Mon Sep 17 00:00:00 2001 From: Aaron Lil Vazquez Date: Fri, 22 Aug 2025 07:07:36 -0300 Subject: [PATCH 01/13] fix(meta): use dynamic installation name in vueapp.html.erb (#11799) This PR updates the meta description in `vueapp.html.erb` to use the dynamic installation name, replacing the previously hardcoded "Chatwoot" brand. **Motivation:** This change allows self-hosted instances to display their own branding in meta descriptions, improving customization and SEO for custom installations. Co-authored-by: Sojan Jose --- app/views/layouts/vueapp.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/vueapp.html.erb b/app/views/layouts/vueapp.html.erb index 8ccac6c48..55fba871c 100644 --- a/app/views/layouts/vueapp.html.erb +++ b/app/views/layouts/vueapp.html.erb @@ -9,7 +9,7 @@ - + <% if ENV['IOS_APP_IDENTIFIER'].present? %> '> <% end %> From d35b0a9465a48f765691d6e95a49554a5d57a7b4 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:12:24 +0530 Subject: [PATCH 02/13] feat: Convert availability slots to local timezone (#12273) Co-authored-by: Shivam Mishra --- .../widget/helpers/availabilityHelpers.js | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/app/javascript/widget/helpers/availabilityHelpers.js b/app/javascript/widget/helpers/availabilityHelpers.js index 9af4f1c8d..256ebec55 100644 --- a/app/javascript/widget/helpers/availabilityHelpers.js +++ b/app/javascript/widget/helpers/availabilityHelpers.js @@ -150,6 +150,53 @@ const findNextSlot = (currentDay, currentMinutes, openDays) => }) .find(Boolean) || null; +/** + * Convert slot details from inbox timezone to user timezone + * @private + * @param {Date} time - Current time + * @param {string} inboxTz - Inbox timezone + * @param {Object} slotDetails - Original slot details + * @returns {Object} Slot details with user timezone adjustments + */ +const convertSlotToUserTimezone = (time, inboxTz, slotDetails) => { + if (!slotDetails) return null; + + const userTz = Intl.DateTimeFormat().resolvedOptions().timeZone; + + // If timezones match, no conversion needed + if (inboxTz === userTz) return slotDetails; + + // Calculate when the slot opens (absolute time) + const now = time instanceof Date ? time : new Date(time); + const openingTime = new Date( + now.getTime() + slotDetails.minutesUntilOpen * 60000 + ); + + // Convert to user timezone + const openingInUserTz = getDateInTimezone(openingTime, userTz); + const nowInUserTz = getDateInTimezone(now, userTz); + + // Calculate days difference in user timezone + const openingDate = new Date(openingInUserTz); + openingDate.setHours(0, 0, 0, 0); + const todayDate = new Date(nowInUserTz); + todayDate.setHours(0, 0, 0, 0); + const daysUntilOpen = Math.round((openingDate - todayDate) / 86400000); + + // Return with user timezone adjustments + return { + ...slotDetails, + config: { + ...slotDetails.config, + openHour: openingInUserTz.getHours(), + openMinutes: openingInUserTz.getMinutes(), + dayOfWeek: openingInUserTz.getDay(), + }, + daysUntilOpen, + dayOfWeek: openingInUserTz.getDay(), + }; +}; + // --------------------------------------------------------------------------- // Exported functions // --------------------------------------------------------------------------- @@ -213,6 +260,7 @@ export const isInWorkingHours = (time, utcOffset, workingHours = []) => { /** * Find next available slot with detailed information + * Returns times adjusted to user's timezone for display * @param {Date|string} time * @param {string} utcOffset * @param {Array} workingHours @@ -238,10 +286,13 @@ export const findNextAvailableSlotDetails = ( currentMinutes, openDays ); - if (todaySlot) return todaySlot; - // Find next slot - return findNextSlot(currentDay, currentMinutes, openDays); + // Find the slot (today or next) + const slotDetails = + todaySlot || findNextSlot(currentDay, currentMinutes, openDays); + + // Convert to user timezone for display + return convertSlotToUserTimezone(time, utcOffset, slotDetails); }; /** From d48503bdcf5fb3e3dd0b5f30b05fa68dc994f28f Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Fri, 22 Aug 2025 13:38:23 +0200 Subject: [PATCH 03/13] feat(voice): Improved voice call creation flow [EE] (#12268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the voice call creation flow by simplifying configuration and automating setup with Twilio APIs. references: #11602 , #11481 ## Key changes - Removed the requirement for twiml_app_sid – provisioning is now automated through APIs. - Auto-configured webhook URLs for: - Voice number callbacks - Status callbacks - twiML callbacks - Disabled business hours, help center, and related options until voice inbox is fully supported. - Added a configuration tab in the voice inbox to display the required Twilio URLs (to make verification easier in Twilio console). ## Test Cases - Provisioning - Create a new voice inbox → verify that Twilio app provisioning happens automatically. - Verify twiML callback - Webhook configuration - Check that both voice number callback and status callback URLs are auto-populated in Twilio. - Disabled features - Confirm that business hours and help center options are hidden/disabled for voice inbox. - Configuration tab - Open the voice inbox configuration tab → verify that the displayed Twilio URLs match what’s set in Twilio. --- .../dashboard/i18n/locale/en/inboxMgmt.json | 11 ++- .../dashboard/settings/inbox/Settings.vue | 25 +++-- .../settings/inbox/channels/Voice.vue | 17 ---- .../inbox/settingsPage/ConfigurationPage.vue | 19 ++++ app/javascript/shared/mixins/inboxMixin.js | 6 +- app/views/api/v1/models/_inbox.json.jbuilder | 6 ++ enterprise/app/models/channel/voice.rb | 40 +++++++- .../twilio/voice_webhook_setup_service.rb | 99 +++++++++++++++++++ .../v1/accounts/inboxes_controller_spec.rb | 6 +- spec/enterprise/models/channel/voice_spec.rb | 21 +++- .../voice_webhook_setup_service_spec.rb | 88 +++++++++++++++++ spec/factories/channel/channel_voice.rb | 3 +- 12 files changed, 302 insertions(+), 39 deletions(-) create mode 100644 enterprise/app/services/twilio/voice_webhook_setup_service.rb create mode 100644 spec/enterprise/services/twilio/voice_webhook_setup_service_spec.rb diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 4006212a3..6f44ec046 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -330,13 +330,14 @@ "LABEL": "API Key Secret", "PLACEHOLDER": "Enter your Twilio API Key Secret", "REQUIRED": "API Key Secret is required" - }, - "TWIML_APP_SID": { - "LABEL": "TwiML App SID", - "PLACEHOLDER": "Enter your Twilio TwiML App SID (starts with AP)", - "REQUIRED": "TwiML App SID is required" } }, + "CONFIGURATION": { + "TWILIO_VOICE_URL_TITLE": "Twilio Voice URL", + "TWILIO_VOICE_URL_SUBTITLE": "Configure this URL as the Voice URL on your Twilio phone number and TwiML App.", + "TWILIO_STATUS_URL_TITLE": "Twilio Status Callback URL", + "TWILIO_STATUS_URL_SUBTITLE": "Configure this URL as the Status Callback URL on your Twilio phone number." + }, "SUBMIT_BUTTON": "Create Voice Channel", "API": { "ERROR_MESSAGE": "We were not able to create the voice channel" diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index 1db35af03..609fd3fd4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -116,16 +116,22 @@ export default { key: 'collaborators', name: this.$t('INBOX_MGMT.TABS.COLLABORATORS'), }, - { - key: 'businesshours', - name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'), - }, - { - key: 'csat', - name: this.$t('INBOX_MGMT.TABS.CSAT'), - }, ]; + if (!this.isAVoiceChannel) { + visibleToAllChannelTabs = [ + ...visibleToAllChannelTabs, + { + key: 'businesshours', + name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'), + }, + { + key: 'csat', + name: this.$t('INBOX_MGMT.TABS.CSAT'), + }, + ]; + } + if (this.isAWebWidgetInbox) { visibleToAllChannelTabs = [ ...visibleToAllChannelTabs, @@ -144,6 +150,7 @@ export default { this.isATwilioChannel || this.isALineChannel || this.isAPIInbox || + this.isAVoiceChannel || (this.isAnEmailChannel && !this.inbox.provider) || this.shouldShowWhatsAppConfiguration || this.isAWebWidgetInbox @@ -676,7 +683,7 @@ export default { }}