diff --git a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb
index e7a1f3fa6..3e7d876c3 100644
--- a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb
+++ b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb
@@ -1,8 +1,10 @@
class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController
before_action :validate_feature_enabled!
+ before_action :fetch_and_validate_inbox, if: -> { params[:inbox_id].present? }
# POST /api/v1/accounts/:account_id/whatsapp/authorization
- # Handles the embedded signup callback data from the Facebook SDK
+ # Handles both initial authorization and reauthorization
+ # If inbox_id is present in params, it performs reauthorization
def create
validate_embedded_signup_params!
channel = process_embedded_signup
@@ -16,21 +18,42 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts:
def process_embedded_signup
service = Whatsapp::EmbeddedSignupService.new(
account: Current.account,
- code: params[:code],
- business_id: params[:business_id],
- waba_id: params[:waba_id],
- phone_number_id: params[:phone_number_id]
+ params: params.permit(:code, :business_id, :waba_id, :phone_number_id).to_h.symbolize_keys,
+ inbox_id: params[:inbox_id]
)
service.perform
end
- def render_success_response(inbox)
+ def fetch_and_validate_inbox
+ @inbox = Current.account.inboxes.find(params[:inbox_id])
+ validate_reauthorization_required
+ end
+
+ def validate_reauthorization_required
+ return if @inbox.channel.reauthorization_required? || can_upgrade_to_embedded_signup?
+
render json: {
+ success: false,
+ message: I18n.t('inbox.reauthorization.not_required')
+ }, status: :unprocessable_entity
+ end
+
+ def can_upgrade_to_embedded_signup?
+ channel = @inbox.channel
+ return false unless channel.provider == 'whatsapp_cloud'
+
+ true
+ end
+
+ def render_success_response(inbox)
+ response = {
success: true,
id: inbox.id,
name: inbox.name,
channel_type: 'whatsapp'
}
+ response[:message] = I18n.t('inbox.reauthorization.success') if params[:inbox_id].present?
+ render json: response
end
def render_error_response(error)
diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb
index 455828228..d607ba151 100644
--- a/app/controllers/twilio/callback_controller.rb
+++ b/app/controllers/twilio/callback_controller.rb
@@ -30,7 +30,8 @@ class Twilio::CallbackController < ApplicationController
:NumMedia,
:Latitude,
:Longitude,
- :MessageType
+ :MessageType,
+ :ProfileName
)
end
end
diff --git a/app/finders/notification_finder.rb b/app/finders/notification_finder.rb
index ccfe470a0..e1958827a 100644
--- a/app/finders/notification_finder.rb
+++ b/app/finders/notification_finder.rb
@@ -15,7 +15,13 @@ class NotificationFinder
end
def unread_count
- @notifications.where(read_at: nil).count
+ if type_included?('read')
+ # If we're including read notifications, filter to unread
+ @notifications.where(read_at: nil).count
+ else
+ # Already filtered to unread notifications, just count
+ @notifications.count
+ end
end
def count
@@ -27,7 +33,7 @@ class NotificationFinder
def set_up
find_all_notifications
filter_snoozed_notifications
- fitler_read_notifications
+ filter_read_notifications
end
def find_all_notifications
@@ -38,7 +44,7 @@ class NotificationFinder
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
end
- def fitler_read_notifications
+ def filter_read_notifications
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
end
diff --git a/app/javascript/dashboard/api/channel/whatsappChannel.js b/app/javascript/dashboard/api/channel/whatsappChannel.js
index e1003b123..8f51f4878 100644
--- a/app/javascript/dashboard/api/channel/whatsappChannel.js
+++ b/app/javascript/dashboard/api/channel/whatsappChannel.js
@@ -9,6 +9,13 @@ class WhatsappChannel extends ApiClient {
createEmbeddedSignup(params) {
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params);
}
+
+ reauthorizeWhatsApp({ inboxId, ...params }) {
+ return axios.post(`${this.baseUrl()}/whatsapp/authorization`, {
+ ...params,
+ inbox_id: inboxId,
+ });
+ }
}
export default new WhatsappChannel();
diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue
index 12a789fee..f4301375d 100644
--- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue
+++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue
@@ -38,11 +38,13 @@ const handleClose = () => emit('close');
-
- {{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
-
-
+
+
+ {{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue
index 0932a79c7..0e893b767 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue
@@ -98,6 +98,7 @@ const onClickViewDetails = () => emit('showContact', props.id);
:src="thumbnail"
:size="48"
:status="availabilityStatus"
+ hide-offline-status
rounded-full
/>
diff --git a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue
index 79a451404..90cc1ff53 100644
--- a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue
+++ b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue
@@ -63,11 +63,12 @@ const lastActivityAt = computed(() => {
});
const menuItems = computed(() => [
- { key: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
{
key: isUnread.value ? 'mark_as_read' : 'mark_as_unread',
+ icon: isUnread.value ? 'mail' : 'mail-unread',
label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`),
},
+ { key: 'delete', icon: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
]);
const messageClasses = computed(() => ({
diff --git a/app/javascript/dashboard/components-next/avatar/Avatar.vue b/app/javascript/dashboard/components-next/avatar/Avatar.vue
index 65859b1c7..08224995c 100644
--- a/app/javascript/dashboard/components-next/avatar/Avatar.vue
+++ b/app/javascript/dashboard/components-next/avatar/Avatar.vue
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
import { removeEmoji } from 'shared/helpers/emoji';
import Icon from 'dashboard/components-next/icon/Icon.vue';
+import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
import wootConstants from 'dashboard/constants/globals';
const props = defineProps({
@@ -33,10 +34,18 @@ const props = defineProps({
validator: value =>
!value || wootConstants.AVAILABILITY_STATUS_KEYS.includes(value),
},
+ inbox: {
+ type: Object,
+ default: null,
+ },
iconName: {
type: String,
default: null,
},
+ hideOfflineStatus: {
+ type: Boolean,
+ default: false,
+ },
});
const emit = defineEmits(['upload', 'delete']);
@@ -66,11 +75,11 @@ const AVATAR_COLORS = {
default: { bg: '#E8E8E8', text: '#60646C' },
};
-const STATUS_CLASSES = {
+const STATUS_CLASSES = computed(() => ({
online: 'bg-n-teal-10',
busy: 'bg-n-amber-10',
- offline: 'bg-n-slate-10',
-};
+ ...(props.hideOfflineStatus ? {} : { offline: 'bg-n-slate-10' }),
+}));
const showDefaultAvatar = computed(() => !props.src && !props.name);
@@ -178,11 +187,18 @@ watch(
+
+
+
@@ -239,24 +255,33 @@ watch(
-
-
-
-
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/icon/ChannelIcon.vue b/app/javascript/dashboard/components-next/icon/ChannelIcon.vue
index 11117cc18..68102dbd3 100644
--- a/app/javascript/dashboard/components-next/icon/ChannelIcon.vue
+++ b/app/javascript/dashboard/components-next/icon/ChannelIcon.vue
@@ -1,4 +1,5 @@
diff --git a/app/javascript/dashboard/components-next/icon/provider.js b/app/javascript/dashboard/components-next/icon/provider.js
index 36dd6216e..887368438 100644
--- a/app/javascript/dashboard/components-next/icon/provider.js
+++ b/app/javascript/dashboard/components-next/icon/provider.js
@@ -22,15 +22,21 @@ export function useChannelIcon(inbox) {
};
const channelIcon = computed(() => {
- const type = inbox.channel_type;
+ const inboxDetails = inbox.value || inbox;
+ const type = inboxDetails.channel_type;
let icon = channelTypeIconMap[type];
- if (type === 'Channel::Email' && inbox.provider) {
- if (Object.keys(providerIconMap).includes(inbox.provider)) {
- icon = providerIconMap[inbox.provider];
+ if (type === 'Channel::Email' && inboxDetails.provider) {
+ if (Object.keys(providerIconMap).includes(inboxDetails.provider)) {
+ icon = providerIconMap[inboxDetails.provider];
}
}
+ // Special case for Twilio whatsapp
+ if (type === 'Channel::TwilioSms' && inboxDetails.medium === 'whatsapp') {
+ icon = 'i-ri-whatsapp-fill';
+ }
+
return icon ?? 'i-ri-global-fill';
});
diff --git a/app/javascript/dashboard/components-next/icon/specs/provider.spec.js b/app/javascript/dashboard/components-next/icon/specs/provider.spec.js
index 5860e30ea..b0b820f25 100644
--- a/app/javascript/dashboard/components-next/icon/specs/provider.spec.js
+++ b/app/javascript/dashboard/components-next/icon/specs/provider.spec.js
@@ -25,6 +25,77 @@ describe('useChannelIcon', () => {
expect(icon).toBe('i-ri-phone-fill');
});
+ it('returns correct icon for Line channel', () => {
+ const inbox = { channel_type: 'Channel::Line' };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-line-fill');
+ });
+
+ it('returns correct icon for SMS channel', () => {
+ const inbox = { channel_type: 'Channel::Sms' };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-chat-1-fill');
+ });
+
+ it('returns correct icon for Telegram channel', () => {
+ const inbox = { channel_type: 'Channel::Telegram' };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-telegram-fill');
+ });
+
+ it('returns correct icon for Twitter channel', () => {
+ const inbox = { channel_type: 'Channel::TwitterProfile' };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-twitter-x-fill');
+ });
+
+ it('returns correct icon for WebWidget channel', () => {
+ const inbox = { channel_type: 'Channel::WebWidget' };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-global-fill');
+ });
+
+ it('returns correct icon for Instagram channel', () => {
+ const inbox = { channel_type: 'Channel::Instagram' };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-instagram-fill');
+ });
+
+ describe('TwilioSms channel', () => {
+ it('returns chat icon for regular Twilio SMS channel', () => {
+ const inbox = { channel_type: 'Channel::TwilioSms' };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-chat-1-fill');
+ });
+
+ it('returns WhatsApp icon for Twilio SMS with WhatsApp medium', () => {
+ const inbox = {
+ channel_type: 'Channel::TwilioSms',
+ medium: 'whatsapp',
+ };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-whatsapp-fill');
+ });
+
+ it('returns chat icon for Twilio SMS with non-WhatsApp medium', () => {
+ const inbox = {
+ channel_type: 'Channel::TwilioSms',
+ medium: 'sms',
+ };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-chat-1-fill');
+ });
+
+ it('returns chat icon for Twilio SMS with undefined medium', () => {
+ const inbox = {
+ channel_type: 'Channel::TwilioSms',
+ medium: undefined,
+ };
+ const { value: icon } = useChannelIcon(inbox);
+ expect(icon).toBe('i-ri-chat-1-fill');
+ });
+ });
+
describe('Email channel', () => {
it('returns mail icon for generic email channel', () => {
const inbox = { channel_type: 'Channel::Email' };
diff --git a/app/javascript/dashboard/components/ChatListHeader.vue b/app/javascript/dashboard/components/ChatListHeader.vue
index 04a22806d..cc53e2e39 100644
--- a/app/javascript/dashboard/components/ChatListHeader.vue
+++ b/app/javascript/dashboard/components/ChatListHeader.vue
@@ -98,7 +98,7 @@ const toggleConversationLayout = () => {
/>
@@ -124,7 +124,7 @@ const toggleConversationLayout = () => {
/>
@@ -150,7 +150,7 @@ const toggleConversationLayout = () => {
/>
diff --git a/app/javascript/dashboard/components/app/UpgradeBanner.vue b/app/javascript/dashboard/components/app/UpgradeBanner.vue
deleted file mode 100644
index d41ce3438..000000000
--- a/app/javascript/dashboard/components/app/UpgradeBanner.vue
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
-
-
-
-
diff --git a/app/javascript/dashboard/components/copilot/CopilotContainer.vue b/app/javascript/dashboard/components/copilot/CopilotContainer.vue
index 41ffa6b61..da0e69492 100644
--- a/app/javascript/dashboard/components/copilot/CopilotContainer.vue
+++ b/app/javascript/dashboard/components/copilot/CopilotContainer.vue
@@ -4,6 +4,7 @@ import { useStore } from 'dashboard/composables/store';
import Copilot from 'dashboard/components-next/copilot/Copilot.vue';
import { useMapGetter } from 'dashboard/composables/store';
import { useUISettings } from 'dashboard/composables/useUISettings';
+import { useConfig } from 'dashboard/composables/useConfig';
import { useWindowSize } from '@vueuse/core';
import { vOnClickOutside } from '@vueuse/components';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
@@ -18,6 +19,7 @@ defineProps({
const store = useStore();
const { uiSettings, updateUISettings } = useUISettings();
+const { isEnterprise } = useConfig();
const { width: windowWidth } = useWindowSize();
const currentUser = useMapGetter('getCurrentUser');
@@ -82,6 +84,9 @@ const setAssistant = async assistant => {
};
const shouldShowCopilotPanel = computed(() => {
+ if (!isEnterprise) {
+ return false;
+ }
const isCaptainEnabled = isFeatureEnabledonAccount.value(
currentAccountId.value,
FEATURE_FLAGS.CAPTAIN
@@ -113,7 +118,9 @@ const sendMessage = async message => {
};
onMounted(() => {
- store.dispatch('captainAssistants/get');
+ if (isEnterprise) {
+ store.dispatch('captainAssistants/get');
+ }
});
diff --git a/app/javascript/dashboard/components/ui/Wizard.vue b/app/javascript/dashboard/components/ui/Wizard.vue
index 2219a07c1..6ab196d9d 100644
--- a/app/javascript/dashboard/components/ui/Wizard.vue
+++ b/app/javascript/dashboard/components/ui/Wizard.vue
@@ -1,9 +1,5 @@
-
-
+
- {{ inbox.name }}
+
+ {{ inbox.name }}
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
index 30e99ca9d..cfac915bf 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue
@@ -1,12 +1,12 @@
-
-
-
+ :inbox="inbox"
+ :class="!showInboxName ? 'mt-4' : 'mt-8'"
+ hide-offline-status
+ rounded-full
+ >
+
+
+
+