authscape 1.0.762 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "authscape",
3
- "version": "1.0.762",
3
+ "version": "1.0.763",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -21,7 +21,8 @@
21
21
  "peerDependencies": {
22
22
  "js-file-download": "^0.4.12",
23
23
  "react-data-table-component": "^7.5.2",
24
- "react-dom": "^18.2.0"
24
+ "react-dom": "^18.2.0",
25
+ "react-toastify": "^9.1.3"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@babel/cli": "^7.27.0",
@@ -57,7 +58,6 @@
57
58
  "react-device-detect": "^2.2.3",
58
59
  "react-hook-form": "^7.50.1",
59
60
  "react-microsoft-clarity": "^1.2.0",
60
- "react-toastify": "^9.1.3",
61
61
  "styled-components": "^5.3.6",
62
62
  "use-places-autocomplete": "^4.0.0",
63
63
  "zustand": "^4.5.2"
@@ -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
+ }
12
70
 
13
- // ---- optional: import your cookie util if not global ----
14
- // import { setCookie } from "cookies-next";
15
- // import { apiService } from "@/services/api"; // wherever yours lives
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
+ }
16
98
 
17
- // Decorate a user object with role/permission helpers (idempotent)
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]);
133
+
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
+ };
141
+
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,12 +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
- // ----- PKCE Sign-in (browser-only) -----
85
492
  const signInValidator = async (codeFromQuery) => {
86
493
  if (queryCodeUsed.current === codeFromQuery) return;
87
494
  queryCodeUsed.current = codeFromQuery;
@@ -92,7 +499,6 @@ export function AuthScapeApp({
92
499
 
93
500
  const codeVerifier = window.localStorage.getItem("verifier");
94
501
  if (!codeFromQuery || !codeVerifier) {
95
- // No code or verifier - redirect to login
96
502
  window.localStorage.clear();
97
503
  module.exports.authService().login();
98
504
  return;
@@ -120,7 +526,6 @@ export function AuthScapeApp({
120
526
 
121
527
  window.localStorage.removeItem("verifier");
122
528
 
123
- // NOTE: replace setCookie below with your implementation if different
124
529
  await setCookie("access_token", response.data.access_token, {
125
530
  maxAge: 60 * 60 * 24 * 365,
126
531
  path: "/",
@@ -143,19 +548,15 @@ export function AuthScapeApp({
143
548
  const redirectUri = window.localStorage.getItem("redirectUri") || "/";
144
549
  window.localStorage.clear();
145
550
 
146
- // Navigate to the redirect URI - use window.location for a clean page load
147
- // This ensures all state is properly initialized on the target page
148
551
  window.location.href = redirectUri;
149
552
  } catch (exp) {
150
553
  console.error("PKCE sign-in failed", exp);
151
- // Invalid code - clear storage and redirect to login
152
554
  window.localStorage.clear();
153
555
  setIsSigningIn(false);
154
556
  module.exports.authService().login();
155
557
  }
156
558
  };
157
559
 
158
- // ----- GA + Clarity -----
159
560
  async function initGA(G) {
160
561
  if (typeof window !== "undefined" && !GA4React.isInitialized() && G) {
161
562
  ga4React.current = new GA4React(G, { debug_mode: !process.env.production });
@@ -169,7 +570,6 @@ export function AuthScapeApp({
169
570
 
170
571
  const logEvent = (category, action, label) => {
171
572
  if (ga4React.current) ga4React.current.event(action, label, category);
172
- // your DB analytics can go here if desired
173
573
  };
174
574
 
175
575
  const databaseDrivenPageView = (pathName) => {
@@ -179,7 +579,6 @@ export function AuthScapeApp({
179
579
 
180
580
  const host = window.location.protocol + "//" + window.location.host;
181
581
 
182
- // Use module.exports to access sibling exports in babel bundle
183
582
  module.exports.apiService().post("/Analytics/PageView", {
184
583
  userId: signedInUser.current?.id,
185
584
  locationId: signedInUser.current?.locationId,
@@ -189,7 +588,6 @@ export function AuthScapeApp({
189
588
  });
190
589
  };
191
590
 
192
- // ----- Auth init (runs once) -----
193
591
  useEffect(() => {
194
592
  if (queryCode) {
195
593
  signInValidator(queryCode);
@@ -200,13 +598,21 @@ export function AuthScapeApp({
200
598
  loadingAuth.current = true;
201
599
 
202
600
  if (enableAuth) {
203
- // Use module.exports to access sibling exports in babel bundle
204
601
  module.exports.apiService().GetCurrentUser().then((usr) => {
205
602
  signedInUser.current = ensureUserHelpers(usr);
206
603
  setSignedInUserState(signedInUser.current);
207
604
  setFrontEndLoadedState(true);
208
- }).catch(() => {
209
- // 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) => {
210
616
  signedInUser.current = null;
211
617
  setSignedInUserState(null);
212
618
  setFrontEndLoadedState(true);
@@ -215,9 +621,8 @@ export function AuthScapeApp({
215
621
  setFrontEndLoadedState(true);
216
622
  }
217
623
  }
218
- }, [queryCode, enableAuth]);
624
+ }, [queryCode, enableAuth, enableErrorTracking]);
219
625
 
220
- // ----- Analytics init -----
221
626
  useEffect(() => {
222
627
  if (!frontEndLoadedState || typeof window === "undefined") return;
223
628
 
@@ -243,7 +648,6 @@ export function AuthScapeApp({
243
648
  return () => Router.events.off("routeChangeComplete", handler);
244
649
  }, [frontEndLoadedState, pageProps.googleAnalytics4Code, pageProps.microsoftClarityCode]);
245
650
 
246
- // ----- Enforce login (client) -----
247
651
  useEffect(() => {
248
652
  if (
249
653
  enforceLoggedIn &&
@@ -251,19 +655,14 @@ export function AuthScapeApp({
251
655
  frontEndLoadedState &&
252
656
  !signedInUserState
253
657
  ) {
254
- // Use module.exports to access sibling exports in babel bundle
255
658
  module.exports.authService().login();
256
659
  }
257
660
  }, [signedInUserState, enforceLoggedIn, frontEndLoadedState, pathname]);
258
661
 
259
- // Stable getter for current user (with helpers)
260
662
  const currentUser = useMemo(() => ensureUserHelpers(signedInUser.current), [signedInUserState]);
261
663
 
262
664
  const useStore = create(() => store);
263
665
 
264
- // ----- Render (SSR-safe; always output page so <title> is visible) -----
265
-
266
- // Default sign-in loading component if none provided
267
666
  const defaultSignInLoading = (
268
667
  <div style={{
269
668
  display: 'flex',
@@ -292,7 +691,6 @@ export function AuthScapeApp({
292
691
  </div>
293
692
  );
294
693
 
295
- // Show loading screen when signing in
296
694
  if (isSigningIn) {
297
695
  return (
298
696
  <>
@@ -302,13 +700,27 @@ export function AuthScapeApp({
302
700
  content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86"
303
701
  />
304
702
  </Head>
305
- <ThemeProvider theme={muiTheme}>
703
+ <AppThemeProvider customTheme={muiTheme}>
306
704
  {signInLoadingComponent || defaultSignInLoading}
307
- </ThemeProvider>
705
+ </AppThemeProvider>
308
706
  </>
309
707
  );
310
708
  }
311
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
+
312
724
  const pageContent = layout
313
725
  ? layout({
314
726
  children: (
@@ -341,6 +753,15 @@ export function AuthScapeApp({
341
753
  />
342
754
  );
343
755
 
756
+ const wrappedContent = enableNotifications && currentUser ? (
757
+ <NotificationProvider
758
+ currentUser={currentUser}
759
+ apiService={module.exports.apiService}
760
+ >
761
+ {pageContent}
762
+ </NotificationProvider>
763
+ ) : pageContent;
764
+
344
765
  return (
345
766
  <>
346
767
  <Head>
@@ -350,11 +771,11 @@ export function AuthScapeApp({
350
771
  />
351
772
  </Head>
352
773
 
353
- <ThemeProvider theme={muiTheme}>
354
- {pageContent}
355
- <ToastContainer />
356
- </ThemeProvider>
774
+ <AppThemeProvider customTheme={muiTheme}>
775
+ {wrappedContent}
776
+ </AppThemeProvider>
357
777
 
778
+ <ToastContainer {...defaultToastConfig} />
358
779
  {loadingLayout && loadingLayout(isLoadingShow)}
359
780
  </>
360
781
  );