@zeniai/client-epic-state 5.0.11 → 5.0.12-beta0ND

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.
@@ -0,0 +1,286 @@
1
+ /**
2
+ * SessionManager — Central session lifecycle manager.
3
+ *
4
+ * Responsibilities:
5
+ * 1. Track user activity (mousemove, keydown, click, scroll, touch)
6
+ * 2. Send heartbeat at configurable intervals — only when user has been active
7
+ * 3. Show warning popup after idle timeout
8
+ * 4. Auto-logout after warning countdown expires
9
+ * 5. Sync activity across same-origin tabs via BroadcastChannel so one
10
+ * active tab keeps other tabs' sessions alive (no cascading logout).
11
+ */
12
+ import { DEFAULT_SESSION_CONFIG, } from './sessionTypes';
13
+ /** Events that indicate user activity. */
14
+ const ACTIVITY_EVENTS = [
15
+ 'mousemove',
16
+ 'mousedown',
17
+ 'keydown',
18
+ 'scroll',
19
+ 'touchstart',
20
+ 'click',
21
+ ];
22
+ /** Debounce interval for activity events (ms). */
23
+ const ACTIVITY_DEBOUNCE_MS = 1000;
24
+ /** Name of the BroadcastChannel used for cross-tab activity sync. */
25
+ const BROADCAST_CHANNEL_NAME = 'zeni-session-activity';
26
+ export class SessionManager {
27
+ constructor() {
28
+ this.config = DEFAULT_SESSION_CONFIG;
29
+ this.callbacks = null;
30
+ /** Timestamps and timers */
31
+ this.lastActivityTime = 0;
32
+ /** Timestamp of the last heartbeat fire (initial/periodic/continue/activity). */
33
+ this.lastHeartbeatTime = 0;
34
+ /** Set to true whenever user (or any same-origin tab) shows activity. */
35
+ this.hadActivitySinceLastHeartbeat = false;
36
+ this.idleCheckInterval = null;
37
+ this.heartbeatInterval = null;
38
+ this.countdownInterval = null;
39
+ this.activityDebounceTimer = null;
40
+ /** State */
41
+ this.running = false;
42
+ this.warningActive = false;
43
+ this.secondsRemaining = 0;
44
+ /** Cross-tab activity sync */
45
+ this.broadcastChannel = null;
46
+ /** Bound handlers for event listener cleanup. */
47
+ this.boundOnActivity = this.onActivity.bind(this);
48
+ this.boundOnVisibilityChange = this.onVisibilityChange.bind(this);
49
+ this.boundOnBroadcastMessage = this.onBroadcastMessage.bind(this);
50
+ }
51
+ // ─── Public API ────────────────────────────────────────────
52
+ start(config = {}, callbacks) {
53
+ if (this.running) {
54
+ this.stop();
55
+ }
56
+ this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
57
+ this.callbacks = callbacks;
58
+ this.running = true;
59
+ this.warningActive = false;
60
+ this.lastActivityTime = Date.now();
61
+ this.lastHeartbeatTime = 0; // fireHeartbeat('initial') below will set this
62
+ this.hadActivitySinceLastHeartbeat = true; // initial heartbeat is expected
63
+ console.log('[SessionManager] start() called with config:', this.config);
64
+ const heartbeatEnabled = this.config.heartbeatIntervalMinutes > 0;
65
+ const idleTrackingEnabled = this.config.isAutoLogoutEnabled;
66
+ if (!heartbeatEnabled && !idleTrackingEnabled) {
67
+ console.log('[SessionManager] Heartbeat and idle tracking both disabled — nothing to start');
68
+ return;
69
+ }
70
+ // Activity listeners — needed for both heartbeat activity-gate and idle
71
+ // detection. BroadcastChannel keeps multiple tabs in sync.
72
+ ACTIVITY_EVENTS.forEach((event) => {
73
+ document.addEventListener(event, this.boundOnActivity, {
74
+ capture: true,
75
+ passive: true,
76
+ });
77
+ });
78
+ this.setupBroadcastChannel();
79
+ if (heartbeatEnabled) {
80
+ this.startHeartbeat();
81
+ // Initial heartbeat on sign-in
82
+ this.fireHeartbeat('initial');
83
+ }
84
+ else {
85
+ console.log('[SessionManager] Heartbeat disabled (heartbeatIntervalMinutes <= 0)');
86
+ }
87
+ if (idleTrackingEnabled) {
88
+ document.addEventListener('visibilitychange', this.boundOnVisibilityChange);
89
+ this.idleCheckInterval = setInterval(() => this.checkIdle(), 1000);
90
+ }
91
+ else {
92
+ console.log('[SessionManager] Idle tracking disabled for this user — heartbeat-only mode');
93
+ }
94
+ }
95
+ stop() {
96
+ this.running = false;
97
+ this.warningActive = false;
98
+ ACTIVITY_EVENTS.forEach((event) => {
99
+ document.removeEventListener(event, this.boundOnActivity, {
100
+ capture: true,
101
+ });
102
+ });
103
+ document.removeEventListener('visibilitychange', this.boundOnVisibilityChange);
104
+ this.teardownBroadcastChannel();
105
+ this.clearTimer('idleCheckInterval');
106
+ this.clearTimer('heartbeatInterval');
107
+ this.clearTimer('countdownInterval');
108
+ this.clearTimer('activityDebounceTimer');
109
+ }
110
+ continueSession() {
111
+ if (!this.running) {
112
+ return;
113
+ }
114
+ this.warningActive = false;
115
+ this.secondsRemaining = 0;
116
+ this.lastActivityTime = Date.now();
117
+ this.hadActivitySinceLastHeartbeat = true;
118
+ this.clearTimer('countdownInterval');
119
+ // Fire heartbeat immediately — user explicitly extended the session
120
+ this.fireHeartbeat('continue-session');
121
+ this.callbacks?.onSessionExtended();
122
+ this.broadcast({ type: 'activity', timestamp: Date.now() });
123
+ this.startHeartbeat();
124
+ }
125
+ isWarningActive() {
126
+ return this.warningActive;
127
+ }
128
+ getSecondsRemaining() {
129
+ return this.secondsRemaining;
130
+ }
131
+ // ─── Private ───────────────────────────────────────────────
132
+ onActivity() {
133
+ if (!this.running || this.warningActive) {
134
+ return;
135
+ }
136
+ // Debounce: only update lastActivityTime at most once per second
137
+ if (this.activityDebounceTimer != null) {
138
+ return;
139
+ }
140
+ const now = Date.now();
141
+ this.registerActivity(now);
142
+ this.broadcast({ type: 'activity', timestamp: now });
143
+ // If heartbeat is enabled and the interval has elapsed since the last
144
+ // heartbeat, fire one immediately and restart the timer. This covers
145
+ // the "user active again after >interval idle" case without waiting
146
+ // for the next scheduled tick.
147
+ if (this.config.heartbeatIntervalMinutes > 0) {
148
+ const intervalMs = this.config.heartbeatIntervalMinutes * 60 * 1000;
149
+ const timeSinceLastHeartbeat = now - this.lastHeartbeatTime;
150
+ if (timeSinceLastHeartbeat >= intervalMs) {
151
+ this.fireHeartbeat('activity-after-idle');
152
+ this.startHeartbeat();
153
+ }
154
+ }
155
+ this.activityDebounceTimer = setTimeout(() => {
156
+ this.activityDebounceTimer = null;
157
+ }, ACTIVITY_DEBOUNCE_MS);
158
+ }
159
+ onVisibilityChange() {
160
+ if (!this.running) {
161
+ return;
162
+ }
163
+ // Industry-standard idle detection: resetting on tab focus is expected.
164
+ if (document.visibilityState === 'visible') {
165
+ this.registerActivity(Date.now());
166
+ this.checkIdle();
167
+ }
168
+ }
169
+ /** Single source of truth for marking activity locally. */
170
+ registerActivity(timestamp) {
171
+ this.lastActivityTime = Math.max(this.lastActivityTime, timestamp);
172
+ this.hadActivitySinceLastHeartbeat = true;
173
+ }
174
+ checkIdle() {
175
+ if (!this.running || this.warningActive) {
176
+ return;
177
+ }
178
+ const idleMs = Date.now() - this.lastActivityTime;
179
+ const idleTimeoutMs = this.config.idleTimeoutMinutes * 60 * 1000;
180
+ if (idleMs >= idleTimeoutMs) {
181
+ this.startWarning();
182
+ }
183
+ }
184
+ startWarning() {
185
+ this.warningActive = true;
186
+ this.secondsRemaining = this.config.warningDurationSeconds;
187
+ // Don't extend session while warning is showing.
188
+ this.clearTimer('heartbeatInterval');
189
+ this.callbacks?.onWarningStart(this.secondsRemaining);
190
+ this.countdownInterval = setInterval(() => {
191
+ this.secondsRemaining -= 1;
192
+ if (this.secondsRemaining <= 0) {
193
+ this.clearTimer('countdownInterval');
194
+ this.warningActive = false;
195
+ this.callbacks?.onAutoLogout();
196
+ }
197
+ else {
198
+ this.callbacks?.onWarningTick(this.secondsRemaining);
199
+ }
200
+ }, 1000);
201
+ }
202
+ startHeartbeat() {
203
+ this.clearTimer('heartbeatInterval');
204
+ const intervalMs = this.config.heartbeatIntervalMinutes * 60 * 1000;
205
+ console.log(`[SessionManager] Heartbeat timer set to fire every ${this.config.heartbeatIntervalMinutes} min (${intervalMs} ms)`);
206
+ this.heartbeatInterval = setInterval(() => {
207
+ if (!this.running || this.warningActive) {
208
+ console.log('[SessionManager] Heartbeat tick skipped — running:', this.running, 'warningActive:', this.warningActive);
209
+ return;
210
+ }
211
+ if (!this.hadActivitySinceLastHeartbeat) {
212
+ console.log('[SessionManager] Heartbeat tick skipped — no activity in this interval');
213
+ return;
214
+ }
215
+ this.fireHeartbeat('periodic');
216
+ }, intervalMs);
217
+ }
218
+ fireHeartbeat(reason) {
219
+ console.log(`[SessionManager] Heartbeat fired (${reason})`);
220
+ this.lastHeartbeatTime = Date.now();
221
+ this.hadActivitySinceLastHeartbeat = false;
222
+ this.callbacks?.onHeartbeat();
223
+ // Let other tabs know we just fired a heartbeat — they can skip theirs
224
+ // this cycle to avoid N tabs all firing heartbeats.
225
+ this.broadcast({ type: 'heartbeat-fired', timestamp: this.lastHeartbeatTime });
226
+ }
227
+ // ─── BroadcastChannel (cross-tab activity sync) ───────────
228
+ setupBroadcastChannel() {
229
+ if (typeof BroadcastChannel === 'undefined') {
230
+ console.log('[SessionManager] BroadcastChannel not available — cross-tab sync disabled');
231
+ return;
232
+ }
233
+ try {
234
+ this.broadcastChannel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
235
+ this.broadcastChannel.addEventListener('message', this.boundOnBroadcastMessage);
236
+ console.log('[SessionManager] BroadcastChannel ready for cross-tab sync');
237
+ }
238
+ catch (err) {
239
+ console.warn('[SessionManager] BroadcastChannel setup failed:', err);
240
+ this.broadcastChannel = null;
241
+ }
242
+ }
243
+ teardownBroadcastChannel() {
244
+ if (this.broadcastChannel == null) {
245
+ return;
246
+ }
247
+ this.broadcastChannel.removeEventListener('message', this.boundOnBroadcastMessage);
248
+ this.broadcastChannel.close();
249
+ this.broadcastChannel = null;
250
+ }
251
+ broadcast(message) {
252
+ this.broadcastChannel?.postMessage(message);
253
+ }
254
+ onBroadcastMessage(event) {
255
+ if (!this.running) {
256
+ return;
257
+ }
258
+ const msg = event.data;
259
+ if (msg == null) {
260
+ return;
261
+ }
262
+ if (msg.type === 'activity') {
263
+ // Another tab saw user activity — treat as activity here too.
264
+ this.registerActivity(msg.timestamp);
265
+ }
266
+ else if (msg.type === 'heartbeat-fired') {
267
+ // Another tab already fired a heartbeat in this interval — backend
268
+ // session is already extended. Reset our local tracking so we don't
269
+ // also fire one this cycle.
270
+ this.lastHeartbeatTime = Math.max(this.lastHeartbeatTime, msg.timestamp);
271
+ this.hadActivitySinceLastHeartbeat = false;
272
+ }
273
+ }
274
+ clearTimer(name) {
275
+ const timer = this[name];
276
+ if (timer != null) {
277
+ if (name === 'activityDebounceTimer') {
278
+ clearTimeout(timer);
279
+ }
280
+ else {
281
+ clearInterval(timer);
282
+ }
283
+ this[name] = null;
284
+ }
285
+ }
286
+ }
@@ -0,0 +1,12 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
4
+ import { sendSessionHeartbeat, sessionHeartbeatFailure, sessionHeartbeatSuccess, } from '../tenantReducer';
5
+ export const sessionHeartbeatEpic = (actions$, _, zeniAPI) => actions$.pipe(filter(sendSessionHeartbeat.match), switchMap(() => zeniAPI
6
+ .postAndGetJSON(`${zeniAPI.apiEndPoints.authMicroServiceBaseUrl}/1.0/heartbeat`)
7
+ .pipe(mergeMap((response) => {
8
+ if (isSuccessResponse(response)) {
9
+ return of(sessionHeartbeatSuccess(response.data?.expiry ?? ''));
10
+ }
11
+ return of(sessionHeartbeatFailure(response.status));
12
+ }), catchError((error) => of(sessionHeartbeatFailure(createZeniAPIStatus('Heartbeat failed', 'Session heartbeat API call errored: ' + JSON.stringify(error))))))));
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Session management types for idle timeout and heartbeat.
3
+ */
4
+ export const DEFAULT_SESSION_CONFIG = {
5
+ heartbeatIntervalMinutes: 10,
6
+ idleTimeoutMinutes: 30,
7
+ isAutoLogoutEnabled: true,
8
+ warningDurationSeconds: 60,
9
+ };
@@ -428,6 +428,26 @@ const tenant = createSlice({
428
428
  signOutSuccess(draft) {
429
429
  draft.loggedInUser = undefined;
430
430
  draft.currentTenantId = undefined;
431
+ draft.sessionExpiresAt = undefined;
432
+ },
433
+ sendSessionHeartbeat() {
434
+ // No state change needed — epic handles the API call
435
+ },
436
+ sessionHeartbeatSuccess: {
437
+ prepare(expiresAt) {
438
+ return { payload: { expiresAt } };
439
+ },
440
+ reducer(draft, action) {
441
+ draft.sessionExpiresAt = action.payload.expiresAt;
442
+ },
443
+ },
444
+ sessionHeartbeatFailure: {
445
+ prepare(error) {
446
+ return { payload: { error } };
447
+ },
448
+ reducer(draft, action) {
449
+ draft.error = action.payload.error;
450
+ },
431
451
  },
432
452
  fetchSubscriptionSummaryForTenant: {
433
453
  prepare(tenantId) {
@@ -535,7 +555,7 @@ const tenant = createSlice({
535
555
  },
536
556
  },
537
557
  });
538
- export const { updateTenants, fetchAllTenants, updateTenantsSuccess, updateTenantsFailure, fetchActiveTenant, updateTenantSuccess, updateTenantFailure, updateCurrentTenant, fetchExcludedResources, updateExcludedResourcesSuccess, updateExcludedResourcesFailure, doSignIn, doMagicLinkSignIn, magicLinkSignInSuccess, magicLinkSignInFailure, sendEmailMagicLinkToUser, sendEmailMagicLinkToUserSuccess, sendEmailMagicLinkToUserFailure, updateSignInState, doSignOut, signOutSuccess, updateLoggedInUser, fetchExternalConnections, saveExternalConnection, saveExternalConnectionSuccess, saveExternalConnectionFailure, fetchExternalConnectionsFailure, fetchExternalConnectionsSuccess, clearAll, fetchSubscriptionSummaryForTenant, updateSubscriptionSummaryForTenantSuccess, updateSubscriptionSummaryForTenantFailure, updateOnboardingTenants, removeOnboardingTenant, updateTenantReimbursementInfo, updateReferViewedForLoggedInUser, updateTreasuryVideoViewedForLoggedInUser, resetSignInState, trigger2FA, updateTenantAccountingClassesEnabled, updateTenantMasterTOSInfo, verifyDeviceWithTwoFA, verifyDeviceWithTwoFASuccess, verifyDeviceWithTwoFAFailure, resendVerifyDeviceOTP, resendVerifyDeviceOTPSuccess, resendVerifyDeviceOTPFailure, } = tenant.actions;
558
+ export const { updateTenants, fetchAllTenants, updateTenantsSuccess, updateTenantsFailure, fetchActiveTenant, updateTenantSuccess, updateTenantFailure, updateCurrentTenant, fetchExcludedResources, updateExcludedResourcesSuccess, updateExcludedResourcesFailure, doSignIn, doMagicLinkSignIn, magicLinkSignInSuccess, magicLinkSignInFailure, sendEmailMagicLinkToUser, sendEmailMagicLinkToUserSuccess, sendEmailMagicLinkToUserFailure, updateSignInState, doSignOut, signOutSuccess, sendSessionHeartbeat, sessionHeartbeatSuccess, sessionHeartbeatFailure, updateLoggedInUser, fetchExternalConnections, saveExternalConnection, saveExternalConnectionSuccess, saveExternalConnectionFailure, fetchExternalConnectionsFailure, fetchExternalConnectionsSuccess, clearAll, fetchSubscriptionSummaryForTenant, updateSubscriptionSummaryForTenantSuccess, updateSubscriptionSummaryForTenantFailure, updateOnboardingTenants, removeOnboardingTenant, updateTenantReimbursementInfo, updateReferViewedForLoggedInUser, updateTreasuryVideoViewedForLoggedInUser, resetSignInState, trigger2FA, updateTenantAccountingClassesEnabled, updateTenantMasterTOSInfo, verifyDeviceWithTwoFA, verifyDeviceWithTwoFASuccess, verifyDeviceWithTwoFAFailure, resendVerifyDeviceOTP, resendVerifyDeviceOTPSuccess, resendVerifyDeviceOTPFailure, } = tenant.actions;
539
559
  export default tenant.reducer;
540
560
  /**
541
561
  * Converts tenants payload to Tenant and User State
package/lib/esm/index.js CHANGED
@@ -59,7 +59,7 @@ import { closeSnackbar, openSnackbar } from './entity/snackbar/snackbarReducer';
59
59
  import { getSnackbar } from './entity/snackbar/snackbarSelector';
60
60
  import { toPriorityCodeType, toTaskStatusCodeType, } from './entity/task/taskState';
61
61
  import { getTaskGroupById } from './entity/taskGroup/taskGroupSelector';
62
- import { clearAll, doMagicLinkSignIn, doSignIn, doSignOut, fetchActiveTenant, fetchAllTenants, fetchExcludedResources as fetchExcludedResourcesForTenant, fetchExternalConnections as fetchExternalConnectionsForTenant, fetchSubscriptionSummaryForTenant, resendVerifyDeviceOTP, resetSignInState, saveExternalConnection as saveExternalConnectionForTenant, sendEmailMagicLinkToUser, updateCurrentTenant, updateSignInState, verifyDeviceWithTwoFA, } from './entity/tenant/tenantReducer';
62
+ import { clearAll, doMagicLinkSignIn, doSignIn, doSignOut, sendSessionHeartbeat, fetchActiveTenant, fetchAllTenants, fetchExcludedResources as fetchExcludedResourcesForTenant, fetchExternalConnections as fetchExternalConnectionsForTenant, fetchSubscriptionSummaryForTenant, resendVerifyDeviceOTP, resetSignInState, saveExternalConnection as saveExternalConnectionForTenant, sendEmailMagicLinkToUser, updateCurrentTenant, updateSignInState, verifyDeviceWithTwoFA, } from './entity/tenant/tenantReducer';
63
63
  import { getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantsBaseByCheckInDateSelector, getTenantsByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView, } from './entity/tenant/tenantSelector';
64
64
  import { pushToastNotification } from './entity/toastNotification/toastNotificationReducer';
65
65
  import { getLastNotificationTime, getNotifications, } from './entity/toastNotification/toastNotificationSelector';
@@ -433,7 +433,7 @@ export { toTimeframeTick, mapTimePeriodtoTimeframeTick, toTimePeriod, toAbsolute
433
433
  export { toVendorSpendTrendFilterTabsTypeStrict, } from './entity/vendorExpense/vendorExpenseSelector';
434
434
  export { getNumberOfPeriods };
435
435
  export { SCHEDULE_DAYS_OF_MONTH, toScheduleDaysOfMonth, toMonth, toMonthStrict, toQuarter, toQuarterStrict, };
436
- export { getTenantBaseById, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getTenantsByCheckInDateSelector, getTenantsBaseByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, toTenantCoreDetailsView, getTenantBaseViewForTenantView, getTenantBaseViewForTenantId, isZeniDomainTenant, isTenantUsingZeniCOA, fetchActiveTenant, fetchAllTenants, updateCurrentTenant, clearAll, doMagicLinkSignIn, verifyDeviceWithTwoFA, resendVerifyDeviceOTP, sendEmailMagicLinkToUser, doSignIn, updateSignInState, doSignOut, resetSignInState, fetchExcludedResourcesForTenant, fetchExternalConnectionsForTenant, saveExternalConnectionForTenant, fetchSubscriptionSummaryForTenant, isTenantBankingOnly, isTenantCardsOnly, isTenantBookkeepingEnabled, isOtherProductsEnabledForTenant, };
436
+ export { getTenantBaseById, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getTenantsByCheckInDateSelector, getTenantsBaseByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, toTenantCoreDetailsView, getTenantBaseViewForTenantView, getTenantBaseViewForTenantId, isZeniDomainTenant, isTenantUsingZeniCOA, fetchActiveTenant, fetchAllTenants, updateCurrentTenant, clearAll, doMagicLinkSignIn, verifyDeviceWithTwoFA, resendVerifyDeviceOTP, sendEmailMagicLinkToUser, doSignIn, updateSignInState, doSignOut, sendSessionHeartbeat, resetSignInState, fetchExcludedResourcesForTenant, fetchExternalConnectionsForTenant, saveExternalConnectionForTenant, fetchSubscriptionSummaryForTenant, isTenantBankingOnly, isTenantCardsOnly, isTenantBookkeepingEnabled, isOtherProductsEnabledForTenant, };
437
437
  export { toAccountType, toAccountGroupType, getAccountGroupKey, };
438
438
  export { getClassById } from './entity/class/classSelector';
439
439
  export { getForecast };
@@ -631,3 +631,6 @@ export { getAutoTransferRules, getAutoTransferRuleById, getAutoTransferRuleHisto
631
631
  export { BULK_UPLOAD_AUTOMATCHING_TIMEOUT_MS, BULK_UPLOAD_BAR_COMPLETE_HOLD_MS, } from './view/expenseAutomationView/helpers/bulkUploadTiming';
632
632
  export { fetchTransactionActivityLog } from './view/transactionActivityLogView/transactionActivityLogViewReducer';
633
633
  export { getTransactionActivityLogView, } from './view/transactionActivityLogView/transactionActivityLogViewSelector';
634
+ // ── Session Management ──────────────────────────────────────────────
635
+ export { SessionManager } from './entity/tenant/SessionManager';
636
+ export { DEFAULT_SESSION_CONFIG } from './entity/tenant/sessionTypes';
@@ -53,7 +53,7 @@ export const fetchBillDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(
53
53
  response.data?.transaction?.transaction_id != null) {
54
54
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
55
55
  const transcationId = response.data.transaction.transaction_id;
56
- const vendorInResponse = response.data?.vendors[0];
56
+ const vendorInResponse = response.data?.vendors?.[0];
57
57
  const flattenedContacts = vendorInResponse?.contacts ?? [];
58
58
  const flattenedAddresses = getFlattenedAddress(response.data?.vendors ?? []);
59
59
  const flattenedBankAccounts = vendorInResponse?.bank_accounts ?? [];
@@ -85,7 +85,7 @@ export const fetchBillDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(
85
85
  ...response.data.bank_accounts,
86
86
  ]),
87
87
  updatePaymentInstruments(flattenedPaymentInstruments),
88
- updateVendors(response.data.vendors),
88
+ updateVendors(response.data.vendors ?? []),
89
89
  // end: save all vendor related entities
90
90
  ];
91
91
  if (vendorInResponse == null &&
@@ -94,15 +94,15 @@ export const fetchBillDetailEpic = (actions$, state$, zeniAPI) => actions$.pipe(
94
94
  fetchActions.push(fetchVendorsList({
95
95
  searchString: response.data?.transaction.candidate_vendor_name,
96
96
  }));
97
- fetchActions.push(waitForVendorByNameThenUpdateBillDetail(billId, response.data, response.data.vendors[0]?.vendor_id));
97
+ fetchActions.push(waitForVendorByNameThenUpdateBillDetail(billId, response.data, undefined));
98
98
  }
99
99
  else {
100
100
  fetchActions.push(updateBillDetail({ billId, billDetail: response.data }));
101
- fetchActions.push(updateBillDetailFetchState(transcationId, 'Completed', undefined, response.data.vendors[0]?.vendor_id));
101
+ fetchActions.push(updateBillDetailFetchState(transcationId, 'Completed', undefined, vendorInResponse?.vendor_id));
102
102
  const transactionInResponse = response.data?.transaction;
103
103
  if (transactionInResponse != null &&
104
104
  transactionInResponse.total_amount != null &&
105
- Boolean(vendorInResponse) === true) {
105
+ vendorInResponse != null) {
106
106
  fetchActions.push(fetchDuplicateBill(vendorInResponse.vendor_name, vendorInResponse.vendor_id, transactionInResponse.total_amount, transactionInResponse.recipient_total_amount, transactionInResponse.document_id, billId));
107
107
  }
108
108
  const billHasPaymentError = [
package/lib/index.d.ts CHANGED
@@ -123,7 +123,7 @@ import { PriorityCodeType, Task, TaskCodeType, TaskPriority, TaskStatus, TaskSta
123
123
  import { getTaskGroupById } from './entity/taskGroup/taskGroupSelector';
124
124
  import { TaskGroupState } from './entity/taskGroup/taskGroupState';
125
125
  import { TaskGroupTemplate } from './entity/taskGroupTemplate/taskGroupTemplateState';
126
- import { DoSignInPayload, clearAll, doMagicLinkSignIn, doSignIn, doSignOut, fetchActiveTenant, fetchAllTenants, fetchExcludedResources as fetchExcludedResourcesForTenant, fetchExternalConnections as fetchExternalConnectionsForTenant, fetchSubscriptionSummaryForTenant, resendVerifyDeviceOTP, resetSignInState, saveExternalConnection as saveExternalConnectionForTenant, sendEmailMagicLinkToUser, updateCurrentTenant, updateSignInState, verifyDeviceWithTwoFA } from './entity/tenant/tenantReducer';
126
+ import { DoSignInPayload, clearAll, doMagicLinkSignIn, doSignIn, doSignOut, sendSessionHeartbeat, fetchActiveTenant, fetchAllTenants, fetchExcludedResources as fetchExcludedResourcesForTenant, fetchExternalConnections as fetchExternalConnectionsForTenant, fetchSubscriptionSummaryForTenant, resendVerifyDeviceOTP, resetSignInState, saveExternalConnection as saveExternalConnectionForTenant, sendEmailMagicLinkToUser, updateCurrentTenant, updateSignInState, verifyDeviceWithTwoFA } from './entity/tenant/tenantReducer';
127
127
  import { CurrentTenant, TenantBaseView, TenantCoreDetailsView, TenantView, TenantsBaseOrdered, TenantsOrdered, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getTenantBaseById, getTenantBaseViewForTenantId, getTenantBaseViewForTenantView, getTenantsBaseByCheckInDateSelector, getTenantsByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, isOtherProductsEnabledForTenant, isTenantBankingOnly, isTenantBookkeepingEnabled, isTenantCardsOnly, isTenantUsingZeniCOA, isZeniDomainTenant, toTenantCoreDetailsView } from './entity/tenant/tenantSelector';
128
128
  import { Connection, LoggedInUser, RoleResource, Tenant, TenantProductSettings } from './entity/tenant/tenantState';
129
129
  import { ToastNotificationPayload } from './entity/toastNotification/toastNotificationPayload';
@@ -643,7 +643,7 @@ export { TimeframeTick, TimeframeTickWithMetaData, toTimeframeTick, mapTimePerio
643
643
  export { VendorSpendTrendFilterTabType, toVendorSpendTrendFilterTabsTypeStrict, } from './entity/vendorExpense/vendorExpenseSelector';
644
644
  export { getNumberOfPeriods };
645
645
  export { SCHEDULE_DAYS_OF_MONTH, ScheduleDaysOfMonth, toScheduleDaysOfMonth, Day, Month, toMonth, toMonthStrict, Quarter, toQuarter, toQuarterStrict, AbsoluteDay, TimePeriod, };
646
- export { Tenant, TenantView, LoggedInUser, TenantBaseView, CurrentTenant, TenantProductSettings, getTenantBaseById, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getTenantsByCheckInDateSelector, getTenantsBaseByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, toTenantCoreDetailsView, getTenantBaseViewForTenantView, getTenantBaseViewForTenantId, isZeniDomainTenant, isTenantUsingZeniCOA, TenantsBaseOrdered, TenantsOrdered, TenantCoreDetailsView, fetchActiveTenant, fetchAllTenants, updateCurrentTenant, clearAll, doMagicLinkSignIn, verifyDeviceWithTwoFA, resendVerifyDeviceOTP, sendEmailMagicLinkToUser, doSignIn, updateSignInState, DoSignInPayload, doSignOut, resetSignInState, RoleResource, Connection, fetchExcludedResourcesForTenant, fetchExternalConnectionsForTenant, saveExternalConnectionForTenant, fetchSubscriptionSummaryForTenant, isTenantBankingOnly, isTenantCardsOnly, isTenantBookkeepingEnabled, isOtherProductsEnabledForTenant, };
646
+ export { Tenant, TenantView, LoggedInUser, TenantBaseView, CurrentTenant, TenantProductSettings, getTenantBaseById, getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, getTenantsByCheckInDateSelector, getTenantsBaseByCheckInDateSelector, getTenantsCoreDetailsByCheckInDateSelector, toTenantCoreDetailsView, getTenantBaseViewForTenantView, getTenantBaseViewForTenantId, isZeniDomainTenant, isTenantUsingZeniCOA, TenantsBaseOrdered, TenantsOrdered, TenantCoreDetailsView, fetchActiveTenant, fetchAllTenants, updateCurrentTenant, clearAll, doMagicLinkSignIn, verifyDeviceWithTwoFA, resendVerifyDeviceOTP, sendEmailMagicLinkToUser, doSignIn, updateSignInState, DoSignInPayload, doSignOut, sendSessionHeartbeat, resetSignInState, RoleResource, Connection, fetchExcludedResourcesForTenant, fetchExternalConnectionsForTenant, saveExternalConnectionForTenant, fetchSubscriptionSummaryForTenant, isTenantBankingOnly, isTenantCardsOnly, isTenantBookkeepingEnabled, isOtherProductsEnabledForTenant, };
647
647
  export { Account, AccountType, AccountBase, StatementCloseDay, toAccountType, AccountReport, NestedAccountReport, AccountWithBalanceReport, AccountGroup, AccountGroupReportV2, AccountGroupType, toAccountGroupType, AccountGroupReport, getAccountGroupKey, AccountGroupKey, RecommendedAccountBase, };
648
648
  export { Class, ClassBase, RecommendedClassBase, } from './entity/class/classState';
649
649
  export { getClassById } from './entity/class/classSelector';
@@ -901,3 +901,6 @@ export { BULK_UPLOAD_AUTOMATCHING_TIMEOUT_MS, BULK_UPLOAD_BAR_COMPLETE_HOLD_MS,
901
901
  export { fetchTransactionActivityLog } from './view/transactionActivityLogView/transactionActivityLogViewReducer';
902
902
  export { TransactionActivityLogViewSelectorView, getTransactionActivityLogView, } from './view/transactionActivityLogView/transactionActivityLogViewSelector';
903
903
  export { UserGroup } from './entity/userGroups/userGroupsState';
904
+ export { SessionManager } from './entity/tenant/SessionManager';
905
+ export type { SessionCallbacks, SessionConfig } from './entity/tenant/sessionTypes';
906
+ export { DEFAULT_SESSION_CONFIG } from './entity/tenant/sessionTypes';