@yuuvis/client-framework 3.8.1 → 3.8.2

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.
@@ -2,10 +2,12 @@ import * as i0 from '@angular/core';
2
2
  import { inject, Injectable, ApplicationRef, DestroyRef, signal, ChangeDetectionStrategy, Component, makeEnvironmentProviders, provideAppInitializer, NgZone, NgModule } from '@angular/core';
3
3
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
4
4
  import { SwUpdate } from '@angular/service-worker';
5
- import { TranslateService } from '@ngx-translate/core';
6
- import { ConfirmService } from '@yuuvis/client-framework/common';
7
- import { filter, first, concat, interval, finalize, timer, switchMap, map, debounceTime } from 'rxjs';
8
- import { AppCacheService, BackendService, UserService, EventService, YuvEventType } from '@yuuvis/client-core';
5
+ import { _, TranslateService } from '@ngx-translate/core';
6
+ import { ConfirmService, DialogComponent } from '@yuuvis/client-framework/common';
7
+ import { filter, first, concat, interval, finalize, timer, debounceTime } from 'rxjs';
8
+ import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
9
+ import { TranslatePipe, AppCacheService, BackendService, UserService, EventService, YuvEventType } from '@yuuvis/client-core';
10
+ import { YmtButtonDirective } from '@yuuvis/material';
9
11
  import * as i1 from '@angular/material/button';
10
12
  import { MatButtonModule } from '@angular/material/button';
11
13
  import { MatSnackBar, MatSnackBarRef, MAT_SNACK_BAR_DATA, MatSnackBarLabel, MatSnackBarActions, MatSnackBarAction } from '@angular/material/snack-bar';
@@ -966,6 +968,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
966
968
  type: Injectable
967
969
  }] });
968
970
 
