Skip to content

Commit 6c10637

Browse files
committed
✨(frontend) add crisp chatbot
Integrate Crisp chatbot for immediate user support access. This enables real-time interaction, enhancing user experience by providing quick assistance.
1 parent af039d0 commit 6c10637

File tree

10 files changed

+120
-5
lines changed

10 files changed

+120
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to
1818
- ✨(backend) config endpoint #425
1919
- ✨(frontend) config endpoint #424
2020
- ✨(frontend) add sentry #424
21+
- ✨(frontend) add crisp chatbot #450
2122

2223
## Changed
2324

src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { expect, test } from '@playwright/test';
55
import { createDoc } from './common';
66

77
const config = {
8+
CRISP_WEBSITE_ID: null,
89
COLLABORATION_SERVER_URL: 'ws://localhost:4444',
910
ENVIRONMENT: 'development',
1011
FRONTEND_THEME: 'dsfr',
@@ -132,4 +133,28 @@ test.describe('Config', () => {
132133
const webSocket = await webSocketPromise;
133134
expect(webSocket.url()).toContain('ws://localhost:4444/');
134135
});
136+
137+
test('it checks that Crisp is trying to init from config endpoint', async ({
138+
page,
139+
}) => {
140+
await page.route('**/api/v1.0/config/', async (route) => {
141+
const request = route.request();
142+
if (request.method().includes('GET')) {
143+
await route.fulfill({
144+
json: {
145+
...config,
146+
CRISP_WEBSITE_ID: '1234',
147+
},
148+
});
149+
} else {
150+
await route.continue();
151+
}
152+
});
153+
154+
await page.goto('/');
155+
156+
await expect(
157+
page.locator('#crisp-chatbox').getByText('Invalid website'),
158+
).toBeVisible();
159+
});
135160
});

src/frontend/apps/impress/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@openfun/cunningham-react": "2.9.4",
2424
"@sentry/nextjs": "8.40.0",
2525
"@tanstack/react-query": "5.61.3",
26+
"crisp-sdk-web": "1.0.25",
2627
"i18next": "24.0.0",
2728
"i18next-browser-languagedetector": "8.0.0",
2829
"idb": "8.0.0",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { Crisp } from 'crisp-sdk-web';
2+
import fetchMock from 'fetch-mock';
3+
4+
import { useAuthStore } from '../useAuthStore';
5+
6+
jest.mock('crisp-sdk-web', () => ({
7+
...jest.requireActual('crisp-sdk-web'),
8+
Crisp: {
9+
isCrispInjected: jest.fn().mockReturnValue(true),
10+
setTokenId: jest.fn(),
11+
user: {
12+
setEmail: jest.fn(),
13+
},
14+
session: {
15+
reset: jest.fn(),
16+
},
17+
},
18+
}));
19+
20+
describe('useAuthStore', () => {
21+
afterEach(() => {
22+
jest.clearAllMocks();
23+
fetchMock.restore();
24+
});
25+
26+
it('checks support session is terminated when logout', () => {
27+
window.$crisp = true;
28+
Object.defineProperty(window, 'location', {
29+
value: {
30+
...window.location,
31+
replace: jest.fn(),
32+
},
33+
writable: true,
34+
});
35+
36+
useAuthStore.getState().logout();
37+
38+
expect(Crisp.session.reset).toHaveBeenCalled();
39+
});
40+
});

src/frontend/apps/impress/src/core/auth/useAuthStore.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { create } from 'zustand';
22

33
import { baseApiUrl } from '@/api';
4+
import { terminateCrispSession } from '@/services';
45

56
import { User, getMe } from './api';
67
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
@@ -42,6 +43,7 @@ export const useAuthStore = create<AuthStore>((set, get) => ({
4243
window.location.replace(`${baseApiUrl()}authenticate/`);
4344
},
4445
logout: () => {
46+
terminateCrispSession();
4547
window.location.replace(`${baseApiUrl()}logout/`);
4648
},
4749
// If we try to access a specific page and we are not authenticated

src/frontend/apps/impress/src/core/config/ConfigProvider.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { PropsWithChildren, useEffect } from 'react';
33

44
import { Box } from '@/components';
55
import { useCunninghamTheme } from '@/cunningham';
6+
import { configureCrispSession } from '@/services';
67
import { useSentryStore } from '@/stores/useSentryStore';
78

89
import { useConfig } from './api/useConfig';
@@ -28,6 +29,14 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
2829
setTheme(conf.FRONTEND_THEME);
2930
}, [conf?.FRONTEND_THEME, setTheme]);
3031

32+
useEffect(() => {
33+
if (!conf?.CRISP_WEBSITE_ID) {
34+
return;
35+
}
36+
37+
configureCrispSession(conf.CRISP_WEBSITE_ID);
38+
}, [conf?.CRISP_WEBSITE_ID]);
39+
3140
if (!conf) {
3241
return (
3342
<Box $height="100vh" $width="100vw" $align="center" $justify="center">

src/frontend/apps/impress/src/core/config/api/useConfig.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { APIError, errorCauses, fetchAPI } from '@/api';
44
import { Theme } from '@/cunningham/';
55

66
interface ConfigResponse {
7-
SENTRY_DSN: string;
8-
COLLABORATION_SERVER_URL: string;
9-
ENVIRONMENT: string;
10-
FRONTEND_THEME: Theme;
117
LANGUAGES: [string, string][];
128
LANGUAGE_CODE: string;
13-
MEDIA_BASE_URL: string;
9+
ENVIRONMENT: string;
10+
COLLABORATION_SERVER_URL?: string;
11+
CRISP_WEBSITE_ID?: string;
12+
FRONTEND_THEME?: Theme;
13+
MEDIA_BASE_URL?: string;
14+
SENTRY_DSN?: string;
1415
}
1516

1617
export const getConfig = async (): Promise<ConfigResponse> => {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Configure Crisp chat for real-time support across all pages.
3+
*/
4+
5+
import { Crisp } from 'crisp-sdk-web';
6+
7+
import { User } from '@/core';
8+
9+
export const initializeCrispSession = (user: User) => {
10+
if (!Crisp.isCrispInjected()) {
11+
return;
12+
}
13+
Crisp.setTokenId(`impress-${user.id}`);
14+
Crisp.user.setEmail(user.email);
15+
};
16+
17+
export const configureCrispSession = (websiteId: string) => {
18+
if (Crisp.isCrispInjected()) {
19+
return;
20+
}
21+
Crisp.configure(websiteId);
22+
};
23+
24+
export const terminateCrispSession = () => {
25+
if (!Crisp.isCrispInjected()) {
26+
return;
27+
}
28+
Crisp.setTokenId();
29+
Crisp.session.reset();
30+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Crisp';

src/frontend/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5930,6 +5930,11 @@ crelt@^1.0.0:
59305930
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
59315931
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
59325932

5933+
5934+
version "1.0.25"
5935+
resolved "https://registry.yarnpkg.com/crisp-sdk-web/-/crisp-sdk-web-1.0.25.tgz#5566227dfcc018435b228db2f998d66581e5fdef"
5936+
integrity sha512-CWTHFFeHRV0oqiXoPh/aIAKhFs6xcIM4NenGPnClAMCZUDQgQsF1OWmZWmnVNjJriXUmWRgDfeUxcxygS0dCRA==
5937+
59335938
cross-env@*, [email protected]:
59345939
version "7.0.3"
59355940
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"

0 commit comments

Comments
 (0)