diff --git a/.changeset/healthy-rivers-nail.md b/.changeset/healthy-rivers-nail.md new file mode 100644 index 000000000000..a8da9bec846e --- /dev/null +++ b/.changeset/healthy-rivers-nail.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +"@rocket.chat/livechat": minor +--- + +Added new setting `Allow visitors to finish conversations` that allows admins to decide if omnichannel visitors can close a conversation or not. This doesn't affect agent's capabilities of room closing, neither apps using the livechat bridge to close rooms. +However, if currently your integration relies on `livechat/room.close` endpoint for closing conversations, it's advised to use the authenticated version `livechat/room.closeByUser` of it before turning off this setting. diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.ts b/apps/meteor/app/livechat/imports/server/rest/appearance.ts index 48863fc9e5d3..7496b6243abe 100644 --- a/apps/meteor/app/livechat/imports/server/rest/appearance.ts +++ b/apps/meteor/app/livechat/imports/server/rest/appearance.ts @@ -51,6 +51,7 @@ API.v1.addRoute( 'Livechat_background', 'Livechat_widget_position', 'Livechat_hide_system_messages', + 'Omnichannel_allow_visitors_to_close_conversation', ]; const valid = settings.every((setting) => validSettingList.includes(setting._id)); diff --git a/apps/meteor/app/livechat/server/api/lib/appearance.ts b/apps/meteor/app/livechat/server/api/lib/appearance.ts index 785413ead9d1..0fc7d3547b2c 100644 --- a/apps/meteor/app/livechat/server/api/lib/appearance.ts +++ b/apps/meteor/app/livechat/server/api/lib/appearance.ts @@ -28,6 +28,7 @@ export async function findAppearance(): Promise<{ appearance: ISetting[] }> { 'Livechat_background', 'Livechat_widget_position', 'Livechat_hide_system_messages', + 'Omnichannel_allow_visitors_to_close_conversation', ], }, }; diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index a922edd40899..8041566d796e 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -142,6 +142,7 @@ export async function settings({ businessUnit = '' }: { businessUnit?: string } hiddenSystemMessages: initSettings.Livechat_hide_system_messages, livechatLogo: initSettings.Assets_livechat_widget_logo, hideWatermark: initSettings.Livechat_hide_watermark || false, + visitorsCanCloseChat: initSettings.Omnichannel_allow_visitors_to_close_conversation, }, theme: { title: initSettings.Livechat_title, diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 565f8e0bb3f4..7aacfacb4476 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -107,6 +107,10 @@ API.v1.addRoute( async post() { const { rid, token } = this.bodyParams; + if (!rcSettings.get('Omnichannel_allow_visitors_to_close_conversation')) { + throw new Error('error-not-allowed-to-close-conversation'); + } + const visitor = await findGuest(token); if (!visitor) { throw new Error('invalid-token'); diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 88f9494159a2..89d125033977 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1079,6 +1079,7 @@ class LivechatClass { 'Livechat_background', 'Assets_livechat_widget_logo', 'Livechat_hide_watermark', + 'Omnichannel_allow_visitors_to_close_conversation', ] as const; type SettingTypes = (typeof validSettings)[number] | 'Livechat_Show_Connecting'; diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx index 4253ff023ee9..a4435398d9a9 100644 --- a/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx +++ b/apps/meteor/client/views/omnichannel/appearance/AppearanceForm.tsx @@ -52,6 +52,7 @@ const AppearanceForm = () => { const livechatWidgetPositionField = useUniqueId(); const livechatBackgroundField = useUniqueId(); const livechatHideSystemMessagesField = useUniqueId(); + const omnichannelVisitorsCanCloseConversationField = useUniqueId(); return ( @@ -140,6 +141,20 @@ const AppearanceForm = () => { /> + + + + {t('Omnichannel_allow_visitors_to_close_conversation')} + + ( + + )} + /> + + diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx index a2cfb7b8103b..b90c32af6a7d 100644 --- a/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx +++ b/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx @@ -28,6 +28,7 @@ type LivechatAppearanceSettings = { Livechat_conversation_finished_text: string; Livechat_enable_message_character_limit: boolean; Livechat_message_character_limit: number; + Omnichannel_allow_visitors_to_close_conversation: boolean; }; type AppearanceSettings = Partial; diff --git a/apps/meteor/server/settings/omnichannel.ts b/apps/meteor/server/settings/omnichannel.ts index ed1daa8ce228..c86cd6674d4e 100644 --- a/apps/meteor/server/settings/omnichannel.ts +++ b/apps/meteor/server/settings/omnichannel.ts @@ -157,6 +157,13 @@ export const createOmniSettings = () => i18nLabel: 'Show_agent_email', }); + await this.add('Omnichannel_allow_visitors_to_close_conversation', true, { + type: 'boolean', + group: 'Omnichannel', + public: true, + enableQuery: omnichannelEnabledQuery, + }); + await this.add('Livechat_request_comment_when_closing_conversation', true, { type: 'boolean', group: 'Omnichannel', diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts index bf14584ed89f..405e7f82e3c4 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat.spec.ts @@ -2,6 +2,7 @@ import { createFakeVisitor } from '../../mocks/data'; import { createAuxContext } from '../fixtures/createAuxContext'; import { Users } from '../fixtures/userStates'; import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects'; +import { setSettingValueById } from '../utils'; import { createAgent } from '../utils/omnichannel/agents'; import { test, expect } from '../utils/test'; @@ -93,6 +94,57 @@ test.describe.serial('OC - Livechat', () => { }); }); +test.describe.serial('OC - Livechat - Visitors closing the room is disabled', () => { + let poLiveChat: OmnichannelLiveChat; + let poHomeOmnichannel: HomeOmnichannel; + + test.beforeAll(async ({ api }) => { + await api.post('/livechat/users/agent', { username: 'user1' }); + }); + + test.beforeAll(async ({ browser, api }) => { + const { page: livechatPage } = await createAuxContext(browser, Users.user1, '/livechat', false); + + poLiveChat = new OmnichannelLiveChat(livechatPage, api); + }); + + test.beforeAll(async ({ browser, api }) => { + await setSettingValueById(api, 'Livechat_allow_visitor_closing_chat', false); + const { page: omniPage } = await createAuxContext(browser, Users.user1, '/', true); + poHomeOmnichannel = new HomeOmnichannel(omniPage); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Livechat_allow_visitor_closing_chat', true); + await api.delete('/livechat/users/agent/user1'); + await poLiveChat.page.close(); + }); + + test('OC - Livechat - Close Chat disabled', async () => { + await poLiveChat.page.reload(); + await poLiveChat.openAnyLiveChat(); + await poLiveChat.sendMessage(firstVisitor, false); + await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_user'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + + await test.step('expect to close a livechat conversation', async () => { + await expect(poLiveChat.btnOptions).not.toBeVisible(); + await expect(poLiveChat.btnCloseChat).not.toBeVisible(); + }); + }); + + test('OC - Livechat - Close chat disabled, agents can close', async () => { + await poHomeOmnichannel.sidenav.openChat(firstVisitor.name); + + await test.step('expect livechat conversation to be closed by agent', async () => { + await poHomeOmnichannel.content.btnCloseChat.click(); + await poHomeOmnichannel.content.closeChatModal.inputComment.fill('this_is_a_test_comment'); + await poHomeOmnichannel.content.closeChatModal.btnConfirm.click(); + await expect(poHomeOmnichannel.toastSuccess).toBeVisible(); + }); + }); +}); + test.describe.serial('OC - Livechat - Resub after close room', () => { let poLiveChat: OmnichannelLiveChat; let poHomeOmnichannel: HomeOmnichannel; diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 7142725a1d99..4388a4d24341 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -689,6 +689,25 @@ describe('LIVECHAT - rooms', () => { expect(latestRoom).to.not.have.property('pdfTranscriptFileId'); }, ); + + describe('Special case: visitors closing is disabled', () => { + before(async () => { + await updateSetting('Omnichannel_allow_visitors_to_close_conversation', false); + }); + after(async () => { + await updateSetting('Omnichannel_allow_visitors_to_close_conversation', true); + }); + it('should not allow visitor to close a conversation', async () => { + const { room, visitor } = await startANewLivechatRoomAndTakeIt(); + await request + .post(api('livechat/room.close')) + .send({ + token: visitor.token, + rid: room._id, + }) + .expect(400); + }); + }); }); describe('livechat/room.forward', () => { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index ea7e31422cb1..d3324c6e749a 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -4037,6 +4037,8 @@ "Omnichannel_Reports_Summary": "Gain insights into your operation and export your metrics.", "Omnichannel_max_fallback_forward_depth": "Maximum fallback forward departments depth", "Omnichannel_max_fallback_forward_depth_Description": "Maximum number of hops that a room being transfered will do when the target department has a Fallback Forward Department set up. When limit is reached, chat won't be transferred and process will stop. Depending on your configuration, setting a high number may cause performance issues.", + "Omnichannel_allow_visitors_to_close_conversation": "Allow visitors to finish conversations", + "Omnichannel_allow_visitors_to_close_conversation_Description": "When disabled, visitors won't be able to finish an ongoing conversation either via UI or via API.", "On": "On", "on-hold-livechat-room": "On Hold Omnichannel Room", "on-hold-livechat-room_description": "Permission to on hold omnichannel room", diff --git a/packages/livechat/src/routes/Chat/connector.tsx b/packages/livechat/src/routes/Chat/connector.tsx index 36e574b246b1..3c72f9ae88cf 100644 --- a/packages/livechat/src/routes/Chat/connector.tsx +++ b/packages/livechat/src/routes/Chat/connector.tsx @@ -22,6 +22,7 @@ export const ChatConnector: FunctionalComponent<{ path: string; default: boolean nameFieldRegistrationForm, emailFieldRegistrationForm, limitTextLength, + visitorsCanCloseChat, }, messages: { conversationFinishedMessage }, theme: { title = '' } = {}, @@ -94,6 +95,7 @@ export const ChatConnector: FunctionalComponent<{ path: string; default: boolean ongoingCall={ongoingCall} messageListPosition={messageListPosition} theme={theme} + visitorsCanCloseChat={visitorsCanCloseChat} /> ); }; diff --git a/packages/livechat/src/routes/Chat/container.js b/packages/livechat/src/routes/Chat/container.js index 19172cc7fe5a..43ff281c6472 100644 --- a/packages/livechat/src/routes/Chat/container.js +++ b/packages/livechat/src/routes/Chat/container.js @@ -288,8 +288,8 @@ class ChatContainer extends Component { }; canFinishChat = () => { - const { room, connecting } = this.props; - return room !== undefined || connecting; + const { room, connecting, visitorsCanCloseChat } = this.props; + return visitorsCanCloseChat && (room !== undefined || connecting); }; canRemoveUserData = () => { diff --git a/packages/livechat/src/store/index.tsx b/packages/livechat/src/store/index.tsx index f8629ce693cc..e7d4b8caae17 100644 --- a/packages/livechat/src/store/index.tsx +++ b/packages/livechat/src/store/index.tsx @@ -59,6 +59,7 @@ export type StoreState = { hideWatermark?: boolean; livechatLogo?: { url: string }; transcript?: boolean; + visitorsCanCloseChat?: boolean; }; online?: boolean; departments: Department[];