971
+ _('yuv.pwa.update.unrecoverable.message');
972
+ _('yuv.pwa.update.message');
973
+ _('yuv.pwa.update.title');
974
+ _('yuv.pwa.update.confirm');
975
+ _('yuv.pwa.update.cancel');
969
976
  /**
970
977
  * Detects newly deployed versions of the application (PWA) and lets the user
971
978
  * decide when to apply them.
@@ -1075,140 +1082,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1075
1082
  args: [{ providedIn: 'root' }]
1076
1083
  }] });
1077
1084
 
1078
- const SNACK_BAR_DEFAULT_DURATION = 3000;
1079
- class SnackBarService {
1080
- #snackBar;
1081
- constructor() {
1082
- this.#snackBar = inject(MatSnackBar);
1083
- this.translate = inject(TranslateService);
1084
- this.translate.onLangChange.pipe(takeUntilDestroyed()).subscribe(() => this.#snackBar.dismiss());
1085
- }
1086
- info(message, action, duration) {
1087
- return this.snack(message, {
1088
- action,
1089
- level: 'info',
1090
- ...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
1091
- });
1092
- }
1093
- success(message, action, duration) {
1094
- return this.snack(message, {
1095
- action,
1096
- level: 'success',
1097
- ...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
1098
- });
1099
- }
1100
- warning(message, action, duration) {
1101
- return this.snack(message, {
1102
- action,
1103
- level: 'warning',
1104
- ...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
1105
- });
1106
- }
1107
- danger(message, action, duration) {
1108
- return this.snack(message, {
1109
- action,
1110
- level: 'danger',
1111
- ...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
1112
- });
1113
- }
1114
- snack(message, options) {
1115
- return this.#snackBar.openFromComponent(SnackBarComponent, {
1116
- data: {
1117
- level: options.level,
1118
- message,
1119
- action: options.action
1120
- },
1121
- ...(options.duration ? { duration: options.duration } : {}),
1122
- horizontalPosition: options.horizontalPosition,
1123
- verticalPosition: options.verticalPosition,
1124
- panelClass: ['yuv-snack-bar', 'level-' + options.level]
1125
- });
1126
- }
1127
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1128
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, providedIn: 'root' }); }
1129
- }
1130
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, decorators: [{
1131
- type: Injectable,
1132
- args: [{
1133
- providedIn: 'root'
1134
- }]
1135
- }], ctorParameters: () => [] });
1136
- class SnackBarComponent {
1085
+ /**
1086
+ * Blocking modal shown shortly before session expiry.
1087
+ *
1088
+ * Replaces the previous non-blocking snackbar: opened with `disableClose: true`, it blocks
1089
+ * background interaction so an active user cannot miss it. Displays a live, second-by-second
1090
+ * countdown until expiry and a single "Extend session" action that closes the dialog with `true`.
1091
+ * If the countdown reaches zero, {@link SessionService} closes the dialog and logs the user out.
1092
+ */
1093
+ class SessionExpiryDialogComponent {
1094
+ //#region Dependencies
1095
+ #dialogRef;
1096
+ #data;
1097
+ //#endregion
1137
1098
  constructor() {
1138
- this.#snackBarRef = inject(MatSnackBarRef);
1139
- this.#snackData = inject(MAT_SNACK_BAR_DATA);
1140
- this.#translate = inject(TranslateService);
1141
- this.level = signal(this.#snackData.level, ...(ngDevMode ? [{ debugName: "level" }] : /* istanbul ignore next */ []));
1142
- this.message = bindMessage(this.#snackData.message, this.#translate, '');
1143
- this.action = bindMessage(this.#snackData.action, this.#translate, undefined);
1099
+ //#region Dependencies
1100
+ this.#dialogRef = inject(MatDialogRef);
1101
+ this.#data = inject(MAT_DIALOG_DATA);
1102
+ //#endregion
1103
+ //#region Properties
1104
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
1105
+ this.remainingSeconds = signal(this.#computeRemaining(), ...(ngDevMode ? [{ debugName: "remainingSeconds" }] : /* istanbul ignore next */ []));
1106
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
1107
+ interval(1000)
1108
+ .pipe(takeUntilDestroyed())
1109
+ .subscribe(() => this.remainingSeconds.set(this.#computeRemaining()));
1144
1110
  }
1145
- #snackBarRef;
1146
- #snackData;
1147
- #translate;
1148
- dismiss(withAction = false) {
1149
- if (withAction) {
1150
- this.#snackBarRef.dismissWithAction();
1151
- }
1152
- else if (!this.action()) {
1153
- this.#snackBarRef.dismiss();
1154
- }
1111
+ //#region Public methods
1112
+ /** Requests session extension by closing the dialog with a positive result. */
1113
+ extend() {
1114
+ this.#dialogRef.close(true);
1155
1115
  }
1156
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1157
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: SnackBarComponent, isStandalone: true, selector: "yuv-snack-bar-component", host: { properties: { "class.info": "level() === 'info'", "class.success": "level() === 'success'", "class.warning": "level() === 'warning'", "class.danger": "level() === 'danger'" } }, ngImport: i0, template: `
1158
- @let a = action();
1159
- <span
1160
- matSnackBarLabel
1161
- [attr.role]="a ? null : 'button'"
1162
- [attr.tabindex]="a ? null : 0"
1163
- (click)="dismiss()"
1164
- (keydown.enter)="dismiss()"
1165
- (keydown.space)="dismiss(); $event.preventDefault()"
1166
- >
1167
- {{ message() }}
1168
- </span>
1169
- @if (a) {
1170
- <span matSnackBarActions>
1171
- <button mat-button matSnackBarAction (click)="dismiss(true)">{{ a }}</button>
1172
- </span>
1116
+ //#endregion
1117
+ //#region Private methods
1118
+ #computeRemaining() {
1119
+ // eslint-disable-next-line @typescript-eslint/no-magic-numbers
1120
+ return Math.max(0, Math.ceil((this.#data.expiresAt - Date.now()) / 1000));
1173
1121
  }
1174
- `, isInline: true, styles: [":host{display:flex}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatSnackBarLabel, selector: "[matSnackBarLabel]" }, { kind: "directive", type: MatSnackBarActions, selector: "[matSnackBarActions]" }, { kind: "directive", type: MatSnackBarAction, selector: "[matSnackBarAction]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1122
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SessionExpiryDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1123
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.12", type: SessionExpiryDialogComponent, isStandalone: true, selector: "yuv-session-expiry-dialog", ngImport: i0, template: "<yuv-dialog [headertitle]=\"'yuv.session.expires.title' | translate\">\n <main>{{ 'yuv.session.expires.message' | translate: { seconds: remainingSeconds() } }}</main>\n <footer>\n <button ymtButton=\"primary\" type=\"button\" (click)=\"extend()\">\n {{ 'yuv.session.extend.action' | translate }}\n </button>\n </footer>\n</yuv-dialog>\n", styles: ["main{padding:var(--ymt-spacing-m)}\n"], dependencies: [{ kind: "component", type: DialogComponent, selector: "yuv-dialog", inputs: ["headertitle", "headertitel"] }, { kind: "directive", type: YmtButtonDirective, selector: "button[ymtButton], a[ymtButton]", inputs: ["ymtButton", "disabled", "aria-disabled", "disableRipple", "disabledInteractive", "button-size"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1175
1124
  }
1176
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarComponent, decorators: [{
1125
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SessionExpiryDialogComponent, decorators: [{
1177
1126
  type: Component,
1178
- args: [{ selector: 'yuv-snack-bar-component', template: `
1179
- @let a = action();
1180
- <span
1181
- matSnackBarLabel
1182
- [attr.role]="a ? null : 'button'"
1183
- [attr.tabindex]="a ? null : 0"
1184
- (click)="dismiss()"
1185
- (keydown.enter)="dismiss()"
1186
- (keydown.space)="dismiss(); $event.preventDefault()"
1187
- >
1188
- {{ message() }}
1189
- </span>
1190
- @if (a) {
1191
- <span matSnackBarActions>
1192
- <button mat-button matSnackBarAction (click)="dismiss(true)">{{ a }}</button>
1193
- </span>
1194
- }
1195
- `, imports: [MatButtonModule, MatSnackBarLabel, MatSnackBarActions, MatSnackBarAction], host: {
1196
- '[class.info]': "level() === 'info'",
1197
- '[class.success]': "level() === 'success'",
1198
- '[class.warning]': "level() === 'warning'",
1199
- '[class.danger]': "level() === 'danger'"
1200
- }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:flex}\n"] }]
1201
- }] });
1202
- function isMessageDescriptor(value) {
1203
- return typeof value === 'object';
1204
- }
1205
- function bindMessage(value, translate, fallback) {
1206
- if (isMessageDescriptor(value)) {
1207
- return toSignal(translate.stream(value.key, value.params), { initialValue: fallback ?? '' });
1208
- }
1209
- const literal = value;
1210
- return signal(literal ?? fallback);
1211
- }
1127
+ args: [{ selector: 'yuv-session-expiry-dialog', imports: [DialogComponent, YmtButtonDirective, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "<yuv-dialog [headertitle]=\"'yuv.session.expires.title' | translate\">\n <main>{{ 'yuv.session.expires.message' | translate: { seconds: remainingSeconds() } }}</main>\n <footer>\n <button ymtButton=\"primary\" type=\"button\" (click)=\"extend()\">\n {{ 'yuv.session.extend.action' | translate }}\n </button>\n </footer>\n</yuv-dialog>\n", styles: ["main{padding:var(--ymt-spacing-m)}\n"] }]
1128
+ }], ctorParameters: () => [] });
1212
1129
 
1213
1130
  /**
1214
1131
  * Manages client-side session expiry: persists expiration, tracks user and HTTP activity,
@@ -1219,7 +1136,7 @@ function bindMessage(value, translate, fallback) {
1219
1136
  * - Automatically listens to BackendService HTTP activity (debounced) to extend sessions
1220
1137
  * - Extends backend session by calling `/api-web/api/idm/whoami` endpoint` (skipped when triggered by HTTP activity)
1221
1138
  * - User activity (mouse, keyboard, scroll) inside a defined window will auto-extend the session
1222
- * - Displays a snack popup shortly before expiry; user can extend from the popup
1139
+ * - Displays a blocking countdown dialog shortly before expiry; user can extend from the dialog
1223
1140
  * - Broadcasts `SessionExtended` / `SessionLogout` to keep tabs in sync
1224
1141
  *
1225
1142
  * Setup options
@@ -1263,42 +1180,6 @@ function bindMessage(value, translate, fallback) {
1263
1180
  * - For UI activity, this service listens to DOM events (mousemove, keydown, click, scroll)
1264
1181
  */
1265
1182
  class SessionService {
1266
- constructor() {
1267
- //#region Dependencies
1268
- /**
1269
- * AppCacheService is used to persist the session expiry timestamp in localStorage.
1270
- *
1271
- * Why persist to storage instead of runtime memory:
1272
- * - Single source of truth: All browser tabs can read the same expiry value directly
1273
- * from storage without inter-tab communication
1274
- * - Avoids master/slave tab pattern: No need to elect one "master" tab to hold the
1275
- * authoritative expiry time and sync it to others
1276
- * - Survives page refresh: Users don't lose their session when refreshing the page
1277
- * - Simplifies synchronization: BroadcastChannel is only used for notifications about
1278
- * changes (extend/logout), not for maintaining shared state
1279
- * - Consistent behavior: Every tab independently calculates timers based on the same
1280
- * persisted expiry value, ensuring uniform warning/logout timing
1281
- */
1282
- this.#appCacheService = inject(AppCacheService);
1283
- this.#snackBarService = inject(SnackBarService);
1284
- this.#backendService = inject(BackendService);
1285
- this.#userService = inject(UserService);
1286
- this.#eventService = inject(EventService);
1287
- this.#destroyRef = inject(DestroyRef);
1288
- this.translate = inject(TranslateService);
1289
- //#endregion
1290
- //#region Properties
1291
- this.#sessionDuration = sessionDefaultDuration;
1292
- this.#sessionStorageKey = 'session-expires-at';
1293
- this.#activityWindowBeforeEnd = sessionActivityWindowBeforeEnd;
1294
- this.#popupBeforeEnd = sessionPopupBeforeEnd;
1295
- this.#activityDetectedInWindow = false;
1296
- this.#trackingWindowActivity = false;
1297
- this.#userActivityTrackingInitialized = false;
1298
- this.#extendingSessionViaBackend = false;
1299
- this.#isLoggingOut = false;
1300
- this.#sessionChannel = new BroadcastChannel('session_channel');
1301
- }
1302
1183
  //#region Dependencies
1303
1184
  /**
1304
1185
  * AppCacheService is used to persist the session expiry timestamp in localStorage.
@@ -1314,30 +1195,39 @@ class SessionService {
1314
1195
  * - Consistent behavior: Every tab independently calculates timers based on the same
1315
1196
  * persisted expiry value, ensuring uniform warning/logout timing
1316
1197
  */
1317
- #appCacheService;
1318
- #snackBarService;
1319
- #backendService;
1320
- #userService;
1321
- #eventService;
1322
- #destroyRef;
1198
+ #appCacheService = inject(AppCacheService);
1199
+ #dialog = inject(MatDialog);
1200
+ #backendService = inject(BackendService);
1201
+ #userService = inject(UserService);
1202
+ #eventService = inject(EventService);
1203
+ #destroyRef = inject(DestroyRef);
1323
1204
  //#endregion
1324
1205
  //#region Properties
1325
- #sessionDuration;
1326
- #sessionStorageKey;
1327
- #activityWindowBeforeEnd;
1328
- #popupBeforeEnd;
1329
- #activityWindowSchedule$;
1206
+ #sessionDuration = sessionDefaultDuration;
1207
+ /**
1208
+ * In-memory source of truth for the current expiry timestamp (ms).
1209
+ *
1210
+ * All timer scheduling reads this synchronously. The value is also persisted to
1211
+ * AppCacheService for cross-tab/refresh recovery, but the persisted copy is intentionally
1212
+ * never read back for scheduling: the previous async read-after-write against IndexedDB
1213
+ * could return a stale (about-to-expire) value right after an extend, firing the logout
1214
+ * timer immediately and logging the user out despite extending.
1215
+ */
1216
+ #expiresAt = 0;
1217
+ #sessionStorageKey = 'session-expires-at';
1218
+ #activityWindowBeforeEnd = sessionActivityWindowBeforeEnd;
1219
+ #popupBeforeEnd = sessionPopupBeforeEnd;
1330
1220
  #activityWindowStart$;
1331
1221
  #activityWindowEnd$;
1332
1222
  #logoutTimer$;
1333
1223
  #httpActivitySubscription$;
1334
- #activityDetectedInWindow;
1335
- #trackingWindowActivity;
1336
- #userActivityTrackingInitialized;
1337
- #extendingSessionViaBackend;
1338
- #isLoggingOut;
1339
- #sessionChannel;
1340
- #snackReference;
1224
+ #activityDetectedInWindow = false;
1225
+ #trackingWindowActivity = false;
1226
+ #userActivityTrackingInitialized = false;
1227
+ #extendingSessionViaBackend = false;
1228
+ #isLoggingOut = false;
1229
+ #sessionChannel = new BroadcastChannel('session_channel');
1230
+ #dialogRef;
1341
1231
  //#endregion
1342
1232
  //#region Public methods
1343
1233
  /**
@@ -1395,6 +1285,7 @@ class SessionService {
1395
1285
  if (this.#isLoggingOut)
1396
1286
  return;
1397
1287
  const newExpiresAt = expiresAt ?? Date.now() + this.#sessionDuration;
1288
+ this.#expiresAt = newExpiresAt;
1398
1289
  this.setExpiresAt(newExpiresAt);
1399
1290
  this.resetAllTimers();
1400
1291
  if (skipBackendCall) {
@@ -1464,49 +1355,45 @@ class SessionService {
1464
1355
  .subscribe(() => this.performLogout(true));
1465
1356
  }
1466
1357
  scheduleActivityWindow() {
1467
- // Capture the outer subscription so clearTimers can cancel a pending
1468
- // async getExpiresAt() resolution. Without this, two extendSession calls
1469
- // in quick succession could leak the inner timers of the first call when
1470
- // its getExpiresAt resolves after the second call's clearTimers has run.
1471
- this.#activityWindowSchedule$ = this.getExpiresAt().subscribe((expiresAt) => {
1472
- const activityWindowStartIn = expiresAt - Date.now() - this.#activityWindowBeforeEnd;
1473
- const activityWindowEndIn = expiresAt - Date.now() - this.#popupBeforeEnd;
1474
- this.#activityWindowStart$ = timer(Math.max(activityWindowStartIn, 0)).subscribe(() => {
1475
- this.#trackingWindowActivity = true;
1476
- this.#activityDetectedInWindow = false;
1477
- });
1478
- this.#activityWindowEnd$ = timer(Math.max(activityWindowEndIn, 0)).subscribe(() => {
1479
- this.#trackingWindowActivity = false;
1480
- if (this.#activityDetectedInWindow) {
1481
- this.extendSession();
1482
- }
1483
- else {
1484
- this.showPopup();
1485
- }
1486
- });
1358
+ // Scheduling reads #expiresAt synchronously (single source of truth) so the timers
1359
+ // always reflect the value just written by extendSession — no async read-after-write race.
1360
+ const now = Date.now();
1361
+ const activityWindowStartIn = this.#expiresAt - now - this.#activityWindowBeforeEnd;
1362
+ const activityWindowEndIn = this.#expiresAt - now - this.#popupBeforeEnd;
1363
+ this.#activityWindowStart$ = timer(Math.max(activityWindowStartIn, 0)).subscribe(() => {
1364
+ this.#trackingWindowActivity = true;
1365
+ this.#activityDetectedInWindow = false;
1366
+ });
1367
+ this.#activityWindowEnd$ = timer(Math.max(activityWindowEndIn, 0)).subscribe(() => {
1368
+ this.#trackingWindowActivity = false;
1369
+ if (this.#activityDetectedInWindow) {
1370
+ this.extendSession();
1371
+ }
1372
+ else {
1373
+ this.showPopup();
1374
+ }
1487
1375
  });
1488
1376
  }
1489
1377
  showPopup() {
1490
- if (this.#snackReference)
1378
+ if (this.#dialogRef)
1491
1379
  return;
1492
- const message = this.translate.instant('yuv.session.expires.message');
1493
- const action = this.translate.instant('yuv.session.extend.action');
1494
- this.#snackReference = this.#snackBarService.warning(message, action);
1495
- this.scheduleLogout();
1496
- this.#snackReference.onAction().subscribe(() => {
1497
- this.#snackReference?.dismiss();
1498
- this.extendSession();
1380
+ this.#dialogRef = this.#dialog.open(SessionExpiryDialogComponent, {
1381
+ data: { expiresAt: this.#expiresAt },
1382
+ disableClose: true,
1383
+ panelClass: 'yuv-session-expiry-dialog'
1499
1384
  });
1500
- this.#snackReference.afterDismissed().subscribe(() => {
1501
- this.#snackReference = undefined;
1385
+ this.scheduleLogout();
1386
+ this.#dialogRef.afterClosed().subscribe((extend) => {
1387
+ this.#dialogRef = undefined;
1388
+ if (extend) {
1389
+ this.extendSession();
1390
+ }
1502
1391
  });
1503
1392
  }
1504
1393
  scheduleLogout() {
1505
1394
  this.#logoutTimer$?.unsubscribe();
1506
- this.#logoutTimer$ = this.getExpiresAt()
1507
- .pipe(switchMap((expiresAt) => timer(Math.max(expiresAt - Date.now(), 0)).pipe(map(() => expiresAt))))
1508
- .subscribe((expiresAt) => {
1509
- if (Date.now() >= expiresAt) {
1395
+ this.#logoutTimer$ = timer(Math.max(this.#expiresAt - Date.now(), 0)).subscribe(() => {
1396
+ if (Date.now() >= this.#expiresAt) {
1510
1397
  this.performLogout(true);
1511
1398
  }
1512
1399
  });
@@ -1517,7 +1404,7 @@ class SessionService {
1517
1404
  this.#isLoggingOut = true;
1518
1405
  this.clearTimers();
1519
1406
  this.#httpActivitySubscription$?.unsubscribe();
1520
- this.#snackReference?.dismiss();
1407
+ this.#dialogRef?.close();
1521
1408
  if (broadcast) {
1522
1409
  this.#sessionChannel.postMessage({
1523
1410
  type: ChannelMessage.SessionLogout
@@ -1555,16 +1442,12 @@ class SessionService {
1555
1442
  resetAllTimers() {
1556
1443
  this.clearTimers();
1557
1444
  this.scheduleActivityWindow();
1558
- this.#snackReference?.dismiss();
1559
- }
1560
- getExpiresAt() {
1561
- return this.#appCacheService.getItem(this.#sessionStorageKey);
1445
+ this.#dialogRef?.close();
1562
1446
  }
1563
1447
  setExpiresAt(expiresAt) {
1564
1448
  this.#appCacheService.setItem(this.#sessionStorageKey, expiresAt).subscribe();
1565
1449
  }
1566
1450
  clearTimers() {
1567
- this.#activityWindowSchedule$?.unsubscribe();
1568
1451
  this.#activityWindowStart$?.unsubscribe();
1569
1452
  this.#activityWindowEnd$?.unsubscribe();
1570
1453
  this.#logoutTimer$?.unsubscribe();
@@ -1577,6 +1460,141 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
1577
1460
  args: [{ providedIn: 'root' }]
1578
1461
  }] });
1579
1462
 
1463
+ const SNACK_BAR_DEFAULT_DURATION = 3000;
1464
+ class SnackBarService {
1465
+ #snackBar;
1466
+ constructor() {
1467
+ this.#snackBar = inject(MatSnackBar);
1468
+ this.translate = inject(TranslateService);
1469
+ this.translate.onLangChange.pipe(takeUntilDestroyed()).subscribe(() => this.#snackBar.dismiss());
1470
+ }
1471
+ info(message, action, duration) {
1472
+ return this.snack(message, {
1473
+ action,
1474
+ level: 'info',
1475
+ ...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
1476
+ });
1477
+ }
1478
+ success(message, action, duration) {
1479
+ return this.snack(message, {
1480
+ action,
1481
+ level: 'success',
1482
+ ...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
1483
+ });
1484
+ }
1485
+ warning(message, action, duration) {
1486
+ return this.snack(message, {
1487
+ action,
1488
+ level: 'warning',
1489
+ ...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
1490
+ });
1491
+ }
1492
+ danger(message, action, duration) {
1493
+ return this.snack(message, {
1494
+ action,
1495
+ level: 'danger',
1496
+ ...(duration ? { duration } : { duration: SNACK_BAR_DEFAULT_DURATION })
1497
+ });
1498
+ }
1499
+ snack(message, options) {
1500
+ return this.#snackBar.openFromComponent(SnackBarComponent, {
1501
+ data: {
1502
+ level: options.level,
1503
+ message,
1504
+ action: options.action
1505
+ },
1506
+ ...(options.duration ? { duration: options.duration } : {}),
1507
+ horizontalPosition: options.horizontalPosition,
1508
+ verticalPosition: options.verticalPosition,
1509
+ panelClass: ['yuv-snack-bar', 'level-' + options.level]
1510
+ });
1511
+ }
1512
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1513
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, providedIn: 'root' }); }
1514
+ }
1515
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarService, decorators: [{
1516
+ type: Injectable,
1517
+ args: [{
1518
+ providedIn: 'root'
1519
+ }]
1520
+ }], ctorParameters: () => [] });
1521
+ class SnackBarComponent {
1522
+ constructor() {
1523
+ this.#snackBarRef = inject(MatSnackBarRef);
1524
+ this.#snackData = inject(MAT_SNACK_BAR_DATA);
1525
+ this.#translate = inject(TranslateService);
1526
+ this.level = signal(this.#snackData.level, ...(ngDevMode ? [{ debugName: "level" }] : /* istanbul ignore next */ []));
1527
+ this.message = bindMessage(this.#snackData.message, this.#translate, '');
1528
+ this.action = bindMessage(this.#snackData.action, this.#translate, undefined);
1529
+ }
1530
+ #snackBarRef;
1531
+ #snackData;
1532
+ #translate;
1533
+ dismiss(withAction = false) {
1534
+ if (withAction) {
1535
+ this.#snackBarRef.dismissWithAction();
1536
+ }
1537
+ else if (!this.action()) {
1538
+ this.#snackBarRef.dismiss();
1539
+ }
1540
+ }
1541
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
1542
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.12", type: SnackBarComponent, isStandalone: true, selector: "yuv-snack-bar-component", host: { properties: { "class.info": "level() === 'info'", "class.success": "level() === 'success'", "class.warning": "level() === 'warning'", "class.danger": "level() === 'danger'" } }, ngImport: i0, template: `
1543
+ @let a = action();
1544
+ <span
1545
+ matSnackBarLabel
1546
+ [attr.role]="a ? null : 'button'"
1547
+ [attr.tabindex]="a ? null : 0"
1548
+ (click)="dismiss()"
1549
+ (keydown.enter)="dismiss()"
1550
+ (keydown.space)="dismiss(); $event.preventDefault()"
1551
+ >
1552
+ {{ message() }}
1553
+ </span>
1554
+ @if (a) {
1555
+ <span matSnackBarActions>
1556
+ <button mat-button matSnackBarAction (click)="dismiss(true)">{{ a }}</button>
1557
+ </span>
1558
+ }
1559
+ `, isInline: true, styles: [":host{display:flex}\n"], dependencies: [{ kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: MatSnackBarLabel, selector: "[matSnackBarLabel]" }, { kind: "directive", type: MatSnackBarActions, selector: "[matSnackBarActions]" }, { kind: "directive", type: MatSnackBarAction, selector: "[matSnackBarAction]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1560
+ }
1561
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: SnackBarComponent, decorators: [{
1562
+ type: Component,
1563
+ args: [{ selector: 'yuv-snack-bar-component', template: `
1564
+ @let a = action();
1565
+ <span
1566
+ matSnackBarLabel
1567
+ [attr.role]="a ? null : 'button'"
1568
+ [attr.tabindex]="a ? null : 0"
1569
+ (click)="dismiss()"
1570
+ (keydown.enter)="dismiss()"
1571
+ (keydown.space)="dismiss(); $event.preventDefault()"
1572
+ >
1573
+ {{ message() }}
1574
+ </span>
1575
+ @if (a) {
1576
+ <span matSnackBarActions>
1577
+ <button mat-button matSnackBarAction (click)="dismiss(true)">{{ a }}</button>
1578
+ </span>
1579
+ }
1580
+ `, imports: [MatButtonModule, MatSnackBarLabel, MatSnackBarActions, MatSnackBarAction], host: {
1581
+ '[class.info]': "level() === 'info'",
1582
+ '[class.success]': "level() === 'success'",
1583
+ '[class.warning]': "level() === 'warning'",
1584
+ '[class.danger]': "level() === 'danger'"
1585
+ }, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:flex}\n"] }]
1586
+ }] });
1587
+ function isMessageDescriptor(value) {
1588
+ return typeof value === 'object';
1589
+ }
1590
+ function bindMessage(value, translate, fallback) {
1591
+ if (isMessageDescriptor(value)) {
1592
+ return toSignal(translate.stream(value.key, value.params), { initialValue: fallback ?? '' });
1593
+ }
1594
+ const literal = value;
1595
+ return signal(literal ?? fallback);
1596
+ }
1597
+
1580
1598
  /**
1581
1599
  * Provides and initializes the Halo Focus feature for the Angular application.
1582
1600
  *