From 9232643440adf233ac04e5bd7140da1f0adf968e Mon Sep 17 00:00:00 2001 From: Arnaud Robin Date: Fri, 20 Sep 2024 17:00:08 +0200 Subject: [PATCH 1/4] =?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. --- src/frontend/apps/impress/.env | 3 +- src/frontend/apps/impress/package.json | 1 + .../impress/src/core/auth/useAuthStore.tsx | 6 +++ .../apps/impress/src/custom-next.d.ts | 1 + .../apps/impress/src/hook/useSupport.tsx | 43 +++++++++++++++++++ src/frontend/apps/impress/src/pages/_app.tsx | 4 ++ src/frontend/yarn.lock | 5 +++ 7 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/frontend/apps/impress/src/hook/useSupport.tsx diff --git a/src/frontend/apps/impress/.env b/src/frontend/apps/impress/.env index 41724eaab..d54786320 100644 --- a/src/frontend/apps/impress/.env +++ b/src/frontend/apps/impress/.env @@ -1,4 +1,5 @@ NEXT_PUBLIC_API_ORIGIN= NEXT_PUBLIC_Y_PROVIDER_URL= NEXT_PUBLIC_MEDIA_URL= -NEXT_PUBLIC_THEME=dsfr \ No newline at end of file +NEXT_PUBLIC_THEME=dsfr +NEXT_PUBLIC_CRISP_WEBSITE_ID= diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index 064eef88a..d257b0192 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -22,6 +22,7 @@ "@hocuspocus/provider": "2.13.5", "@openfun/cunningham-react": "2.9.4", "@tanstack/react-query": "5.56.2", + "crisp-sdk-web": "1.0.25", "i18next": "23.15.1", "idb": "8.0.0", "lodash": "4.17.21", diff --git a/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx b/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx index 64eeeb36b..82e3fc435 100644 --- a/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx +++ b/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx @@ -1,6 +1,10 @@ import { create } from 'zustand'; import { baseApiUrl } from '@/core/conf'; +import { + initializeSupportSession, + terminateSupportSession, +} from '@/hook/useSupport'; import { User, getMe } from './api'; import { PATH_AUTH_LOCAL_STORAGE } from './conf'; @@ -36,6 +40,7 @@ export const useAuthStore = create((set) => ({ return; } + initializeSupportSession(data); set({ authenticated: true, userData: data }); }) .catch(() => {}) @@ -53,6 +58,7 @@ export const useAuthStore = create((set) => ({ window.location.replace(`${baseApiUrl()}authenticate/`); }, logout: () => { + terminateSupportSession(); window.location.replace(`${baseApiUrl()}logout/`); }, })); diff --git a/src/frontend/apps/impress/src/custom-next.d.ts b/src/frontend/apps/impress/src/custom-next.d.ts index ff3906c6f..0d029e136 100644 --- a/src/frontend/apps/impress/src/custom-next.d.ts +++ b/src/frontend/apps/impress/src/custom-next.d.ts @@ -24,5 +24,6 @@ namespace NodeJS { NEXT_PUBLIC_Y_PROVIDER_URL?: string; NEXT_PUBLIC_SW_DEACTIVATED?: string; NEXT_PUBLIC_THEME?: string; + NEXT_PUBLIC_CRISP_WEBSITE_ID?: string; } } diff --git a/src/frontend/apps/impress/src/hook/useSupport.tsx b/src/frontend/apps/impress/src/hook/useSupport.tsx new file mode 100644 index 000000000..c7c2f55df --- /dev/null +++ b/src/frontend/apps/impress/src/hook/useSupport.tsx @@ -0,0 +1,43 @@ +import { Crisp } from 'crisp-sdk-web'; +import { useEffect } from 'react'; + +import { User } from '@/core'; + +const isCrispConfigured = (): boolean => { + return typeof window !== 'undefined' && !!window.$crisp; +}; + +export const initializeSupportSession = (user: User) => { + if (!isCrispConfigured()) { + return; + } + Crisp.setTokenId(user.id); + Crisp.user.setEmail(user.email); +}; + +export const terminateSupportSession = () => { + if (!isCrispConfigured()) { + return; + } + Crisp.session.reset(); +}; + +/** + * Configure Crisp chat for real-time support across all pages. + */ +export const useSupport = () => { + useEffect(() => { + const CRISP_WEBSITE_ID = process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID; + + if (!CRISP_WEBSITE_ID) { + console.warn('Crisp Website ID is not set'); + return; + } + if (isCrispConfigured()) { + return; + } + Crisp.configure(CRISP_WEBSITE_ID); + }, []); + + return null; +}; diff --git a/src/frontend/apps/impress/src/pages/_app.tsx b/src/frontend/apps/impress/src/pages/_app.tsx index 48b8a2d09..df8cd1f94 100644 --- a/src/frontend/apps/impress/src/pages/_app.tsx +++ b/src/frontend/apps/impress/src/pages/_app.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { AppProvider } from '@/core/'; import { useSWRegister } from '@/features/service-worker/'; +import { useSupport } from '@/hook/useSupport'; import { NextPageWithLayout } from '@/types/next'; import './globals.css'; @@ -17,6 +18,9 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) { const getLayout = Component.getLayout ?? ((page) => page); const { t } = useTranslation(); + // Initialize Crisp, the support chat used among all La Suite products + useSupport(); + return ( <> diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index fb79fbb38..9e3768e68 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -5105,6 +5105,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" From 87e82a5f703f1a6298ea8f5ac9baf04f37784293 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 23 Sep 2024 12:12:34 +0200 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=94=90(CI)=20add=20Crisp=20secret?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We need to add the Crisp secret at build time to the frontend container. The frontend is build from the CI pipeline and the secret is added to the container as an environment variable. --- .github/workflows/docker-hub.yml | 5 ++++- secrets | 2 +- src/frontend/Dockerfile | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-hub.yml b/.github/workflows/docker-hub.yml index 000e62543..4c7b4e7ff 100644 --- a/.github/workflows/docker-hub.yml +++ b/.github/workflows/docker-hub.yml @@ -13,6 +13,7 @@ on: env: DOCKER_USER: 1001:127 + CRISP_WEBSITE_ID: "" jobs: build-and-push-backend: @@ -99,7 +100,9 @@ jobs: context: . file: ./src/frontend/Dockerfile target: frontend-production - build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000 + build-args: | + DOCKER_USER=${{ env.DOCKER_USER }}:-1000 + CRISP_WEBSITE_ID=${{ env.CRISP_WEBSITE_ID }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/secrets b/secrets index 2643697e5..551d72daa 160000 --- a/secrets +++ b/secrets @@ -1 +1 @@ -Subproject commit 2643697e5f15279f0eca403ba591260e71ec499d +Subproject commit 551d72daa07e9a71b81b0ad211ae132205730bcd diff --git a/src/frontend/Dockerfile b/src/frontend/Dockerfile index d00319b68..0d1d6964c 100644 --- a/src/frontend/Dockerfile +++ b/src/frontend/Dockerfile @@ -70,6 +70,9 @@ ENV NEXT_PUBLIC_Y_PROVIDER_URL=${Y_PROVIDER_URL} ARG API_ORIGIN ENV NEXT_PUBLIC_API_ORIGIN=${API_ORIGIN} +ARG CRISP_WEBSITE_ID +ENV NEXT_PUBLIC_CRISP_WEBSITE_ID=${CRISP_WEBSITE_ID} + RUN yarn build # ---- Front-end image ---- From 574c167c6a299cdde1afd1bdeebe6c2733ed7749 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Mon, 23 Sep 2024 13:37:13 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fixup!=20=E2=9C=A8(frontend)=20add=20crisp?= =?UTF-8?q?=20chatbot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/auth/__tests__/useAuthStore.test.tsx | 56 +++++++++++++++++++ .../src/hook/__tests__/useSupport.test.tsx | 30 ++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/frontend/apps/impress/src/core/auth/__tests__/useAuthStore.test.tsx create mode 100644 src/frontend/apps/impress/src/hook/__tests__/useSupport.test.tsx 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..2bf7d9898 --- /dev/null +++ b/src/frontend/apps/impress/src/core/auth/__tests__/useAuthStore.test.tsx @@ -0,0 +1,56 @@ +import { waitFor } from '@testing-library/react'; +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: { + setTokenId: jest.fn(), + user: { + setEmail: jest.fn(), + }, + session: { + reset: jest.fn(), + }, + }, +})); + +describe('useAuthStore', () => { + afterEach(() => { + jest.clearAllMocks(); + fetchMock.restore(); + }); + + it('checks initialize support session when initAuth', async () => { + window.$crisp = true; + fetchMock.mock('end:users/me/', { + id: '123456', + email: 'test@email.com', + }); + + useAuthStore.getState().initAuth(); + + await waitFor(() => { + expect(Crisp.setTokenId).toHaveBeenCalledWith('123456'); + }); + + expect(Crisp.user.setEmail).toHaveBeenCalledWith('test@email.com'); + }); + + 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/hook/__tests__/useSupport.test.tsx b/src/frontend/apps/impress/src/hook/__tests__/useSupport.test.tsx new file mode 100644 index 000000000..d543c65ce --- /dev/null +++ b/src/frontend/apps/impress/src/hook/__tests__/useSupport.test.tsx @@ -0,0 +1,30 @@ +import { renderHook } from '@testing-library/react'; +import { Crisp } from 'crisp-sdk-web'; + +import { useSupport } from '../useSupport'; + +jest.mock('crisp-sdk-web', () => ({ + ...jest.requireActual('crisp-sdk-web'), + Crisp: { + configure: jest.fn(), + }, +})); + +describe('useSupport', () => { + afterEach(() => jest.clearAllMocks()); + + it('checks that env NEXT_PUBLIC_CRISP_WEBSITE_ID not set give a warning', () => { + process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID = ''; + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + renderHook(() => useSupport()); + expect(console.warn).toHaveBeenCalledWith('Crisp Website ID is not set'); + }); + + it('checks Crisp is configured', () => { + process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID = '123456'; + renderHook(() => useSupport()); + + expect(Crisp.configure).toHaveBeenCalledWith('123456'); + }); +}); From ed944d5c2aaf834950d61c9c0772fb00c98d44c0 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 23 Sep 2024 20:18:22 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fixup!=20=E2=9C=A8(frontend)=20add=20crisp?= =?UTF-8?q?=20chatbot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/frontend/apps/impress/src/hook/useSupport.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/frontend/apps/impress/src/hook/useSupport.tsx b/src/frontend/apps/impress/src/hook/useSupport.tsx index c7c2f55df..56c549c55 100644 --- a/src/frontend/apps/impress/src/hook/useSupport.tsx +++ b/src/frontend/apps/impress/src/hook/useSupport.tsx @@ -3,22 +3,19 @@ import { useEffect } from 'react'; import { User } from '@/core'; -const isCrispConfigured = (): boolean => { - return typeof window !== 'undefined' && !!window.$crisp; -}; - export const initializeSupportSession = (user: User) => { - if (!isCrispConfigured()) { + if (!Crisp.isCrispInjected()) { return; } - Crisp.setTokenId(user.id); + Crisp.setTokenId(`impress-${user.id}`); Crisp.user.setEmail(user.email); }; export const terminateSupportSession = () => { - if (!isCrispConfigured()) { + if (!Crisp.isCrispInjected()) { return; } + Crisp.setTokenId(); Crisp.session.reset(); }; @@ -33,7 +30,7 @@ export const useSupport = () => { console.warn('Crisp Website ID is not set'); return; } - if (isCrispConfigured()) { + if (Crisp.isCrispInjected()) { return; } Crisp.configure(CRISP_WEBSITE_ID);