authscape 1.0.760 → 1.0.763

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.
@@ -1,7 +1,9 @@
1
- import React, { useState, useRef, useEffect, useMemo } from "react";
2
- import { ToastContainer, toast } from "react-toastify";
3
- import { ThemeProvider } from "@mui/material/styles";
1
+ import React, { useState, useRef, useEffect, useMemo, createContext, useContext, useCallback } from "react";
2
+ import { ToastContainer, toast, Bounce, Slide, Zoom, Flip } from "react-toastify";
4
3
  import Head from "next/head";
4
+
5
+ // Re-export toast and transitions so pages can import from authscape
6
+ export { toast, Bounce, Slide, Zoom, Flip };
5
7
  import { useSearchParams, usePathname } from "next/navigation";
6
8
  import axios from "axios";
7
9
  import querystring from "query-string";
@@ -9,16 +11,415 @@ import Router from "next/router";
9
11
  import GA4React from "ga-4-react";
10
12
  import { create } from "zustand";
11
13
  import { clarity } from "react-microsoft-clarity";
14
+ import { createTheme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
15
+ import CssBaseline from '@mui/material/CssBaseline';
16
+ import { HubConnectionBuilder, LogLevel, HttpTransportType } from '@microsoft/signalr';
17
+ import Cookies from 'js-cookie';
18
+
19
+ // ============================================================================
20
+ // Cookie utility function
21
+ // ============================================================================
22
+ const setCookie = (name, value, options = {}) => {
23
+ return new Promise((resolve) => {
24
+ let cookieString = `${name}=${value};`;
25
+ if (options.maxAge) cookieString += `max-age=${options.maxAge};`;
26
+ if (options.path) cookieString += `path=${options.path};`;
27
+ if (options.domain) cookieString += `domain=${options.domain};`;
28
+ if (options.secure) cookieString += `secure;`;
29
+ document.cookie = cookieString;
30
+ resolve();
31
+ });
32
+ };
33
+
34
+ // ============================================================================
35
+ // Error Tracking Service
36
+ // ============================================================================
37
+ let errorTrackingSessionId = null;
38
+ let errorTrackingUserId = null;
39
+ let errorTrackingInitialized = false;
40
+
41
+ function generateGuid() {
42
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
43
+ const r = Math.random() * 16 | 0;
44
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
45
+ return v.toString(16);
46
+ });
47
+ }
48
+
49
+ function getOrCreateSessionId() {
50
+ if (typeof window === 'undefined') return null;
51
+
52
+ let storedSessionId = sessionStorage.getItem('errorTrackingSessionId');
53
+
54
+ if (!storedSessionId) {
55
+ storedSessionId = sessionStorage.getItem('analyticsSessionId') || generateGuid();
56
+ sessionStorage.setItem('errorTrackingSessionId', storedSessionId);
57
+ }
58
+
59
+ return storedSessionId;
60
+ }
61
+
62
+ function initializeErrorTracking(currentUser = null) {
63
+ if (currentUser && currentUser.id) {
64
+ errorTrackingUserId = currentUser.id;
65
+ }
66
+
67
+ errorTrackingSessionId = getOrCreateSessionId();
68
+ errorTrackingInitialized = true;
69
+ }
70
+
71
+ export async function logError(errorData) {
72
+ if (!errorTrackingSessionId && typeof window !== 'undefined') {
73
+ errorTrackingSessionId = getOrCreateSessionId();
74
+ }
75
+
76
+ const error = {
77
+ message: errorData.message || 'Unknown error',
78
+ errorType: errorData.errorType || 'JavaScriptError',
79
+ stackTrace: errorData.stackTrace || '',
80
+ url: errorData.url || (typeof window !== 'undefined' ? window.location.href : ''),
81
+ componentName: errorData.componentName || null,
82
+ userId: errorTrackingUserId || null,
83
+ sessionId: errorTrackingSessionId || null,
84
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : null,
85
+ ipAddress: '',
86
+ metadata: errorData.metadata || null
87
+ };
88
+
89
+ try {
90
+ const response = await module.exports.apiService().post('/ErrorTracking/LogError', error);
91
+ if (response && response.status !== 200) {
92
+ console.error('Error tracking API returned:', response.status);
93
+ }
94
+ } catch (err) {
95
+ console.error('Failed to send error to tracking system:', err.message);
96
+ }
97
+ }
98
+
99
+ export function setErrorTrackingUserId(newUserId) {
100
+ errorTrackingUserId = newUserId;
101
+ }
102
+
103
+ // ============================================================================
104
+ // AppThemeProvider
105
+ // ============================================================================
106
+ const ThemeContext = createContext();
107
+
108
+ export const useAppTheme = () => {
109
+ const context = useContext(ThemeContext);
110
+ if (!context) {
111
+ return { mode: 'light', toggleTheme: () => {} };
112
+ }
113
+ return context;
114
+ };
115
+
116
+ const AppThemeProvider = ({ children, customTheme }) => {
117
+ const [mode, setMode] = useState('light');
118
+
119
+ useEffect(() => {
120
+ if (typeof window !== 'undefined') {
121
+ const savedMode = localStorage.getItem('themeMode');
122
+ if (savedMode) {
123
+ setMode(savedMode);
124
+ }
125
+ }
126
+ }, []);
127
+
128
+ useEffect(() => {
129
+ if (typeof document !== 'undefined') {
130
+ document.documentElement.setAttribute('data-theme', mode);
131
+ }
132
+ }, [mode]);
12
133
 
13
- // ---- optional: import your cookie util if not global ----
14
- // import { setCookie } from "cookies-next";
15
- // import { apiService } from "@/services/api"; // wherever yours lives
134
+ const toggleTheme = () => {
135
+ const newMode = mode === 'light' ? 'dark' : 'light';
136
+ setMode(newMode);
137
+ if (typeof window !== 'undefined') {
138
+ localStorage.setItem('themeMode', newMode);
139
+ }
140
+ };
16
141
 
17
- // Decorate a user object with role/permission helpers (idempotent)
142
+ const theme = customTheme || createTheme({
143
+ palette: {
144
+ mode,
145
+ ...(mode === 'light'
146
+ ? {
147
+ primary: { main: '#0098e5', light: '#4db8ff', dark: '#006ba6' },
148
+ secondary: { main: '#44596e', light: '#6b7f94', dark: '#2d3d4f' },
149
+ background: { default: '#f5f8fa', paper: '#ffffff' },
150
+ text: { primary: '#1a202c', secondary: '#4a5568' },
151
+ divider: 'rgba(0, 0, 0, 0.12)',
152
+ }
153
+ : {
154
+ primary: { main: '#2196f3', light: '#42a5f5', dark: '#1976d2' },
155
+ secondary: { main: '#90caf9', light: '#bbdefb', dark: '#42a5f5' },
156
+ background: { default: '#121212', paper: '#1e1e1e' },
157
+ text: { primary: '#ffffff', secondary: '#b0b0b0' },
158
+ divider: 'rgba(255, 255, 255, 0.12)',
159
+ action: { hover: 'rgba(255, 255, 255, 0.08)', selected: 'rgba(255, 255, 255, 0.16)' },
160
+ }),
161
+ },
162
+ typography: { fontFamily: 'Poppins, sans-serif' },
163
+ components: {
164
+ MuiPaper: {
165
+ styleOverrides: {
166
+ root: {
167
+ backgroundImage: 'none',
168
+ ...(mode === 'dark' && { backgroundColor: '#1e1e1e', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.4)' }),
169
+ },
170
+ },
171
+ },
172
+ MuiTextField: {
173
+ styleOverrides: {
174
+ root: {
175
+ '& .MuiOutlinedInput-root': {
176
+ ...(mode === 'dark' && {
177
+ '& fieldset': { borderColor: 'rgba(255, 255, 255, 0.23)' },
178
+ '&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.4)' },
179
+ '&.Mui-focused fieldset': { borderColor: '#2196f3' },
180
+ }),
181
+ },
182
+ },
183
+ },
184
+ },
185
+ MuiChip: {
186
+ styleOverrides: {
187
+ root: { ...(mode === 'dark' && { borderColor: 'rgba(255, 255, 255, 0.23)' }) },
188
+ },
189
+ },
190
+ MuiDivider: {
191
+ styleOverrides: {
192
+ root: { ...(mode === 'dark' && { borderColor: 'rgba(255, 255, 255, 0.12)' }) },
193
+ },
194
+ },
195
+ },
196
+ });
197
+
198
+ return React.createElement(
199
+ ThemeContext.Provider,
200
+ { value: { mode, toggleTheme } },
201
+ React.createElement(
202
+ MuiThemeProvider,
203
+ { theme },
204
+ React.createElement(CssBaseline),
205
+ children
206
+ )
207
+ );
208
+ };
209
+
210
+ // ============================================================================
211
+ // NotificationProvider
212
+ // ============================================================================
213
+ const NotificationContext = createContext();
214
+
215
+ let globalConnection = null;
216
+ let globalUserId = null;
217
+ let globalIsInitialized = false;
218
+
219
+ export function useNotifications() {
220
+ const context = useContext(NotificationContext);
221
+ if (!context) {
222
+ return {
223
+ notifications: [],
224
+ unreadCount: 0,
225
+ isConnected: false,
226
+ markAsRead: () => {},
227
+ markAllAsRead: () => {},
228
+ deleteNotification: () => {},
229
+ clearAll: () => {},
230
+ refresh: () => {}
231
+ };
232
+ }
233
+ return context;
234
+ }
235
+
236
+ function NotificationProvider({ children, currentUser, apiService }) {
237
+ const [notifications, setNotifications] = useState([]);
238
+ const [unreadCount, setUnreadCount] = useState(0);
239
+ const [isConnected, setIsConnected] = useState(false);
240
+
241
+ const markAsRead = useCallback(async (notificationId) => {
242
+ try {
243
+ await apiService().post('/Notification/MarkAsRead', { notificationId });
244
+ setNotifications(prev =>
245
+ prev.map(n => n.id === notificationId ? { ...n, isRead: true, readAt: new Date() } : n)
246
+ );
247
+ setUnreadCount(prev => Math.max(0, prev - 1));
248
+ } catch (err) {
249
+ console.error('Failed to mark as read:', err);
250
+ }
251
+ }, [apiService]);
252
+
253
+ const markAllAsRead = useCallback(async () => {
254
+ try {
255
+ await apiService().post('/Notification/MarkAllAsRead');
256
+ setNotifications(prev => prev.map(n => ({ ...n, isRead: true, readAt: new Date() })));
257
+ setUnreadCount(0);
258
+ } catch (err) {
259
+ console.error('Failed to mark all as read:', err);
260
+ }
261
+ }, [apiService]);
262
+
263
+ const deleteNotification = useCallback(async (notificationId) => {
264
+ try {
265
+ await apiService().delete(`/Notification/DeleteNotification?id=${notificationId}`);
266
+ setNotifications(prev => {
267
+ const notification = prev.find(n => n.id === notificationId);
268
+ if (notification && !notification.isRead) {
269
+ setUnreadCount(count => Math.max(0, count - 1));
270
+ }
271
+ return prev.filter(n => n.id !== notificationId);
272
+ });
273
+ } catch (err) {
274
+ console.error('Failed to delete notification:', err);
275
+ }
276
+ }, [apiService]);
277
+
278
+ const clearAll = useCallback(async () => {
279
+ try {
280
+ await apiService().delete('/Notification/ClearAllNotifications');
281
+ setNotifications([]);
282
+ setUnreadCount(0);
283
+ } catch (err) {
284
+ console.error('Failed to clear notifications:', err);
285
+ }
286
+ }, [apiService]);
287
+
288
+ const fetchNotifications = useCallback(async () => {
289
+ try {
290
+ const [notifResponse, countResponse] = await Promise.all([
291
+ apiService().get('/Notification/GetNotifications?unreadOnly=false&take=50'),
292
+ apiService().get('/Notification/GetUnreadCount')
293
+ ]);
294
+ if (notifResponse.status === 200) {
295
+ setNotifications(notifResponse.data);
296
+ }
297
+ if (countResponse.status === 200) {
298
+ setUnreadCount(countResponse.data.count);
299
+ }
300
+ } catch (err) {
301
+ console.error('Failed to fetch notifications:', err);
302
+ }
303
+ }, [apiService]);
304
+
305
+ useEffect(() => {
306
+ const userId = currentUser?.id;
307
+ if (!userId) return;
308
+
309
+ const fetchData = async () => {
310
+ try {
311
+ const [notifResponse, countResponse] = await Promise.all([
312
+ apiService().get('/Notification/GetNotifications?unreadOnly=false&take=50'),
313
+ apiService().get('/Notification/GetUnreadCount')
314
+ ]);
315
+ if (notifResponse.status === 200) {
316
+ setNotifications(notifResponse.data);
317
+ }
318
+ if (countResponse.status === 200) {
319
+ setUnreadCount(countResponse.data.count);
320
+ }
321
+ } catch (err) {
322
+ console.error('Failed to fetch notifications:', err);
323
+ }
324
+ };
325
+
326
+ if (globalIsInitialized && globalUserId === userId && globalConnection) {
327
+ setIsConnected(globalConnection.state === 'Connected');
328
+ fetchData();
329
+ return;
330
+ }
331
+
332
+ if (globalConnection && globalUserId !== userId) {
333
+ globalConnection.stop();
334
+ globalConnection = null;
335
+ globalIsInitialized = false;
336
+ }
337
+
338
+ globalUserId = userId;
339
+ globalIsInitialized = true;
340
+
341
+ const apiBaseUrl = process.env.apiUri || 'http://localhost:54218';
342
+ const hubUrl = `${apiBaseUrl}/notifications`;
343
+
344
+ // Get access token for SignalR authentication
345
+ const accessToken = Cookies.get('access_token') || '';
346
+
347
+ const connection = new HubConnectionBuilder()
348
+ .withUrl(hubUrl, {
349
+ accessTokenFactory: () => accessToken,
350
+ transport: HttpTransportType.WebSockets | HttpTransportType.LongPolling
351
+ })
352
+ .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
353
+ .configureLogging(LogLevel.Warning)
354
+ .build();
355
+
356
+ globalConnection = connection;
357
+
358
+ connection.on('OnNotificationReceived', (notification) => {
359
+ setNotifications(prev => [notification, ...prev]);
360
+ setUnreadCount(prev => prev + 1);
361
+
362
+ const description = notification.message || notification.categoryName || '';
363
+ toast.info(
364
+ React.createElement('div', null,
365
+ React.createElement('strong', null, notification.title),
366
+ description && React.createElement('div', { style: { fontSize: '0.9em', marginTop: '4px' } }, description)
367
+ ),
368
+ {
369
+ onClick: () => {
370
+ if (notification.linkUrl) {
371
+ window.location.href = notification.linkUrl;
372
+ }
373
+ }
374
+ }
375
+ );
376
+ });
377
+
378
+ connection.onreconnecting(() => setIsConnected(false));
379
+ connection.onreconnected(() => { setIsConnected(true); fetchData(); });
380
+ connection.onclose(() => setIsConnected(false));
381
+
382
+ const startConnection = async () => {
383
+ try {
384
+ await connection.start();
385
+ setIsConnected(true);
386
+ await connection.invoke('JoinUserNotifications', userId);
387
+ if (currentUser?.companyId) {
388
+ await connection.invoke('JoinCompanyNotifications', currentUser.companyId);
389
+ }
390
+ if (currentUser?.locationId) {
391
+ await connection.invoke('JoinLocationNotifications', currentUser.locationId);
392
+ }
393
+ await fetchData();
394
+ } catch (err) {
395
+ console.error('Failed to connect to NotificationHub:', err.message);
396
+ await fetchData();
397
+ }
398
+ };
399
+
400
+ startConnection();
401
+ }, [currentUser?.id, currentUser?.companyId, currentUser?.locationId, apiService]);
402
+
403
+ const value = {
404
+ notifications,
405
+ unreadCount,
406
+ isConnected,
407
+ markAsRead,
408
+ markAllAsRead,
409
+ deleteNotification,
410
+ clearAll,
411
+ refresh: fetchNotifications
412
+ };
413
+
414
+ return React.createElement(NotificationContext.Provider, { value }, children);
415
+ }
416
+
417
+ // ============================================================================
418
+ // User Helpers
419
+ // ============================================================================
18
420
  function ensureUserHelpers(u) {
19
421
  if (!u || typeof u !== "object") return u;
20
422
 
21
- // Avoid redefining on every call
22
423
  if (typeof u.hasRole === "function" &&
23
424
  typeof u.hasRoleId === "function" &&
24
425
  typeof u.hasPermission === "function") {
@@ -28,7 +429,6 @@ function ensureUserHelpers(u) {
28
429
  const rolesArr = Array.isArray(u.roles) ? u.roles : [];
29
430
  const permsArr = Array.isArray(u.permissions) ? u.permissions : [];
30
431
 
31
- // defineProperty keeps them non-enumerable
32
432
  Object.defineProperty(u, "hasRole", {
33
433
  value: function hasRole(name) {
34
434
  if (!name) return false;
@@ -56,16 +456,23 @@ function ensureUserHelpers(u) {
56
456
  return u;
57
457
  }
58
458
 
459
+ // ============================================================================
460
+ // AuthScapeApp Component
461
+ // ============================================================================
59
462
  export function AuthScapeApp({
60
463
  Component,
61
464
  layout,
62
465
  loadingLayout,
63
466
  signInLoadingComponent,
64
467
  pageProps,
65
- muiTheme = {},
468
+ muiTheme = null,
66
469
  store = {},
67
470
  enforceLoggedIn = false,
68
471
  enableAuth = true,
472
+ enableNotifications = true,
473
+ enableErrorTracking = true,
474
+ toastConfig = {},
475
+ onUserLoaded = null,
69
476
  }) {
70
477
  const [frontEndLoadedState, setFrontEndLoadedState] = useState(false);
71
478
  const [isLoadingShow, setIsLoadingShow] = useState(false);
@@ -76,15 +483,12 @@ export function AuthScapeApp({
76
483
  const signedInUser = useRef(null);
77
484
  const queryCodeUsed = useRef(null);
78
485
  const ga4React = useRef(null);
486
+ const errorTrackingInitializedRef = useRef(false);
79
487
 
80
488
  const searchParams = useSearchParams();
81
489
  const queryCode = searchParams.get("code");
82
490
  const pathname = usePathname();
83
491
 
84
- // Check if we're on the signin-oidc page
85
- const isOnSignInPage = pathname === "/signin-oidc";
86
-
87
- // ----- PKCE Sign-in (browser-only) -----
88
492
  const signInValidator = async (codeFromQuery) => {
89
493
  if (queryCodeUsed.current === codeFromQuery) return;
90
494
  queryCodeUsed.current = codeFromQuery;
@@ -95,7 +499,6 @@ export function AuthScapeApp({
95
499
 
96
500
  const codeVerifier = window.localStorage.getItem("verifier");
97
501
  if (!codeFromQuery || !codeVerifier) {
98
- // No code or verifier - redirect to login
99
502
  window.localStorage.clear();
100
503
  module.exports.authService().login();
101
504
  return;
@@ -123,7 +526,6 @@ export function AuthScapeApp({
123
526
 
124
527
  window.localStorage.removeItem("verifier");
125
528
 
126
- // NOTE: replace setCookie below with your implementation if different
127
529
  await setCookie("access_token", response.data.access_token, {
128
530
  maxAge: 60 * 60 * 24 * 365,
129
531
  path: "/",
@@ -146,31 +548,15 @@ export function AuthScapeApp({
146
548
  const redirectUri = window.localStorage.getItem("redirectUri") || "/";
147
549
  window.localStorage.clear();
148
550
 
149
- // Use history.replaceState to change URL without a page reload, then fetch user
150
- window.history.replaceState({}, "", redirectUri);
151
-
152
- // Now load the current user and update state
153
- try {
154
- const usr = await module.exports.apiService().GetCurrentUser();
155
- signedInUser.current = ensureUserHelpers(usr);
156
- setSignedInUserState(signedInUser.current);
157
- setFrontEndLoadedState(true);
158
-
159
- // Trigger a soft navigation using Next.js router
160
- Router.replace(redirectUri, undefined, { shallow: false });
161
- } catch (userErr) {
162
- // If we can't get user, still navigate
163
- Router.replace(redirectUri);
164
- }
551
+ window.location.href = redirectUri;
165
552
  } catch (exp) {
166
553
  console.error("PKCE sign-in failed", exp);
167
- // Invalid code - clear storage and redirect to login
168
554
  window.localStorage.clear();
555
+ setIsSigningIn(false);
169
556
  module.exports.authService().login();
170
557
  }
171
558
  };
172
559
 
173
- // ----- GA + Clarity -----
174
560
  async function initGA(G) {
175
561
  if (typeof window !== "undefined" && !GA4React.isInitialized() && G) {
176
562
  ga4React.current = new GA4React(G, { debug_mode: !process.env.production });
@@ -184,7 +570,6 @@ export function AuthScapeApp({
184
570
 
185
571
  const logEvent = (category, action, label) => {
186
572
  if (ga4React.current) ga4React.current.event(action, label, category);
187
- // your DB analytics can go here if desired
188
573
  };
189
574
 
190
575
  const databaseDrivenPageView = (pathName) => {
@@ -194,7 +579,6 @@ export function AuthScapeApp({
194
579
 
195
580
  const host = window.location.protocol + "//" + window.location.host;
196
581
 
197
- // Use module.exports to access sibling exports in babel bundle
198
582
  module.exports.apiService().post("/Analytics/PageView", {
199
583
  userId: signedInUser.current?.id,
200
584
  locationId: signedInUser.current?.locationId,
@@ -204,7 +588,6 @@ export function AuthScapeApp({
204
588
  });
205
589
  };
206
590
 
207
- // ----- Auth init (runs once) -----
208
591
  useEffect(() => {
209
592
  if (queryCode) {
210
593
  signInValidator(queryCode);
@@ -215,13 +598,21 @@ export function AuthScapeApp({
215
598
  loadingAuth.current = true;
216
599
 
217
600
  if (enableAuth) {
218
- // Use module.exports to access sibling exports in babel bundle
219
601
  module.exports.apiService().GetCurrentUser().then((usr) => {
220
602
  signedInUser.current = ensureUserHelpers(usr);
221
603
  setSignedInUserState(signedInUser.current);
222
604
  setFrontEndLoadedState(true);
223
- }).catch(() => {
224
- // no user / anonymous
605
+
606
+ // Initialize error tracking with user info
607
+ if (enableErrorTracking && usr && !errorTrackingInitializedRef.current) {
608
+ initializeErrorTracking(usr);
609
+ errorTrackingInitializedRef.current = true;
610
+ }
611
+
612
+ if (onUserLoaded && usr) {
613
+ onUserLoaded(usr);
614
+ }
615
+ }).catch((err) => {
225
616
  signedInUser.current = null;
226
617
  setSignedInUserState(null);
227
618
  setFrontEndLoadedState(true);
@@ -230,9 +621,8 @@ export function AuthScapeApp({
230
621
  setFrontEndLoadedState(true);
231
622
  }
232
623
  }
233
- }, [queryCode, enableAuth]);
624
+ }, [queryCode, enableAuth, enableErrorTracking]);
234
625
 
235
- // ----- Analytics init -----
236
626
  useEffect(() => {
237
627
  if (!frontEndLoadedState || typeof window === "undefined") return;
238
628
 
@@ -258,7 +648,6 @@ export function AuthScapeApp({
258
648
  return () => Router.events.off("routeChangeComplete", handler);
259
649
  }, [frontEndLoadedState, pageProps.googleAnalytics4Code, pageProps.microsoftClarityCode]);
260
650
 
261
- // ----- Enforce login (client) -----
262
651
  useEffect(() => {
263
652
  if (
264
653
  enforceLoggedIn &&
@@ -266,19 +655,14 @@ export function AuthScapeApp({
266
655
  frontEndLoadedState &&
267
656
  !signedInUserState
268
657
  ) {
269
- // Use module.exports to access sibling exports in babel bundle
270
658
  module.exports.authService().login();
271
659
  }
272
660
  }, [signedInUserState, enforceLoggedIn, frontEndLoadedState, pathname]);
273
661
 
274
- // Stable getter for current user (with helpers)
275
662
  const currentUser = useMemo(() => ensureUserHelpers(signedInUser.current), [signedInUserState]);
276
663
 
277
664
  const useStore = create(() => store);
278
665
 
279
- // ----- Render (SSR-safe; always output page so <title> is visible) -----
280
-
281
- // Default sign-in loading component if none provided
282
666
  const defaultSignInLoading = (
283
667
  <div style={{
284
668
  display: 'flex',
@@ -307,8 +691,7 @@ export function AuthScapeApp({
307
691
  </div>
308
692
  );
309
693
 
310
- // Show loading screen when on signin-oidc page
311
- if (isOnSignInPage || isSigningIn) {
694
+ if (isSigningIn) {
312
695
  return (
313
696
  <>
314
697
  <Head>
@@ -317,13 +700,27 @@ export function AuthScapeApp({
317
700
  content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86"
318
701
  />
319
702
  </Head>
320
- <ThemeProvider theme={muiTheme}>
703
+ <AppThemeProvider customTheme={muiTheme}>
321
704
  {signInLoadingComponent || defaultSignInLoading}
322
- </ThemeProvider>
705
+ </AppThemeProvider>
323
706
  </>
324
707
  );
325
708
  }
326
709
 
710
+ const defaultToastConfig = {
711
+ position: "top-right",
712
+ autoClose: 3000,
713
+ hideProgressBar: false,
714
+ newestOnTop: true,
715
+ closeOnClick: true,
716
+ rtl: false,
717
+ pauseOnFocusLoss: true,
718
+ draggable: true,
719
+ pauseOnHover: true,
720
+ theme: "colored",
721
+ ...toastConfig
722
+ };
723
+
327
724
  const pageContent = layout
328
725
  ? layout({
329
726
  children: (
@@ -356,6 +753,15 @@ export function AuthScapeApp({
356
753
  />
357
754
  );
358
755
 
756
+ const wrappedContent = enableNotifications && currentUser ? (
757
+ <NotificationProvider
758
+ currentUser={currentUser}
759
+ apiService={module.exports.apiService}
760
+ >
761
+ {pageContent}
762
+ </NotificationProvider>
763
+ ) : pageContent;
764
+
359
765
  return (
360
766
  <>
361
767
  <Head>
@@ -365,11 +771,11 @@ export function AuthScapeApp({
365
771
  />
366
772
  </Head>
367
773
 
368
- <ThemeProvider theme={muiTheme}>
369
- {pageContent}
370
- <ToastContainer />
371
- </ThemeProvider>
774
+ <AppThemeProvider customTheme={muiTheme}>
775
+ {wrappedContent}
776
+ </AppThemeProvider>
372
777
 
778
+ <ToastContainer {...defaultToastConfig} />
373
779
  {loadingLayout && loadingLayout(isLoadingShow)}
374
780
  </>
375
781
  );