@mistertemp/libs-front-shared 3.0.4 → 4.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/package.json +10 -1
  2. package/src/components/Notifications/CustomerNotificationCenterClient.ts +195 -0
  3. package/src/components/Notifications/EmptyNotificationsList.tsx +33 -0
  4. package/src/components/Notifications/Notification.tsx +105 -0
  5. package/src/components/Notifications/NotificationContent.tsx +153 -0
  6. package/src/components/Notifications/Notifications.module.scss +345 -0
  7. package/src/components/Notifications/NotificationsBell.tsx +242 -0
  8. package/src/components/Notifications/NotificationsCenterContext.tsx +578 -0
  9. package/src/components/Notifications/NotificationsList.tsx +293 -0
  10. package/src/components/Notifications/PopupNotifications.module.scss +73 -0
  11. package/src/components/Notifications/PopupNotifications.tsx +42 -0
  12. package/src/components/Notifications/dateISOToString.ts +32 -0
  13. package/src/components/Notifications/index.ts +7 -0
  14. package/src/components/Notifications/types.ts +39 -0
  15. package/src/components/Notifications/useLiveNotifications.ts +122 -0
  16. package/src/components/index.ts +1 -0
  17. package/src/locales/es/date-picker.json +11 -0
  18. package/src/locales/es/notification-error.json +12 -0
  19. package/src/locales/es/notification-utils.json +7 -0
  20. package/src/locales/es/notification.json +17 -0
  21. package/src/locales/fr/notification-error.json +12 -0
  22. package/src/locales/fr/notification-utils.json +7 -0
  23. package/src/locales/fr/notification.json +17 -0
  24. package/src/locales/it/notification-error.json +12 -0
  25. package/src/locales/it/notification-utils.json +7 -0
  26. package/src/locales/it/notification.json +17 -0
