From 2a173304bd7a63949f5792306293e44757390f1c Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 25 Nov 2024 15:59:24 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20crisp=20chatbot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrate Crisp chatbot for immediate user support access. This enables real-time interaction, enhancing user experience by providing quick assistance. --- CHANGELOG.md | 1 + .../e2e/__tests__/app-impress/config.spec.ts | 25 ++++++++++++ src/frontend/apps/impress/package.json | 1 + .../core/auth/__tests__/useAuthStore.test.tsx | 40 +++++++++++++++++++ .../impress/src/core/auth/useAuthStore.tsx | 2 + .../src/core/config/ConfigProvider.tsx | 9 +++++ .../impress/src/core/config/api/useConfig.tsx | 11 ++--- .../apps/impress/src/services/Crisp.tsx | 30 ++++++++++++++ .../apps/impress/src/services/index.ts | 1 + src/frontend/yarn.lock | 5 +++ 10 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 src/frontend/apps/impress/src/core/auth/__tests__/useAuthStore.test.tsx create mode 100644 src/frontend/apps/impress/src/services/Crisp.tsx create mode 100644 src/frontend/apps/impress/src/services/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f418b8226..de29895f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to - ✨(backend) config endpoint #425 - ✨(frontend) config endpoint #424 - ✨(frontend) add sentry #424 +- ✨(frontend) add crisp chatbot #273 ## Changed diff --git a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts index 2ae8746f7..e005ca3a5 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts @@ -5,6 +5,7 @@ import { expect, test } from '@playwright/test'; import { createDoc } from './common'; const config = { + CRISP_WEBSITE_ID: null, COLLABORATION_SERVER_URL: 'ws://localhost:4444', ENVIRONMENT: 'development', FRONTEND_THEME: 'dsfr', @@ -132,4 +133,28 @@ test.describe('Config', () => { const webSocket = await webSocketPromise; expect(webSocket.url()).toContain('ws://localhost:4444/'); }); + + test('it checks that Crisp is trying to init from config endpoint', async ({ + page, + }) => { + await page.route('**/api/v1.0/config/', async (route) => { + const request = route.request(); + if (request.method().includes('GET')) { + await route.fulfill({ + json: { + ...config, + CRISP_WEBSITE_ID: '1234', + }, + }); + } else { + await route.continue(); + } + }); + + await page.goto('/'); + + await expect( + page.locator('#crisp-chatbox').getByText('Invalid website'), + ).toBeVisible(); + }); }); diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index 97674f6f0..884758e85 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -23,6 +23,7 @@ "@openfun/cunningham-react": "2.9.4", "@sentry/nextjs": "8.40.0", "@tanstack/react-query": "5.61.3", + "crisp-sdk-web": "1.0.25", "i18next": "24.0.0", "i18next-browser-languagedetector": "8.0.0", "idb": "8.0.0", diff --git a/src/frontend/apps/impress/src/core/auth/__tests__/useAuthStore.test.tsx b/src/frontend/apps/impress/src/core/auth/__tests__/useAuthStore.test.tsx new file mode 100644 index 000000000..028146771 --- /dev/null +++ b/src/frontend/apps/impress/src/core/auth/__tests__/useAuthStore.test.tsx @@ -0,0 +1,40 @@ +import { Crisp } from 'crisp-sdk-web'; +import fetchMock from 'fetch-mock'; + +import { useAuthStore } from '../useAuthStore'; + +jest.mock('crisp-sdk-web', () => ({ + ...jest.requireActual('crisp-sdk-web'), + Crisp: { + isCrispInjected: jest.fn().mockReturnValue(true), + setTokenId: jest.fn(), + user: { + setEmail: jest.fn(), + }, + session: { + reset: jest.fn(), + }, + }, +})); + +describe('useAuthStore', () => { + afterEach(() => { + jest.clearAllMocks(); + fetchMock.restore(); + }); + + it('checks support session is terminated when logout', () => { + window.$crisp = true; + Object.defineProperty(window, 'location', { + value: { + ...window.location, + replace: jest.fn(), + }, + writable: true, + }); + + useAuthStore.getState().logout(); + + expect(Crisp.session.reset).toHaveBeenCalled(); + }); +}); diff --git a/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx b/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx index 09283db90..63e285b27 100644 --- a/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx +++ b/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx @@ -1,6 +1,7 @@ import { create } from 'zustand'; import { baseApiUrl } from '@/api'; +import { terminateCrispSession } from '@/services'; import { User, getMe } from './api'; import { PATH_AUTH_LOCAL_STORAGE } from './conf'; @@ -42,6 +43,7 @@ export const useAuthStore = create((set, get) => ({ window.location.replace(`${baseApiUrl()}authenticate/`); }, logout: () => { + terminateCrispSession(); window.location.replace(`${baseApiUrl()}logout/`); }, // If we try to access a specific page and we are not authenticated diff --git a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx index 217866e9e..570331107 100644 --- a/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx +++ b/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx @@ -3,6 +3,7 @@ import { PropsWithChildren, useEffect } from 'react'; import { Box } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; +import { configureCrispSession } from '@/services'; import { useSentryStore } from '@/stores/useSentryStore'; import { useConfig } from './api/useConfig'; @@ -28,6 +29,14 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => { setTheme(conf.FRONTEND_THEME); }, [conf?.FRONTEND_THEME, setTheme]); + useEffect(() => { + if (!conf?.CRISP_WEBSITE_ID) { + return; + } + + configureCrispSession(conf.CRISP_WEBSITE_ID); + }, [conf?.CRISP_WEBSITE_ID]); + if (!conf) { return ( diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index 8717c56b6..04bb06c6a 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -4,13 +4,14 @@ import { APIError, errorCauses, fetchAPI } from '@/api'; import { Theme } from '@/cunningham/'; interface ConfigResponse { - SENTRY_DSN: string; - COLLABORATION_SERVER_URL: string; - ENVIRONMENT: string; - FRONTEND_THEME: Theme; LANGUAGES: [string, string][]; LANGUAGE_CODE: string; - MEDIA_BASE_URL: string; + ENVIRONMENT: string; + COLLABORATION_SERVER_URL?: string; + CRISP_WEBSITE_ID?: string; + FRONTEND_THEME?: Theme; + MEDIA_BASE_URL?: string; + SENTRY_DSN?: string; } export const getConfig = async (): Promise => { diff --git a/src/frontend/apps/impress/src/services/Crisp.tsx b/src/frontend/apps/impress/src/services/Crisp.tsx new file mode 100644 index 000000000..f0002aebc --- /dev/null +++ b/src/frontend/apps/impress/src/services/Crisp.tsx @@ -0,0 +1,30 @@ +/** + * Configure Crisp chat for real-time support across all pages. + */ + +import { Crisp } from 'crisp-sdk-web'; + +import { User } from '@/core'; + +export const initializeCrispSession = (user: User) => { + if (!Crisp.isCrispInjected()) { + return; + } + Crisp.setTokenId(`impress-${user.id}`); + Crisp.user.setEmail(user.email); +}; + +export const configureCrispSession = (websiteId: string) => { + if (Crisp.isCrispInjected()) { + return; + } + Crisp.configure(websiteId); +}; + +export const terminateCrispSession = () => { + if (!Crisp.isCrispInjected()) { + return; + } + Crisp.setTokenId(); + Crisp.session.reset(); +}; diff --git a/src/frontend/apps/impress/src/services/index.ts b/src/frontend/apps/impress/src/services/index.ts new file mode 100644 index 000000000..08bbf631f --- /dev/null +++ b/src/frontend/apps/impress/src/services/index.ts @@ -0,0 +1 @@ +export * from './Crisp'; diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 9453c1427..357288ed3 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -5930,6 +5930,11 @@ crelt@^1.0.0: resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== +crisp-sdk-web@1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/crisp-sdk-web/-/crisp-sdk-web-1.0.25.tgz#5566227dfcc018435b228db2f998d66581e5fdef" + integrity sha512-CWTHFFeHRV0oqiXoPh/aIAKhFs6xcIM4NenGPnClAMCZUDQgQsF1OWmZWmnVNjJriXUmWRgDfeUxcxygS0dCRA== + cross-env@*, cross-env@7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"