✨(frontend) add crisp chatbot
Integrate Crisp chatbot for immediate user support access. This enables real-time interaction, enhancing user experience by providing quick assistance.
This commit is contained in:
@@ -18,6 +18,7 @@ and this project adheres to
|
|||||||
- ✨(backend) config endpoint #425
|
- ✨(backend) config endpoint #425
|
||||||
- ✨(frontend) config endpoint #424
|
- ✨(frontend) config endpoint #424
|
||||||
- ✨(frontend) add sentry #424
|
- ✨(frontend) add sentry #424
|
||||||
|
- ✨(frontend) add crisp chatbot #450
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { expect, test } from '@playwright/test';
|
|||||||
import { createDoc } from './common';
|
import { createDoc } from './common';
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
CRISP_WEBSITE_ID: null,
|
||||||
COLLABORATION_SERVER_URL: 'ws://localhost:4444',
|
COLLABORATION_SERVER_URL: 'ws://localhost:4444',
|
||||||
ENVIRONMENT: 'development',
|
ENVIRONMENT: 'development',
|
||||||
FRONTEND_THEME: 'dsfr',
|
FRONTEND_THEME: 'dsfr',
|
||||||
@@ -132,4 +133,28 @@ test.describe('Config', () => {
|
|||||||
const webSocket = await webSocketPromise;
|
const webSocket = await webSocketPromise;
|
||||||
expect(webSocket.url()).toContain('ws://localhost:4444/');
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@openfun/cunningham-react": "2.9.4",
|
"@openfun/cunningham-react": "2.9.4",
|
||||||
"@sentry/nextjs": "8.40.0",
|
"@sentry/nextjs": "8.40.0",
|
||||||
"@tanstack/react-query": "5.61.3",
|
"@tanstack/react-query": "5.61.3",
|
||||||
|
"crisp-sdk-web": "1.0.25",
|
||||||
"i18next": "24.0.0",
|
"i18next": "24.0.0",
|
||||||
"i18next-browser-languagedetector": "8.0.0",
|
"i18next-browser-languagedetector": "8.0.0",
|
||||||
"idb": "8.0.0",
|
"idb": "8.0.0",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { baseApiUrl } from '@/api';
|
import { baseApiUrl } from '@/api';
|
||||||
|
import { terminateCrispSession } from '@/services';
|
||||||
|
|
||||||
import { User, getMe } from './api';
|
import { User, getMe } from './api';
|
||||||
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
|
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
|
||||||
@@ -42,6 +43,7 @@ export const useAuthStore = create<AuthStore>((set, get) => ({
|
|||||||
window.location.replace(`${baseApiUrl()}authenticate/`);
|
window.location.replace(`${baseApiUrl()}authenticate/`);
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
|
terminateCrispSession();
|
||||||
window.location.replace(`${baseApiUrl()}logout/`);
|
window.location.replace(`${baseApiUrl()}logout/`);
|
||||||
},
|
},
|
||||||
// If we try to access a specific page and we are not authenticated
|
// If we try to access a specific page and we are not authenticated
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { PropsWithChildren, useEffect } from 'react';
|
|||||||
|
|
||||||
import { Box } from '@/components';
|
import { Box } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { configureCrispSession } from '@/services';
|
||||||
import { useSentryStore } from '@/stores/useSentryStore';
|
import { useSentryStore } from '@/stores/useSentryStore';
|
||||||
|
|
||||||
import { useConfig } from './api/useConfig';
|
import { useConfig } from './api/useConfig';
|
||||||
@@ -28,6 +29,14 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
|
|||||||
setTheme(conf.FRONTEND_THEME);
|
setTheme(conf.FRONTEND_THEME);
|
||||||
}, [conf?.FRONTEND_THEME, setTheme]);
|
}, [conf?.FRONTEND_THEME, setTheme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!conf?.CRISP_WEBSITE_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
configureCrispSession(conf.CRISP_WEBSITE_ID);
|
||||||
|
}, [conf?.CRISP_WEBSITE_ID]);
|
||||||
|
|
||||||
if (!conf) {
|
if (!conf) {
|
||||||
return (
|
return (
|
||||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { APIError, errorCauses, fetchAPI } from '@/api';
|
|||||||
import { Theme } from '@/cunningham/';
|
import { Theme } from '@/cunningham/';
|
||||||
|
|
||||||
interface ConfigResponse {
|
interface ConfigResponse {
|
||||||
SENTRY_DSN: string;
|
|
||||||
COLLABORATION_SERVER_URL: string;
|
|
||||||
ENVIRONMENT: string;
|
|
||||||
FRONTEND_THEME: Theme;
|
|
||||||
LANGUAGES: [string, string][];
|
LANGUAGES: [string, string][];
|
||||||
LANGUAGE_CODE: 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<ConfigResponse> => {
|
export const getConfig = async (): Promise<ConfigResponse> => {
|
||||||
|
|||||||
30
src/frontend/apps/impress/src/services/Crisp.tsx
Normal file
30
src/frontend/apps/impress/src/services/Crisp.tsx
Normal file
@@ -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();
|
||||||
|
};
|
||||||
1
src/frontend/apps/impress/src/services/index.ts
Normal file
1
src/frontend/apps/impress/src/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './Crisp';
|
||||||
@@ -5930,6 +5930,11 @@ crelt@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
|
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
|
||||||
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
|
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:
|
cross-env@*, cross-env@7.0.3:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
|
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
|
||||||
|
|||||||
Reference in New Issue
Block a user