@bnsights/bbsf-utilities 1.2.5 → 1.2.8-beta.1

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.
@@ -12,11 +12,11 @@ import * as i4$1 from 'ngx-cookie-service';
12
12
  import { CookieService } from 'ngx-cookie-service';
13
13
  import * as i1 from '@angular/common/http';
14
14
  import { HttpParams, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
15
- import { Subject, throwError, of, Observable, lastValueFrom, BehaviorSubject } from 'rxjs';
15
+ import { Subject, switchMap, tap, take, takeUntil, throwError, of, Observable, lastValueFrom, BehaviorSubject } from 'rxjs';
16
16
  import { JwtHelperService } from '@auth0/angular-jwt';
17
17
  import { __decorate } from 'tslib';
18
18
  import { plainToClass } from 'class-transformer';
19
- import { takeUntil, tap, map, shareReplay, take, filter, switchMap } from 'rxjs/operators';
19
+ import { takeUntil as takeUntil$1, tap as tap$1, map, shareReplay, take as take$1, filter, switchMap as switchMap$1 } from 'rxjs/operators';
20
20
  import * as i1$2 from '@angular/service-worker';
21
21
 
22
22
  class User {
@@ -67,10 +67,6 @@ class ConfigurationService {
67
67
  constructor(httpClient, injector) {
68
68
  this.httpClient = httpClient;
69
69
  this.injector = injector;
70
- this.httpClient.get("./assets/config/configurations.json")
71
- .subscribe(data => {
72
- ConfigurationService.JsonData = data;
73
- });
74
70
  }
75
71
  get authService() {
76
72
  if (!this._authService) {
@@ -386,39 +382,63 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.16", ngImpo
386
382
  }] });
387
383
 
388
384
  class MasterLayoutService {
389
- constructor(router, http, authService, stylesBundleService, translate, environmentService) {
385
+ constructor(router, http, authService, stylesBundleService, translate, fileLoaderService) {
390
386
  this.router = router;
391
387
  this.http = http;
392
388
  this.authService = authService;
393
389
  this.stylesBundleService = stylesBundleService;
394
390
  this.translate = translate;
395
- this.environmentService = environmentService;
391
+ this.fileLoaderService = fileLoaderService;
396
392
  this.apiUrl = '/api/Home/';
393
+ this.destroy$ = new Subject();
397
394
  }
398
395
  switchLang(lang, bundleEnglishName, bundleArabicName) {
399
- if (this.authService.isAuthenticated()) {
400
- this.changeLanguage(lang).subscribe((result) => {
401
- this.updateUserInfo().subscribe((Value) => {
402
- let UserInfoObject = Value;
403
- this.authService.handleAccessToken(UserInfoObject.token);
404
- this.stylesBundleService.loadThemes(lang, bundleEnglishName, bundleArabicName);
405
- localStorage.setItem('language', lang);
406
- this.translate.use(lang);
407
- });
408
- });
396
+ if (!lang?.trim()) {
397
+ return;
409
398
  }
410
- else
399
+ if (!this.authService.isAuthenticated()) {
411
400
  this.router.navigate(['/Admin/account/login']);
401
+ return;
402
+ }
403
+ this.changeLanguage(lang).pipe(switchMap(() => this.updateUserInfo()), tap((userInfo) => {
404
+ if (!userInfo?.token) {
405
+ return;
406
+ }
407
+ this.authService.handleAccessToken(userInfo.token).then(async () => {
408
+ this.stylesBundleService.loadThemes(lang, bundleEnglishName, bundleArabicName);
409
+ localStorage.setItem('language', lang);
410
+ try {
411
+ await this.fileLoaderService.loadLanguage(lang);
412
+ }
413
+ catch (e) {
414
+ this.logError(e?.message || `loadLanguage failed for ${lang}`).pipe(take(1)).subscribe();
415
+ }
416
+ this.translate.use(lang);
417
+ });
418
+ }), takeUntil(this.destroy$)).subscribe({
419
+ error: (error) => {
420
+ this.logError(error?.message || 'switchLang failed').pipe(take(1)).subscribe();
421
+ }
422
+ });
412
423
  }
413
424
  reloadComponent() {
414
- let currentUrl = this.router.url;
425
+ const currentUrl = this.router.url;
426
+ const previousShouldReuseRoute = this.router.routeReuseStrategy.shouldReuseRoute;
427
+ const previousOnSameUrlNavigation = this.router.onSameUrlNavigation;
415
428
  this.router.routeReuseStrategy.shouldReuseRoute = () => false;
416
429
  this.router.onSameUrlNavigation = 'reload';
417
- this.router.navigate([currentUrl]);
430
+ this.router.navigate([currentUrl]).finally(() => {
431
+ this.router.routeReuseStrategy.shouldReuseRoute = previousShouldReuseRoute;
432
+ this.router.onSameUrlNavigation = previousOnSameUrlNavigation;
433
+ });
418
434
  }
419
435
  changeLanguage(key) {
436
+ const profile = this.authService.getCurrentUserProfile();
437
+ if (!profile?.id) {
438
+ return this.logError('changeLanguage failed: missing user profile id');
439
+ }
420
440
  let params = new HttpParams();
421
- params = params.append('UserId', AuthService.user.profile.id);
441
+ params = params.append('UserId', profile.id);
422
442
  params = params.append('LanguageKey', key);
423
443
  return this.http.post(this.apiUrl + 'UpdateLanguage', null, null, params);
424
444
  }
@@ -431,20 +451,34 @@ class MasterLayoutService {
431
451
  return this.http.get(this.apiUrl + 'UpdateUserInfo', null, null);
432
452
  }
433
453
  switchRole(permissionSetID) {
434
- this.updateRole(permissionSetID).subscribe((result) => {
435
- this.updateUserInfo().subscribe((Value) => {
436
- let UserInfoObject = Value;
437
- this.authService.handleAccessToken(UserInfoObject.token);
438
- });
454
+ if (!permissionSetID?.trim()) {
455
+ return;
456
+ }
457
+ this.updateRole(permissionSetID).pipe(switchMap(() => this.updateUserInfo()), tap((userInfo) => {
458
+ if (userInfo?.token) {
459
+ this.authService.handleAccessToken(userInfo.token);
460
+ }
461
+ }), takeUntil(this.destroy$)).subscribe({
462
+ error: (error) => {
463
+ this.logError(error?.message || 'switchRole failed').pipe(take(1)).subscribe();
464
+ }
439
465
  });
440
466
  }
441
467
  updateRole(permissionSetID) {
468
+ const profile = this.authService.getCurrentUserProfile();
469
+ if (!profile?.id) {
470
+ return this.logError('updateRole failed: missing user profile id');
471
+ }
442
472
  let params = new HttpParams();
443
- params = params.append('UserId', AuthService.user.profile.id);
473
+ params = params.append('UserId', profile.id);
444
474
  params = params.append('RoleID', permissionSetID);
445
475
  return this.http.post(this.apiUrl + 'SwitchRole', null, null, params);
446
476
  }
447
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.16", ngImport: i0, type: MasterLayoutService, deps: [{ token: i1$1.Router }, { token: RequestHandlerService }, { token: AuthService }, { token: StylesBundleService }, { token: i4.TranslateService }, { token: EnvironmentService }], target: i0.ɵɵFactoryTarget.Injectable }); }
477
+ ngOnDestroy() {
478
+ this.destroy$.next();
479
+ this.destroy$.complete();
480
+ }
481
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.16", ngImport: i0, type: MasterLayoutService, deps: [{ token: i1$1.Router }, { token: RequestHandlerService }, { token: AuthService }, { token: StylesBundleService }, { token: i4.TranslateService }, { token: FileLoaderService }], target: i0.ɵɵFactoryTarget.Injectable }); }
448
482
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.16", ngImport: i0, type: MasterLayoutService, providedIn: 'root' }); }
449
483
  }
450
484
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.16", ngImport: i0, type: MasterLayoutService, decorators: [{
@@ -452,7 +486,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.16", ngImpo
452
486
  args: [{
453
487
  providedIn: 'root',
454
488
  }]
455
- }], ctorParameters: () => [{ type: i1$1.Router }, { type: RequestHandlerService }, { type: AuthService }, { type: StylesBundleService }, { type: i4.TranslateService }, { type: EnvironmentService }] });
489
+ }], ctorParameters: () => [{ type: i1$1.Router }, { type: RequestHandlerService }, { type: AuthService }, { type: StylesBundleService }, { type: i4.TranslateService }, { type: FileLoaderService }] });
456
490
 
