@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.
- package/lib/common/aiCfo/aiCfoSuggestedQuestionsPageContext.d.ts +1 -1
- package/lib/coreEpics.js +2 -1
- package/lib/entity/tenant/SessionManager.d.ts +54 -0
- package/lib/entity/tenant/SessionManager.js +290 -0
- package/lib/entity/tenant/epic/sessionHeartbeatEpic.d.ts +16 -0
- package/lib/entity/tenant/epic/sessionHeartbeatEpic.js +16 -0
- package/lib/entity/tenant/sessionTypes.d.ts +26 -0
- package/lib/entity/tenant/sessionTypes.js +12 -0
- package/lib/entity/tenant/tenantReducer.d.ts +5 -1
- package/lib/entity/tenant/tenantReducer.js +23 -2
- package/lib/entity/tenant/tenantState.d.ts +1 -0
- package/lib/esm/coreEpics.js +2 -1
- package/lib/esm/entity/tenant/SessionManager.js +286 -0
- package/lib/esm/entity/tenant/epic/sessionHeartbeatEpic.js +12 -0
- package/lib/esm/entity/tenant/sessionTypes.js +9 -0
- package/lib/esm/entity/tenant/tenantReducer.js +21 -1
- package/lib/esm/index.js +5 -2
- package/lib/esm/view/spendManagement/billPay/billDetailView/epics/fetchBillDetailEpic.js +5 -5
- package/lib/index.d.ts +5 -2
- package/lib/index.js +36 -30
- package/lib/tsconfig.typecheck.tsbuildinfo +1 -1
- package/lib/view/spendManagement/billPay/billDetailView/epics/fetchBillDetailEpic.js +5 -5
- package/package.json +1 -1
|
@@ -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))))))));
|
|
@@ -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,
|
|
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,
|
|
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
|
-
|
|
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';
|