@@ -0,0 +1,578 @@
1
+ import { NotificationsContext } from '@mistertemp/design-system';
2
+ import { getToken, OAuthContext } from '@mistertemp/front-core';
3
+ import {
4
+ CategoryType,
5
+ NotificationTypes,
6
+ PublishedNotification,
7
+ } from '@mistertemp/types-agency-notification-center';
8
+ import React, {
9
+ createContext,
10
+ Dispatch,
11
+ SetStateAction,
12
+ useCallback,
13
+ useContext,
14
+ useEffect,
15
+ useMemo,
16
+ useState,
17
+ } from 'react';
18
+
19
+ import { useBundledTranslation } from '../../hooks';
20
+ import es from '../../locales/es/notification-error.json';
21
+ import fr from '../../locales/fr/notification-error.json';
22
+ import it from '../../locales/it/notification-error.json';
23
+ import { NotificationCenterClientInterface } from './types';
24
+
25
+ const translationNs = 'notification-error';
26
+ const translations = { it, fr, es };
27
+
28
+ const POPUP_NOTIFICATION_DURATION = 10000;
29
+
30
+ export interface NotificationsCenterContextProps {
31
+ // State
32
+ unreadNotifications: number;
33
+ setUnreadNotifications: Dispatch<SetStateAction<number>>;
34
+ removeUnreadNotification: () => void;
35
+ unseenNotifications: number;
36
+ setUnseenNotifications: Dispatch<SetStateAction<number>>;
37
+ notifications: PublishedNotification<NotificationTypes>[];
38
+ setNotifications: Dispatch<SetStateAction<PublishedNotification<NotificationTypes>[]>>;
39
+ addNotification: (notification: PublishedNotification<NotificationTypes>) => void;
40
+ popupNotifications: PopupNotificationType[];
41
+ setPopupNotifications: Dispatch<SetStateAction<PopupNotificationType[]>>;
42
+ addPopupNotification: (notification: PublishedNotification<NotificationTypes>) => void;
43
+ closePopupNotification: (notification: PublishedNotification<NotificationTypes>) => void;
44
+ isPopoverOpen: boolean;
45
+ setIsPopoverOpen: Dispatch<SetStateAction<boolean>>;
46
+ notificationsHasBeenLoaded: boolean;
47
+ setNotificationsHasBeenLoaded: Dispatch<SetStateAction<boolean>>;
48
+ agencyId: string | null | undefined;
49
+ startDate: Date | undefined;
50
+ setStartDate: (startDate: Date | undefined) => void;
51
+ endDate: Date | undefined;
52
+ setEndDate: (endDate: Date | undefined) => void;
53
+ isFullPageMode: boolean;
54
+ setIsFullPageMode: Dispatch<SetStateAction<boolean>>;
55
+ categories: CategoryType[];
56
+ setCategories: (categories: CategoryType[]) => void;
57
+ isDisplayReadNotification: boolean | undefined;
58
+ setIsDisplayReadNotification: (isDisplayReadNotification: boolean | undefined) => void;
59
+ // Services
60
+ markNotificationAsRead: (notificationId: string) => void;
61
+ markAllAsRead: () => void;
62
+ }
63
+
64
+ export type PopupNotificationType = PublishedNotification<NotificationTypes> & {
65
+ displayed: boolean;
66
+ hidden: boolean;
67
+ };
68
+
69
+ export interface NotificationsCenterProviderProps {
70
+ children: React.ReactNode;
71
+ isFullPageMode?: boolean;
72
+ /**
73
+ * The agency/account ID used to scope notifications.
74
+ * In match context this is agencyId, in customer context this is accountId.
75
+ */
76
+ agencyId: string | null | undefined;
77
+ /**
78
+ * The notification center client to use for API calls.
79
+ * Pass `new NotificationCenterClient()` for match,
80
+ * or `new CustomerNotificationCenterClient(env)` for customer.
81
+ */
82
+ client: NotificationCenterClientInterface;
83
+ /**
84
+ * Name of the BroadcastChannel used to sync notification state across tabs.
85
+ * @default 'shared_notifs'
86
+ */
87
+ broadcastChannelName?: string;
88
+ }
89
+
90
+ export const NotificationsCenterContext = createContext<NotificationsCenterContextProps>({
91
+ // State
92
+ unreadNotifications: 0,
93
+ setUnreadNotifications: () => void 0,
94
+ removeUnreadNotification: () => void 0,
95
+ unseenNotifications: 0,
96
+ setUnseenNotifications: () => void 0,
97
+ notifications: [],
98
+ addNotification: () => void 0,
99
+ setNotifications: () => void 0,
100
+ popupNotifications: [],
101
+ setPopupNotifications: () => void 0,
102
+ addPopupNotification: () => void 0,
103
+ closePopupNotification: () => void 0,
104
+ isPopoverOpen: false,
105
+ setIsPopoverOpen: () => void 0,
106
+ notificationsHasBeenLoaded: false,
107
+ setNotificationsHasBeenLoaded: () => void 0,
108
+ agencyId: undefined,
109
+ isFullPageMode: false,
110
+ setIsFullPageMode: () => void 0,
111
+ categories: [],
112
+ setCategories: () => void 0,
113
+ startDate: undefined,
114
+ setStartDate: () => void 0,
115
+ endDate: undefined,
116
+ setEndDate: () => void 0,
117
+ isDisplayReadNotification: undefined,
118
+ setIsDisplayReadNotification: () => void 0,
119
+ // Services
120
+ markNotificationAsRead: () => void 0,
121
+ markAllAsRead: () => void 0,
122
+ });
123
+
124
+ NotificationsCenterContext.displayName = 'NotificationsCenter';
125
+
126
+ export const NotificationsCenterProvider = ({
127
+ children,
128
+ isFullPageMode: initialIsFullPageMode = false,
129
+ agencyId,
130
+ client,
131
+ broadcastChannelName = 'shared_notifs',
132
+ }: NotificationsCenterProviderProps) => {
133
+ const idToken = useMemo(() => getToken('id'), []);
134
+
135
+ const { t, isReady } = useBundledTranslation(translationNs, translations);
136
+
137
+ const { enqueue } = useContext(NotificationsContext);
138
+
139
+ const { decodedIdToken } = useContext(OAuthContext);
140
+ const userCorrelationId = useMemo(
141
+ () => decodedIdToken && decodedIdToken['custom:userCorrelationId'],
142
+ [decodedIdToken],
143
+ );
144
+
145
+ const [closePopupNotifTimeout, setClosePopupNotifTimeout] = useState<NodeJS.Timeout | null>(
146
+ null,
147
+ );
148
+
149
+ const [unreadNotifications, setUnreadNotifications] = useState<number>(0);
150
+ const [unseenNotifications, setUnseenNotifications] = useState<number>(0);
151
+ const [notifications, setNotifications] = useState<PublishedNotification<NotificationTypes>[]>(
152
+ [],
153
+ );
154
+
155
+ const [popupNotifications, setPopupNotifications] = useState<PopupNotificationType[]>([]);
156
+
157
+ const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
158
+ const [isFullPageMode, setIsFullPageMode] = useState<boolean>(initialIsFullPageMode);
159
+ const [notificationsHasBeenLoaded, setNotificationsHasBeenLoaded] = useState<boolean>(false);
160
+ const [categories, setCategories] = useState<CategoryType[]>([]);
161
+ const [startDate, setStartDate] = useState<Date | undefined>(undefined);
162
+ const [endDate, setEndDate] = useState<Date | undefined>(undefined);
163
+ const [isDisplayReadNotification, setIsDisplayReadNotification] = useState<boolean | undefined>(
164
+ isFullPageMode ? undefined : false,
165
+ );
166
+
167
+ const removeUnreadNotification = useCallback(() => {
168
+ setUnreadNotifications(unreadNotifications - 1);
169
+ }, [unreadNotifications]);
170
+
171
+ const broadcastChannel = useMemo(
172
+ () => new BroadcastChannel(broadcastChannelName),
173
+ [broadcastChannelName],
174
+ );
175
+
176
+ const markAllAsReadStates = useCallback(async () => {
177
+ setUnreadNotifications(0);
178
+ setNotifications(
179
+ notifications.map((notification) => ({
180
+ ...notification,
181
+ readAt: notification.readAt ?? new Date().toISOString(),
182
+ })),
183
+ );
184
+ }, [notifications]);
185
+
186
+ const markNotificationAsReadStates = useCallback(
187
+ async (notificationId: string) => {
188
+ removeUnreadNotification();
189
+ setNotifications(
190
+ notifications.map((notification) => ({
191
+ ...notification,
192
+ readAt:
193
+ notification.notificationId === notificationId
194
+ ? new Date().toISOString()
195
+ : notification.readAt,
196
+ })),
197
+ );
198
+ },
199
+ [removeUnreadNotification, notifications],
200
+ );
201
+
202
+ const markAlertAsOpenStates = useCallback(async () => {
203
+ setNotifications(
204
+ notifications.map((notification) => ({
205
+ ...notification,
206
+ seenAt: notification.seenAt ?? new Date().toISOString(),
207
+ })),
208
+ );
209
+ setUnseenNotifications(0);
210
+ }, [notifications]);
211
+
212
+ const listener = useCallback(
213
+ async ({
214
+ data: { method, params },
215
+ }: {
216
+ data: { method: string; params: { notificationId: string } };
217
+ }) => {
218
+ switch (method) {
219
+ case 'markAllAsRead':
220
+ await markAllAsReadStates();
221
+ break;
222
+ case 'markNotificationAsRead':
223
+ if (params.notificationId) {
224
+ await markNotificationAsReadStates(params.notificationId);
225
+ }
226
+ break;
227
+ case 'markAlertAsOpen':
228
+ await markAlertAsOpenStates();
229
+ break;
230
+ default:
231
+ break;
232
+ }
233
+ },
234
+ [markAllAsReadStates, markNotificationAsReadStates, markAlertAsOpenStates],
235
+ );
236
+
237
+ useEffect(() => {
238
+ broadcastChannel.addEventListener('message', listener);
239
+
240
+ return () => broadcastChannel.removeEventListener('message', listener);
241
+ }, [broadcastChannel, listener]);
242
+
243
+ useEffect(() => {
244
+ if (isPopoverOpen && !isFullPageMode) {
245
+ setIsDisplayReadNotification(false);
246
+ }
247
+ }, [isPopoverOpen, isFullPageMode]);
248
+
249
+ const addNotification = useCallback(
250
+ (notification: PublishedNotification<NotificationTypes>) => {
251
+ setNotifications([
252
+ notification,
253
+ ...notifications.filter((n) => n.notificationId !== notification.notificationId),
254
+ ]);
255
+ },
256
+ [notifications],
257
+ );
258
+
259
+ const closePopupNotification = useCallback(
260
+ (notification: PublishedNotification<NotificationTypes>) => {
261
+ if (closePopupNotifTimeout) {
262
+ clearTimeout(closePopupNotifTimeout);
263
+ setClosePopupNotifTimeout(null);
264
+ }
265
+
266
+ setPopupNotifications((oldArray) =>
267
+ oldArray.map((n) =>
268
+ n.notificationId === notification.notificationId ? { ...n, hidden: true } : n,
269
+ ),
270
+ );
271
+
272
+ setTimeout(() => {
273
+ setPopupNotifications((oldArray) =>
274
+ oldArray.filter((n) => n.notificationId !== notification.notificationId),
275
+ );
276
+ }, 1000);
277
+ },
278
+ [closePopupNotifTimeout],
279
+ );
280
+
281
+ const addPopupNotification = useCallback(
282
+ (notification: PublishedNotification<NotificationTypes>) => {
283
+ setPopupNotifications((oldArray) => [
284
+ { ...notification, displayed: false, hidden: false },
285
+ ...oldArray.filter((n) => n.notificationId !== notification.notificationId),
286
+ ]);
287
+ setTimeout(() => {
288
+ setPopupNotifications((oldArray) =>
289
+ oldArray.map((n) => ({ ...n, displayed: true })),
290
+ );
291
+ }, 100);
292
+
293
+ setClosePopupNotifTimeout(
294
+ setTimeout(() => {
295
+ closePopupNotification(notification);
296
+ }, POPUP_NOTIFICATION_DURATION),
297
+ );
298
+ },
299
+ [closePopupNotification],
300
+ );
301
+
302
+ /**
303
+ * Call the API to mark a notification as read, and update notifications state
304
+ */
305
+ const markNotificationAsRead = useCallback(
306
+ async (notificationId: string) => {
307
+ if (!idToken || !userCorrelationId || !agencyId) {
308
+ enqueue({
309
+ text: `${t('notification-error:common.userNotAuthenticated')}`,
310
+ isPersistant: true,
311
+ type: 'negative',
312
+ });
313
+
314
+ return;
315
+ }
316
+
317
+ try {
318
+ await markNotificationAsReadStates(notificationId);
319
+ await client.markNotificationAsRead({
320
+ userCorrelationId,
321
+ agencyId,
322
+ notificationId,
323
+ token: idToken,
324
+ });
325
+ broadcastChannel.postMessage({
326
+ method: 'markNotificationAsRead',
327
+ params: { notificationId },
328
+ });
329
+ } catch (error) {
330
+ console.error(error);
331
+ enqueue({
332
+ text: `${t('notification-error:notifications.markNotificationAsRead')}`,
333
+ isPersistant: true,
334
+ type: 'negative',
335
+ });
336
+ }
337
+ },
338
+ [
339
+ idToken,
340
+ userCorrelationId,
341
+ agencyId,
342
+ enqueue,
343
+ t,
344
+ client,
345
+ broadcastChannel,
346
+ markNotificationAsReadStates,
347
+ ],
348
+ );
349
+
350
+ /**
351
+ * Call the API to mark all notifications as read, and update notifications state
352
+ */
353
+ const markAllAsRead = useCallback(async () => {
354
+ if (!idToken || !userCorrelationId || !agencyId) {
355
+ enqueue({
356
+ text: `${t('notification-error:common.userNotAuthenticated')}`,
357
+ isPersistant: true,
358
+ type: 'negative',
359
+ });
360
+
361
+ return;
362
+ }
363
+
364
+ try {
365
+ await markAllAsReadStates();
366
+ await client.markAllNotificationAsRead({
367
+ userCorrelationId,
368
+ agencyId,
369
+ token: idToken,
370
+ });
371
+ broadcastChannel.postMessage({ method: 'markAllAsRead' });
372
+ } catch (error) {
373
+ console.error(error);
374
+ enqueue({
375
+ text: `${t('notification-error:notifications.markAllNotificationAsRead')}`,
376
+ isPersistant: true,
377
+ type: 'negative',
378
+ });
379
+ }
380
+ }, [
381
+ idToken,
382
+ userCorrelationId,
383
+ agencyId,
384
+ enqueue,
385
+ t,
386
+ client,
387
+ markAllAsReadStates,
388
+ broadcastChannel,
389
+ ]);
390
+
391
+ /**
392
+ * On the first popover opening, call the API to get the notifications list
393
+ */
394
+ useEffect(() => {
395
+ if (!idToken || !userCorrelationId || !agencyId) {
396
+ enqueue({
397
+ text: `${t('notification-error:common.userNotAuthenticated')}`,
398
+ isPersistant: true,
399
+ type: 'negative',
400
+ });
401
+
402
+ return;
403
+ }
404
+
405
+ const queryString: Record<string, unknown> = { isRead: isDisplayReadNotification };
406
+
407
+ if (categories.length) {
408
+ queryString.categories = categories.join(', ');
409
+ }
410
+
411
+ if (startDate !== undefined && endDate !== undefined && startDate < endDate) {
412
+ queryString.startDate = startDate.toISOString();
413
+ queryString.endDate = endDate.toISOString();
414
+ }
415
+
416
+ setNotificationsHasBeenLoaded(false);
417
+ client
418
+ .getNotificationList({
419
+ userCorrelationId,
420
+ agencyId,
421
+ token: idToken,
422
+ queryString,
423
+ })
424
+ .then((fetchedNotifications) => {
425
+ setNotifications(
426
+ fetchedNotifications.sort(
427
+ (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
428
+ ),
429
+ );
430
+ })
431
+ .catch((error) => {
432
+ console.error(error);
433
+ enqueue({
434
+ text: `${t('notification-error:notifications.getNotificationList')}`,
435
+ isPersistant: true,
436
+ type: 'negative',
437
+ });
438
+ })
439
+ .finally(() => {
440
+ setNotificationsHasBeenLoaded(true);
441
+ });
442
+ }, [
443
+ client,
444
+ isPopoverOpen,
445
+ idToken,
446
+ userCorrelationId,
447
+ agencyId,
448
+ enqueue,
449
+ t,
450
+ isFullPageMode,
451
+ categories,
452
+ startDate,
453
+ endDate,
454
+ isDisplayReadNotification,
455
+ ]);
456
+
457
+ /**
458
+ * On the first render, call the API to get the number of unread and unseen notifications
459
+ */
460
+ useEffect(() => {
461
+ if (!idToken || !userCorrelationId || !agencyId || !isReady) {
462
+ return;
463
+ }
464
+
465
+ client
466
+ .getNotificationStatus({
467
+ userCorrelationId,
468
+ agencyId,
469
+ token: idToken,
470
+ })
471
+ .then((notificationsStatus) => {
472
+ setUnreadNotifications(notificationsStatus.countUnread);
473
+ setUnseenNotifications(notificationsStatus.countUnseen);
474
+ })
475
+ .catch(() => {
476
+ if (process.env.ENV !== 'sta') {
477
+ enqueue({
478
+ text: `${t('notification-error:notifications.getNotificationStatus')}`,
479
+ isPersistant: true,
480
+ type: 'negative',
481
+ });
482
+ }
483
+ });
484
+ }, [client, idToken, agencyId, userCorrelationId, enqueue, t, isReady]);
485
+
486
+ /**
487
+ * On popover opening, if there are notifications not seen, call the API to mark them as open
488
+ */
489
+ useEffect(() => {
490
+ const hasNotificationsNotSeen = notifications.some(
491
+ (notification) => notification.seenAt === undefined,
492
+ );
493
+
494
+ if (hasNotificationsNotSeen && (isPopoverOpen || isFullPageMode)) {
495
+ if (!idToken || !userCorrelationId || !agencyId) {
496
+ enqueue({
497
+ text: `${t('notification-error:common.userNotAuthenticated')}`,
498
+ isPersistant: true,
499
+ type: 'negative',
500
+ });
501
+
502
+ return;
503
+ }
504
+
505
+ client
506
+ .markAlertAsOpen({
507
+ userCorrelationId,
508
+ agencyId,
509
+ token: idToken,
510
+ })
511
+ .then(() => {
512
+ markAlertAsOpenStates();
513
+ broadcastChannel.postMessage({ method: 'markAlertAsOpen' });
514
+ })
515
+ .catch((error) => {
516
+ console.error(error);
517
+ enqueue({
518
+ text: `${t('notification-error:notifications.markAlertAsOpen')}`,
519
+ isPersistant: true,
520
+ type: 'negative',
521
+ });
522
+ });
523
+ }
524
+ }, [
525
+ notifications,
526
+ idToken,
527
+ agencyId,
528
+ userCorrelationId,
529
+ isPopoverOpen,
530
+ client,
531
+ enqueue,
532
+ t,
533
+ broadcastChannel,
534
+ markAlertAsOpenStates,
535
+ isFullPageMode,
536
+ ]);
537
+
538
+ return (
539
+ <NotificationsCenterContext.Provider
540
+ value={{
541
+ // State
542
+ unreadNotifications,
543
+ setUnreadNotifications,
544
+ removeUnreadNotification,
545
+ unseenNotifications,
546
+ setUnseenNotifications,
547
+ notifications,
548
+ setNotifications,
549
+ addNotification,
550
+ popupNotifications,
551
+ setPopupNotifications,
552
+ addPopupNotification,
553
+ closePopupNotification,
554
+ isPopoverOpen,
555
+ setIsPopoverOpen,
556
+ notificationsHasBeenLoaded,
557
+ setNotificationsHasBeenLoaded,
558
+ agencyId,
559
+ isFullPageMode,
560
+ setIsFullPageMode,
561
+ categories,
562
+ setCategories,
563
+ startDate,
564
+ setStartDate,
565
+ endDate,
566
+ setEndDate,
567
+ isDisplayReadNotification,
568
+ setIsDisplayReadNotification,
569
+ // Services
570
+ markNotificationAsRead,
571
+ markAllAsRead,
572
+ }}>
573
+ {children}
574
+ </NotificationsCenterContext.Provider>
575
+ );
576
+ };
577
+
578
+ export const useNotificationsCenterContext = () => useContext(NotificationsCenterContext);