457
491
  class RequestHandlerService {
458
492
  constructor(http, authService, environmentService, utilityService, bbsfTranslateService) {
@@ -488,7 +522,7 @@ class RequestHandlerService {
488
522
  let headers = this.getHeaders();
489
523
  if (!currentRequestOptions.disableBlockUI)
490
524
  this.utilityService.startBlockUI();
491
- return this.http.get(this.environmentService.getApiUrl() + Url, { headers: headers, params: params }).pipe(takeUntil(this.onDestroy$), tap((result) => {
525
+ return this.http.get(this.environmentService.getApiUrl() + Url, { headers: headers, params: params }).pipe(takeUntil$1(this.onDestroy$), tap$1((result) => {
492
526
  if (!currentRequestOptions.disableBlockUI)
493
527
  this.utilityService.stopBlockUI();
494
528
  }, error => {
@@ -513,7 +547,7 @@ class RequestHandlerService {
513
547
  let headers = this.getHeaders();
514
548
  if (!currentRequestOptions.disableBlockUI)
515
549
  this.utilityService.startBlockUI();
516
- return this.http.post(this.environmentService.getApiUrl() + Url, model, { headers: headers, params: params, responseType: currentRequestOptions.responseType }).pipe(takeUntil(this.onDestroy$), tap((result) => {
550
+ return this.http.post(this.environmentService.getApiUrl() + Url, model, { headers: headers, params: params, responseType: currentRequestOptions.responseType }).pipe(takeUntil$1(this.onDestroy$), tap$1((result) => {
517
551
  if (!currentRequestOptions.disableBlockUI)
518
552
  this.utilityService.stopBlockUI();
519
553
  }, error => {
@@ -538,7 +572,7 @@ class RequestHandlerService {
538
572
  let headers = this.getHeaders();
539
573
  if (!currentRequestOptions.disableBlockUI)
540
574
  this.utilityService.startBlockUI();
541
- return this.http.delete(this.environmentService.getApiUrl() + Url + `/${deletedId}`, { headers: headers, params: params }).pipe(takeUntil(this.onDestroy$), tap((result) => {
575
+ return this.http.delete(this.environmentService.getApiUrl() + Url + `/${deletedId}`, { headers: headers, params: params }).pipe(takeUntil$1(this.onDestroy$), tap$1((result) => {
542
576
  if (!currentRequestOptions.disableBlockUI)
543
577
  this.utilityService.stopBlockUI();
544
578
  }, error => {
@@ -563,7 +597,7 @@ class RequestHandlerService {
563
597
  let headers = this.getHeaders();
564
598
  if (!currentRequestOptions.disableBlockUI)
565
599
  this.utilityService.startBlockUI();
566
- return this.http.put(this.environmentService.getApiUrl() + Url, model, { headers: headers, params: params, responseType: currentRequestOptions.responseType }).pipe(takeUntil(this.onDestroy$), tap((result) => {
600
+ return this.http.put(this.environmentService.getApiUrl() + Url, model, { headers: headers, params: params, responseType: currentRequestOptions.responseType }).pipe(takeUntil$1(this.onDestroy$), tap$1((result) => {
567
601
  if (!currentRequestOptions.disableBlockUI)
568
602
  this.utilityService.stopBlockUI();
569
603
  }, error => {
@@ -594,7 +628,7 @@ class RequestHandlerService {
594
628
  headers = headers.set('ignore-cookies', 'true');
595
629
  if (!currentRequestOptions.disableBlockUI)
596
630
  this.utilityService.startBlockUI();
597
- return this.http.patch(this.environmentService.getApiUrl() + Url, model, { headers: headers, params: params, responseType: currentRequestOptions.responseType }).pipe(takeUntil(this.onDestroy$), tap((result) => {
631
+ return this.http.patch(this.environmentService.getApiUrl() + Url, model, { headers: headers, params: params, responseType: currentRequestOptions.responseType }).pipe(takeUntil$1(this.onDestroy$), tap$1((result) => {
598
632
  if (!currentRequestOptions.disableBlockUI)
599
633
  this.utilityService.stopBlockUI();
600
634
  }, error => {
@@ -619,7 +653,7 @@ class RequestHandlerService {
619
653
  let headers = this.getHeaders();
620
654
  if (!currentRequestOptions.disableBlockUI)
621
655
  this.utilityService.startBlockUI();
622
- return this.http.get(this.environmentService.getApiUrl() + Url, { headers: headers, params: params, responseType: currentRequestOptions.responseType }).pipe(takeUntil(this.onDestroy$), tap((result) => {
656
+ return this.http.get(this.environmentService.getApiUrl() + Url, { headers: headers, params: params, responseType: currentRequestOptions.responseType }).pipe(takeUntil$1(this.onDestroy$), tap$1((result) => {
623
657
  if (!currentRequestOptions.disableBlockUI)
624
658
  this.utilityService.stopBlockUI();
625
659
  }, error => {
@@ -637,7 +671,7 @@ class RequestHandlerService {
637
671
  }
638
672
  const currentRequestOptions = requestOptions || new RequestOptionsModel();
639
673
  let headers = this.getHeadersUpdated();
640
- return this.http.post(this.environmentService.getApiUrl() + Url, model, { headers: headers, reportProgress: true, observe: 'events' }).pipe(takeUntil(this.onDestroy$), tap((result) => {
674
+ return this.http.post(this.environmentService.getApiUrl() + Url, model, { headers: headers, reportProgress: true, observe: 'events' }).pipe(takeUntil$1(this.onDestroy$), tap$1((result) => {
641
675
  if (!currentRequestOptions.disableBlockUI)
642
676
  this.utilityService.stopBlockUI();
643
677
  }, error => {
@@ -896,13 +930,13 @@ class LanguageService {
896
930
  if (this.isLoaded || this.isLoading)
897
931
  return; // Sync guard prevents race condition
898
932
  this.isLoading = true; // Set immediately to block concurrent calls
899
- this.loadRequest$ = this.http.get(this.apiUrl + 'GetAll', null, null).pipe(tap((languages) => {
933
+ this.loadRequest$ = this.http.get(this.apiUrl + 'GetAll', null, null).pipe(tap$1((languages) => {
900
934
  this._languages = languages;
901
935
  this.isLoaded = true;
902
936
  this.isLoading = false;
903
937
  }), shareReplay(1));
904
938
  // take(1) ensures auto-unsubscribe after completion
905
- this.loadRequest$.pipe(take(1)).subscribe({
939
+ this.loadRequest$.pipe(take$1(1)).subscribe({
906
940
  error: (err) => {
907
941
  this.isLoading = false;
908
942
  this.loadRequest$ = null;
@@ -1007,8 +1041,23 @@ class FileLoaderService {
1007
1041
  this.translate = translate;
1008
1042
  this.http = http;
1009
1043
  this.availableLanguages = ['en', 'ar']; // Add more languages as needed
1044
+ this.loadedLanguages = new Set();
1010
1045
  this.translate.addLangs(this.availableLanguages);
1011
1046
  }
1047
+ async preloadAll() {
1048
+ const defaultLang = this.getDefaultLanguage();
1049
+ try {
1050
+ await Promise.all([
1051
+ this.loadEnvironment(),
1052
+ this.loadConfigurations(),
1053
+ this.loadLanguage(defaultLang),
1054
+ ]);
1055
+ this.translate.use(defaultLang);
1056
+ }
1057
+ catch (error) {
1058
+ console.error('Error during preloadAll:', error);
1059
+ }
1060
+ }
1012
1061
  loadEnvironment() {
1013
1062
  return new Promise((resolve, reject) => {
1014
1063
  const script = document.createElement('script');
@@ -1026,6 +1075,32 @@ class FileLoaderService {
1026
1075
  document.head.insertBefore(script, document.head.firstChild);
1027
1076
  });
1028
1077
  }
1078
+ async loadConfigurations() {
1079
+ const data = await lastValueFrom(this.http.get('./assets/config/configurations.json'));
1080
+ ConfigurationService.JsonData = data;
1081
+ }
1082
+ getDefaultLanguage() {
1083
+ const lang = localStorage.getItem('language');
1084
+ if (lang && this.availableLanguages.includes(lang)) {
1085
+ return lang;
1086
+ }
1087
+ return 'en';
1088
+ }
1089
+ async loadLanguage(lang) {
1090
+ const normalizedLang = (lang || '').trim();
1091
+ if (!normalizedLang) {
1092
+ return;
1093
+ }
1094
+ if (!this.availableLanguages.includes(normalizedLang)) {
1095
+ return;
1096
+ }
1097
+ if (this.loadedLanguages.has(normalizedLang)) {
1098
+ return;
1099
+ }
1100
+ const translations = await lastValueFrom(this.http.get(`/assets/i18n/${normalizedLang}.json`));
1101
+ this.translate.setTranslation(normalizedLang, translations, true);
1102
+ this.loadedLanguages.add(normalizedLang);
1103
+ }
1029
1104
  async preloadTranslations() {
1030
1105
  try {
1031
1106
  const requests = this.availableLanguages.map(async (lang) => {
@@ -1054,7 +1129,12 @@ class ServiceWorkerHelperService {
1054
1129
  this.updates = updates;
1055
1130
  this.swRegistration = null;
1056
1131
  this.updateCheckInProgress = false;
1132
+ this.chunkHandlersRegistered = false;
1133
+ this.chunkRecoveryInFlight = false;
1134
+ this.sessionRecoveryAttemptsKey = 'bbsf_chunk_recovery_count';
1135
+ this.maxChunkRecoveryAttempts = 3;
1057
1136
  this.initializeServiceWorker();
1137
+ this.setupChunkErrorHandler();
1058
1138
  }
1059
1139
  checkForUpdates() {
1060
1140
  if (!this.updates.isEnabled) {
@@ -1065,8 +1145,6 @@ class ServiceWorkerHelperService {
1065
1145
  this.setupUpdateListeners();
1066
1146
  // Start smart checking for updates
1067
1147
  this.startSmartUpdateChecking();
1068
- // CRITICAL: Handle chunk load errors
1069
- this.setupChunkErrorHandler();
1070
1148
  }
1071
1149
  setupUpdateListeners() {
1072
1150
  this.updates.versionUpdates
@@ -1076,7 +1154,7 @@ class ServiceWorkerHelperService {
1076
1154
  this.skipWaiting();
1077
1155
  });
1078
1156
  this.updates.versionUpdates
1079
- .pipe(filter((event) => event.type === 'VERSION_READY'), switchMap(() => this.updates.activateUpdate()))
1157
+ .pipe(filter((event) => event.type === 'VERSION_READY'), switchMap$1(() => this.updates.activateUpdate()))
1080
1158
  .subscribe(() => {
1081
1159
  console.log('New version activated, reloading page...');
1082
1160
  // CRITICAL FIX: Reload the page after activation
@@ -1142,38 +1220,92 @@ class ServiceWorkerHelperService {
1142
1220
  this.swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' });
1143
1221
  }
1144
1222
  }
1145
- // CRITICAL: Handle chunk load errors
1146
1223
  setupChunkErrorHandler() {
1224
+ if (this.chunkHandlersRegistered) {
1225
+ return;
1226
+ }
1227
+ this.chunkHandlersRegistered = true;
1147
1228
  window.addEventListener('error', (event) => {
1148
- const chunkFailedMessage = /Loading chunk [\d]+ failed|ChunkLoadError/i;
1149
- const isChunkError = event.message && chunkFailedMessage.test(event.message);
1150
- if (isChunkError) {
1151
- console.warn('Chunk load error detected, attempting recovery...');
1152
- event.preventDefault(); // Prevent default error handling
1153
- this.handleChunkLoadError();
1229
+ if (!this.isChunkErrorEvent(event)) {
1230
+ return;
1154
1231
  }
1232
+ console.warn('Chunk load error detected, attempting recovery...');
1233
+ event.preventDefault();
1234
+ this.handleChunkLoadError();
1155
1235
  });
1156
- // Also handle unhandled promise rejections (some chunk errors appear here)
1157
1236
  window.addEventListener('unhandledrejection', (event) => {
1158
- const reason = event.reason?.message || event.reason?.toString() || '';
1159
- const isChunkError = /Loading chunk [\d]+ failed|ChunkLoadError/i.test(reason);
1160
- if (isChunkError) {
1161
- console.warn('Chunk load error in promise, attempting recovery...');
1162
- event.preventDefault();
1163
- this.handleChunkLoadError();
1237
+ const reason = event.reason;
1238
+ const text = (reason && typeof reason === 'object' && 'message' in reason
1239
+ ? String(reason.message)
1240
+ : '') || (reason instanceof Error ? reason.message : String(reason ?? ''));
1241
+ const name = reason instanceof Error ? reason.name : '';
1242
+ if (!this.isChunkLoadFailure(reason, text) && name !== 'ChunkLoadError') {
1243
+ return;
1164
1244
  }
1245
+ console.warn('Chunk load error in promise, attempting recovery...');
1246
+ event.preventDefault();
1247
+ this.handleChunkLoadError();
1165
1248
  });
1166
1249
  }
1250
+ isChunkErrorEvent(event) {
1251
+ const ev = event;
1252
+ const message = ev.message || '';
1253
+ const scriptSrc = event.target instanceof HTMLScriptElement ? event.target.src || undefined : undefined;
1254
+ return this.isChunkLoadFailure(ev.error, message, scriptSrc);
1255
+ }
1256
+ isChunkLoadFailure(error, message, scriptSrc) {
1257
+ const segments = [message];
1258
+ if (error instanceof Error) {
1259
+ segments.push(error.message, error.name);
1260
+ }
1261
+ else if (error && typeof error === 'object' && 'message' in error) {
1262
+ segments.push(String(error.message));
1263
+ }
1264
+ else if (error != null) {
1265
+ segments.push(String(error));
1266
+ }
1267
+ const text = segments.join(' ');
1268
+ if (/Loading chunk .+ failed/i.test(text)) {
1269
+ return true;
1270
+ }
1271
+ if (/ChunkLoadError/i.test(text)) {
1272
+ return true;
1273
+ }
1274
+ if (/Failed to fetch dynamically imported module/i.test(text)) {
1275
+ return true;
1276
+ }
1277
+ if (scriptSrc && /\.js(\?|#|$)/i.test(scriptSrc)) {
1278
+ const looksLikeBundle = /chunk|main|runtime|polyfills|vendor/i.test(scriptSrc) ||
1279
+ /\/\d+\.[\w.-]*\.js(\?|#|$)/i.test(scriptSrc);
1280
+ const opaqueOrEmpty = !message || message === 'Script error.';
1281
+ if (looksLikeBundle && opaqueOrEmpty) {
1282
+ return true;
1283
+ }
1284
+ }
1285
+ return false;
1286
+ }
1167
1287
  handleChunkLoadError() {
1168
- // Check if we've already tried to recover recently (prevent infinite loops)
1169
- const lastReloadTime = localStorage.getItem('lastChunkErrorReload');
1170
- const now = Date.now();
1171
- if (lastReloadTime && (now - parseInt(lastReloadTime, 10)) < 5000) {
1172
- console.error('Multiple chunk errors in short time, skipping auto-reload');
1288
+ if (this.chunkRecoveryInFlight) {
1173
1289
  return;
1174
1290
  }
1175
- localStorage.setItem('lastChunkErrorReload', now.toString());
1176
- // Clear cache and reload
1291
+ let attempts = 0;
1292
+ try {
1293
+ attempts = parseInt(sessionStorage.getItem(this.sessionRecoveryAttemptsKey) || '0', 10);
1294
+ }
1295
+ catch {
1296
+ /* private / blocked storage */
1297
+ }
1298
+ if (attempts >= this.maxChunkRecoveryAttempts) {
1299
+ console.error('Chunk error recovery limit reached for this session; not reloading again.');
1300
+ return;
1301
+ }
1302
+ try {
1303
+ sessionStorage.setItem(this.sessionRecoveryAttemptsKey, String(attempts + 1));
1304
+ }
1305
+ catch {
1306
+ /* ignore */
1307
+ }
1308
+ this.chunkRecoveryInFlight = true;
1177
1309
  this.clearCacheAndReload();
1178
1310
  }
1179
1311
  clearCacheAndReload() {
@@ -1222,15 +1354,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.16", ngImpo
1222
1354
  class AuthService {
1223
1355
  static { this.user = null; }
1224
1356
  get user() {
1225
- if (!this._user) {
1226
- this._user = this.getUserManager();
1357
+ // Always use static as source of truth
1358
+ // Only hydrate if static is null
1359
+ if (!AuthService.user) {
1360
+ this.getUserManager(); // This will set AuthService.user
1227
1361
  }
1228
- return this._user;
1362
+ return AuthService.user; // Always return fresh static value
1229
1363
  }
1230
1364
  static { this.UserClaims = null; }
1231
1365
  //refresh
1232
1366
  static { this.timers = []; }
1233
1367
  static { this.seconds = 0; }
1368
+ static { this.isLoginInProgress = false; }
1234
1369
  constructor(injector, http, environmentService, translateService, router, cookieService, utilityService) {
1235
1370
  this.injector = injector;
1236
1371
  this.http = http;
@@ -1243,6 +1378,9 @@ class AuthService {
1243
1378
  this.jwtHelper = new JwtHelperService();
1244
1379
  this.isAuthenticatedSubject = new BehaviorSubject(this.hasToken());
1245
1380
  this.isAuthenticate$ = this.isAuthenticatedSubject.asObservable();
1381
+ // User cache to prevent inconsistent multi-call results
1382
+ this._userCache = null;
1383
+ this.USER_CACHE_TTL = 100; // ms
1246
1384
  // Listen for logout events from other tabs
1247
1385
  window.addEventListener('storage', (event) => {
1248
1386
  if (event.key === 'auth_logout_broadcast' && event.newValue) {
@@ -1262,6 +1400,11 @@ class AuthService {
1262
1400
  return token && !this.jwtHelper.isTokenExpired(token);
1263
1401
  }
1264
1402
  getUserManager() {
1403
+ const now = Date.now();
1404
+ // Return cached user if fresh (within 100ms TTL)
1405
+ if (this._userCache && now - this._userCache.timestamp < this.USER_CACHE_TTL) {
1406
+ return this._userCache.user;
1407
+ }
1265
1408
  const token = this.cookieService.get(this.TOKEN_KEY);
1266
1409
  if (token) {
1267
1410
  try {
@@ -1270,10 +1413,7 @@ class AuthService {
1270
1413
  const expiryDate = this.jwtHelper.getTokenExpirationDate(token);
1271
1414
  const hasTimeLeft = expiryDate && expiryDate.getTime() > Date.now();
1272
1415
  if (!isExpired && hasTimeLeft) {
1273
- // Only hydrate if user is null OR token has changed
1274
- if (!AuthService.user || AuthService.user.access_token !== token) {
1275
- this.handleAccessTokenWithoutLanguage(token);
1276
- }
1416
+ this.handleAccessTokenWithoutLanguage(token);
1277
1417
  }
1278
1418
  else {
1279
1419
  this.clearLocalAuthState({ keepRedirectUrl: true });
@@ -1287,13 +1427,17 @@ class AuthService {
1287
1427
  else {
1288
1428
  this.clearLocalAuthState({ keepRedirectUrl: true });
1289
1429
  }
1430
+ // Update cache with current user state
1431
+ this._userCache = { user: AuthService.user, timestamp: now };
1290
1432
  return AuthService.user;
1291
1433
  }
1292
1434
  getUser() {
1293
- this._user = AuthService.user;
1435
+ // No-op: getter now always reads from static
1436
+ // Kept for backward compatibility
1294
1437
  }
1295
1438
  storUser(User) {
1296
- AuthService.user = this._user = this.user;
1439
+ // Getter now always reads from static, so we don't need to sync
1440
+ // This method appears unused but kept for backward compatibility
1297
1441
  }
1298
1442
  getCurrentUser() {
1299
1443
  return AuthService.user;
@@ -1308,13 +1452,17 @@ class AuthService {
1308
1452
  return AuthService.user?.profile ?? null;
1309
1453
  }
1310
1454
  isAuthenticated() {
1311
- AuthService.user = this.user;
1312
- if (!AuthService.user)
1455
+ const user = this.user; // Call getter to ensure hydrated
1456
+ if (!user) {
1313
1457
  return false;
1458
+ }
1314
1459
  // Add 10-second grace period to prevent race conditions
1315
- const expiryDate = new Date(AuthService.user.expires_at);
1460
+ const expiryDate = new Date(user.expires_at);
1316
1461
  const nowWithBuffer = new Date(Date.now() + 10000);
1317
- return expiryDate.getTime() > nowWithBuffer.getTime();
1462
+ const now = new Date();
1463
+ const secondsUntilExpiry = (expiryDate.getTime() - now.getTime()) / 1000;
1464
+ const result = expiryDate.getTime() > nowWithBuffer.getTime();
1465
+ return result;
1318
1466
  }
1319
1467
  isUserInRole(allowedPermission) {
1320
1468
  const profile = this.getCurrentUserProfile();
@@ -1362,8 +1510,12 @@ class AuthService {
1362
1510
  */
1363
1511
  clearLocalAuthState(options) {
1364
1512
  // Clear timers
1365
- AuthService.timers.map(t => clearInterval(t));
1513
+ AuthService.timers.forEach((t) => {
1514
+ clearInterval(t);
1515
+ });
1366
1516
  AuthService.timers = [];
1517
+ // Reset seconds counter to prevent stale timers
1518
+ AuthService.seconds = 0;
1367
1519
  // Clear memory
1368
1520
  AuthService.user = null;
1369
1521
  this._user = null;
@@ -1374,6 +1526,8 @@ class AuthService {
1374
1526
  if (options?.keepRedirectUrl !== true) {
1375
1527
  localStorage.removeItem('redirectUrl');
1376
1528
  }
1529
+ // Invalidate user cache since auth state is cleared
1530
+ this._userCache = null;
1377
1531
  this.isAuthenticatedSubject.next(false);
1378
1532
  }
1379
1533
  signOut() {
@@ -1412,24 +1566,37 @@ class AuthService {
1412
1566
  return this.http.get(this.environmentService.getBaseUrl() + ApiUrl + 'ClearCurrentUserSession', httpOptions);
1413
1567
  }
1414
1568
  async handleAccessToken(response) {
1415
- const token = response;
1416
- // Clear timers only (don't delete cookie to avoid race condition window)
1417
- AuthService.timers.map(t => clearInterval(t));
1418
- AuthService.timers = [];
1419
- // Update user atomically
1420
- AuthService.user = new User();
1421
- AuthService.user.token_type = "Bearer";
1422
- AuthService.user.access_token = token;
1423
- AuthService.user.profile = this.jwtHelper.decodeToken(token);
1424
- AuthService.user.expires_at = this.jwtHelper.getTokenExpirationDate(token);
1425
- this._user = AuthService.user;
1426
- // Update cookie atomically (no deletion window)
1427
- this.cookieService.set(this.TOKEN_KEY, token, AuthService.user.expires_at, "/", null, true, 'Lax');
1428
- localStorage.setItem("language", AuthService.user.profile.locale);
1429
- this.isAuthenticatedSubject.next(true);
1430
- this.setTokenSeconds();
1431
- AuthService.timers.push(this.checkRefreshToken());
1432
- await this.updateLanguage();
1569
+ AuthService.isLoginInProgress = true;
1570
+ try {
1571
+ const token = response;
1572
+ // AGGRESSIVE timer cleanup before starting new session
1573
+ AuthService.timers.map(t => clearInterval(t));
1574
+ AuthService.timers = [];
1575
+ AuthService.seconds = 0; // Reset counter
1576
+ // Brief pause to ensure intervals are cleared
1577
+ await new Promise(resolve => setTimeout(resolve, 50));
1578
+ // Update user atomically
1579
+ AuthService.user = new User();
1580
+ AuthService.user.token_type = "Bearer";
1581
+ AuthService.user.access_token = token;
1582
+ AuthService.user.profile = this.jwtHelper.decodeToken(token);
1583
+ AuthService.user.expires_at = this.jwtHelper.getTokenExpirationDate(token);
1584
+ // Add 5-minute buffer to cookie expiry to prevent browser deletion before refresh
1585
+ // JWT expiry in isAuthenticated() remains authoritative
1586
+ const cookieExpiry = new Date(AuthService.user.expires_at.getTime() + (5 * 60 * 1000));
1587
+ // Update cookie atomically (no deletion window)
1588
+ this.cookieService.set(this.TOKEN_KEY, token, cookieExpiry, "/", null, true, 'Lax');
1589
+ localStorage.setItem("language", AuthService.user.profile.locale);
1590
+ this.isAuthenticatedSubject.next(true);
1591
+ // Invalidate user cache since we have a new token
1592
+ this._userCache = null;
1593
+ this.setTokenSeconds();
1594
+ AuthService.timers.push(this.checkRefreshToken());
1595
+ await this.updateLanguage();
1596
+ }
1597
+ finally {
1598
+ AuthService.isLoginInProgress = false;
1599
+ }
1433
1600
  }
1434
1601
  handleAccessTokenWithoutLanguage(response) {
1435
1602
  const token = response;
@@ -1442,11 +1609,15 @@ class AuthService {
1442
1609
  AuthService.user.access_token = token;
1443
1610
  AuthService.user.profile = this.jwtHelper.decodeToken(token);
1444
1611
  AuthService.user.expires_at = this.jwtHelper.getTokenExpirationDate(token);
1445
- this._user = AuthService.user;
1612
+ // Add 5-minute buffer to cookie expiry to prevent browser deletion before refresh
1613
+ // JWT expiry in isAuthenticated() remains authoritative
1614
+ const cookieExpiry = new Date(AuthService.user.expires_at.getTime() + (5 * 60 * 1000));
1446
1615
  // Update cookie atomically (no deletion window)
1447
- this.cookieService.set(this.TOKEN_KEY, token, AuthService.user.expires_at, "/", null, true, 'Lax');
1616
+ this.cookieService.set(this.TOKEN_KEY, token, cookieExpiry, "/", null, true, 'Lax');
1448
1617
  localStorage.setItem("language", AuthService.user.profile.locale);
1449
1618
  this.isAuthenticatedSubject.next(true);
1619
+ // Invalidate user cache since we have a new token
1620
+ this._userCache = null;
1450
1621
  this.setTokenSeconds();
1451
1622
  AuthService.timers.push(this.checkRefreshToken());
1452
1623
  }
@@ -1464,23 +1635,68 @@ class AuthService {
1464
1635
  }
1465
1636
  checkRefreshToken() {
1466
1637
  let date = new Date();
1467
- return setInterval(() => {
1468
- if (Math.floor(AuthService.seconds) < 120 && this.isAuthenticated()) {
1638
+ const timerId = setInterval(() => {
1639
+ // Early exit if user logged out or no user exists
1640
+ if (!AuthService.user) {
1641
+ return;
1642
+ }
1643
+ const secondsRemaining = Math.floor(AuthService.seconds);
1644
+ // Early exit if seconds is invalid (negative or zero means already expired/cleared)
1645
+ if (secondsRemaining <= 0) {
1646
+ return;
1647
+ }
1648
+ // Refresh when 30 seconds remain (instead of 120)
1649
+ // This prevents continuous refresh loops with short-lived tokens
1650
+ // Timer is cleared after triggering to ensure only ONE refresh per token cycle
1651
+ if (secondsRemaining <= 30 && secondsRemaining > 0 && this.isAuthenticated()) {
1652
+ // Double-check user still exists before refreshing
1653
+ if (!AuthService.user) {
1654
+ return;
1655
+ }
1469
1656
  AuthService.timers.map(t => clearInterval(t));
1470
1657
  AuthService.timers = [];
1471
1658
  const token = this.cookieService.get(this.TOKEN_KEY);
1472
1659
  if (token) {
1473
1660
  this.refresh();
1474
1661
  }
1662
+ else {
1663
+ // Defensive: Token disappeared between isAuthenticated() check and now
1664
+ console.error('[REFRESH-TIMER] Token disappeared! Attempting recovery...', {
1665
+ timestamp: new Date().toISOString(),
1666
+ secondsRemaining,
1667
+ wasAuthenticated: true
1668
+ });
1669
+ // Try one more time after short delay
1670
+ setTimeout(() => {
1671
+ const retryToken = this.cookieService.get(this.TOKEN_KEY);
1672
+ if (retryToken) {
1673
+ this.refresh();
1674
+ }
1675
+ else {
1676
+ console.error('[REFRESH-TIMER] Token still missing after retry');
1677
+ // Don't clear auth state here - let natural expiry handle it
1678
+ // This prevents premature logout if user is still active
1679
+ }
1680
+ }, 100);
1681
+ }
1475
1682
  }
1476
1683
  AuthService.seconds--;
1477
1684
  }, 1000);
1685
+ return timerId;
1478
1686
  }
1479
1687
  setTokenSeconds() {
1480
1688
  let date = new Date();
1481
1689
  AuthService.seconds = (AuthService.user.expires_at - date) / 1000;
1482
1690
  }
1483
1691
  refresh() {
1692
+ // Don't refresh if login is in progress
1693
+ if (AuthService.isLoginInProgress) {
1694
+ return;
1695
+ }
1696
+ // Don't refresh if user logged out
1697
+ if (!AuthService.user) {
1698
+ return;
1699
+ }
1484
1700
  const refreshLockKey = 'auth_refreshing_lock';
1485
1701
  const lockValue = localStorage.getItem(refreshLockKey);
1486
1702
  // Check if another tab is refreshing (within last 5 seconds)
@@ -1489,6 +1705,10 @@ class AuthService {
1489
1705
  }
1490
1706
  // Acquire lock
1491
1707
  localStorage.setItem(refreshLockKey, Date.now().toString());
1708
+ // IMPORTANT: Don't delete cookie before refresh to prevent:
1709
+ // 1. Race condition window where no token exists
1710
+ // 2. Repeated cookie deletion/recreation cycles with short-lived tokens
1711
+ // The cookie will be atomically replaced by handleAccessTokenWithoutLanguage()
1492
1712
  const httpOptions = {
1493
1713
  headers: new HttpHeaders({
1494
1714
  'Content-Type': 'application/json',
@@ -1498,16 +1718,56 @@ class AuthService {
1498
1718
  let ApiUrl = '/api/Home/';
1499
1719
  this.http.get(this.environmentService.getApiUrl() + ApiUrl + 'RefreshAccessToken', httpOptions).subscribe({
1500
1720
  next: (res) => {
1721
+ // Check if user logged out during HTTP call
1722
+ if (!AuthService.user) {
1723
+ localStorage.removeItem(refreshLockKey);
1724
+ return; // Abort - don't set new token
1725
+ }
1726
+ // Atomically replaces the old cookie with new token
1501
1727
  this.handleAccessTokenWithoutLanguage(res.val);
1502
1728
  localStorage.removeItem(refreshLockKey);
1729
+ // Auto-navigate if user was redirected to login during token expiry
1730
+ // this.handlePostRefreshNavigation();
1503
1731
  },
1504
1732
  error: (err) => {
1505
- // Only clear auth state if refresh actually failed
1506
- this.clearLocalAuthState({ keepRedirectUrl: true });
1507
1733
  localStorage.removeItem(refreshLockKey);
1734
+ // On error, old cookie remains valid until natural expiry
1508
1735
  }
1509
1736
  });
1510
1737
  }
1738
+ /**
1739
+ * Handles navigation after successful token refresh.
1740
+ * If user is on login page and has a stored redirect URL, navigates to that URL.
1741
+ * This resolves the issue where users remain on login page after token refresh.
1742
+ * NOTE: This is not used anymore because we are using the async guard to wait for the refresh to complete before navigating to the protected route.
1743
+ */
1744
+ handlePostRefreshNavigation() {
1745
+ // Get the stored redirect URL
1746
+ const redirectUrl = this.getUrl(); // Returns localStorage 'redirectUrl' or "/"
1747
+ // Get current URL
1748
+ const currentUrl = this.router.url;
1749
+ // Check if we're currently on the login page
1750
+ const isOnLoginPage = currentUrl.toLowerCase().includes('login');
1751
+ const isAuth = this.isAuthenticated();
1752
+ // More lenient conditions:
1753
+ // 1. User is authenticated
1754
+ // 2. User is on login page
1755
+ // 3. There's ANY redirect URL (even "/")
1756
+ if (isAuth && isOnLoginPage) {
1757
+ // Determine target URL
1758
+ const targetUrl = redirectUrl && !redirectUrl.toLowerCase().includes('login')
1759
+ ? redirectUrl
1760
+ : '/Admin/Home'; // Default fallback instead of rejecting "/"
1761
+ // Clear the stored redirect URL first
1762
+ localStorage.removeItem('redirectUrl');
1763
+ // Use setTimeout to ensure router has finished current navigation
1764
+ // This prevents competing navigation attempts
1765
+ setTimeout(() => {
1766
+ this.router.navigate([targetUrl]);
1767
+ }, 100);
1768
+ }
1769
+ }
1770
+ //#region uaepass methods
1511
1771
  loginWithUAEPass() {
1512
1772
  const authEndpoint = `${this.environmentService.getUAEPassBaseUrl()}${this.environmentService.getUAEPassAuthorizationEndPoint()}`;
1513
1773
  const queryParams = {