-
Notifications
You must be signed in to change notification settings - Fork 86
/
IdleTimeout.tsx
241 lines (218 loc) · 7.42 KB
/
IdleTimeout.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
import type * as React from 'react';
import { useState, useEffect, useCallback } from 'react';
import useInterval from './useInterval';
import IdleTimeoutDialog from './IdleTimeoutDialog';
import { checkPassiveSupport } from './utilities/checkPassive';
import { t } from '../i18n';
export interface IdleTimeoutProps {
/**
* The text for the 'continue session' button in warning dialog.
*/
continueSessionText?: React.ReactNode;
/**
* The heading text for the warning dialog.
*/
heading?: React.ReactNode;
/**
* The text for the button that ends the session in warning dialog.
*/
endSessionButtonText?: React.ReactNode;
/**
* The URL to direct to when the user intentionally ends the session.
*/
endSessionUrl?: string;
/**
* A formatting function that returns the string to be used in the warning modal.
* The formatting function is provided the timeTilTimeout (in minutes).
*/
formatMessage?: (timeTilTimeout: number) => React.ReactNode;
/**
* Optional function that is called when the user chooses to keep the session alive. This function is called by the 'continue session' button or the 'close' button.
* The IdleTimeout component will reset the countdown internally.
*/
onSessionContinue?: (...args: any[]) => any;
/**
* Optional function that is called when the session is manually ended by user.
* If not provided, the behavior of `onTimeout` will be used.
*/
onSessionForcedEnd?: (...args: any[]) => any;
/**
* Function that is called when the timeout countdown reaches zero.
*/
onTimeout: (...args: any[]) => any;
/**
* Describes if the button to manually end session should be shown in the warning dialog.
*/
showSessionEndButton?: boolean;
/**
* Defines the amount of minutes of idle activity until the session is timed out
*/
timeToTimeout: number;
/**
* Defines the amount of minutes of idle activity that will trigger the warning message.
*/
timeToWarning: number;
}
/**
*
* @param timeTilTimeout {number} time in minutes until timeout occurs
* @returns {string | ReactNode}
*/
const defaultMessageFormatter = (timeTilTimeout: number): React.ReactNode => {
const unitOfTime =
timeTilTimeout === 1 ? t('idleTimeoutDialog.min') : t('idleTimeoutDialog.mins');
return (
<p>
{t('idleTimeoutDialog.messageLine1')}
<br />
{t('idleTimeoutDialog.messageLine2')}
<strong>
{timeTilTimeout} {unitOfTime}
</strong>
.
<br />
<br />
{t('idleTimeoutDialog.continueSessionMessage')}
</p>
);
};
// local storage variable name
const lastActiveCookieName = 'CMS_DS_IT_LAST_ACTIVE';
/**
* For information about how and when to use this component,
* [refer to its full documentation page](https://design.cms.gov/components/idle-timeout/).
*/
export const IdleTimeout = ({
continueSessionText = t('idleTimeoutDialog.continueSessionButtonText'),
heading = t('idleTimeoutDialog.heading'),
endSessionButtonText = t('idleTimeoutDialog.endSessionButtonText'),
endSessionUrl = '/logout',
formatMessage = defaultMessageFormatter,
onSessionContinue,
onSessionForcedEnd,
onTimeout,
showSessionEndButton = false,
timeToTimeout,
timeToWarning,
}: IdleTimeoutProps): JSX.Element => {
if (timeToWarning > timeToTimeout) {
console.error(
'Error in TimeoutManager component. `timeToWarning` is greater or equal to `timeToTimeout`'
);
}
const msBetweenStatusChecks = 30000;
// convert minutes to milliseconds
const msToTimeout = timeToTimeout * 60000;
const msToWarning = timeToWarning * 60000;
const [checkStatusTime, setCheckStatusTime] = useState<number>(null);
const [showWarning, setShowWarning] = useState<boolean>(false);
const [timeInWarning, setTimeInWarning] = useState<number>(
Math.ceil(timeToTimeout - timeToWarning)
);
// cleanup timeouts & intervals
const clearTimeouts = () => {
setCheckStatusTime(null);
};
// when the countdown for the session ends, clean up, call callback & close modal
const handleTimeout = () => {
clearTimeouts();
removeEventListeners();
onTimeout();
setShowWarning(false);
};
// when it's time to warn the user about idleness,
// set an interval that updates the modal message
const handleWarningTimeout = () => {
removeEventListeners();
setShowWarning(true);
};
const setTimeoutCookies = () => {
localStorage.setItem(lastActiveCookieName, Date.now().toString());
if (checkStatusTime === null) {
setCheckStatusTime(msBetweenStatusChecks);
}
};
// have to useCallback so that the function can be removed properly from event listeners
const resetTimeouts = useCallback(() => {
clearTimeouts();
setTimeoutCookies();
}, []);
const removeEventListeners = () => {
document.removeEventListener('mousemove', resetTimeouts);
document.removeEventListener('keypress', resetTimeouts);
};
const addEventListeners = () => {
const passiveSupported = checkPassiveSupport();
const options = passiveSupported ? { passive: true } : false;
document.addEventListener('mousemove', resetTimeouts, options);
document.addEventListener('keypress', resetTimeouts, options);
};
const checkWarningStatus = () => {
const lastActiveTime = Number(localStorage.getItem(lastActiveCookieName));
const now = Date.now();
const msSinceLastActive = now - lastActiveTime;
if (msSinceLastActive >= msToTimeout) {
handleTimeout();
} else if (!showWarning && msSinceLastActive >= msToWarning) {
removeEventListeners();
handleWarningTimeout();
} else if (showWarning && msSinceLastActive >= msToWarning) {
// if the warning is showing, update the timeInWarning variable (in minutes)
const minutesLeft = Math.ceil((msToTimeout - msSinceLastActive) / 60000);
setTimeInWarning(minutesLeft);
} else if (showWarning && msSinceLastActive < msToWarning) {
// if another tab updates the last active time, hide current warning modal
setShowWarning(false);
}
};
useEffect(() => {
setTimeoutCookies();
// event listeners have to be added before status check in case they are removed in status check
addEventListeners();
checkWarningStatus();
return () => {
clearTimeouts();
removeEventListeners();
};
}, []);
useEffect(() => {
setTimeInWarning(Math.ceil(timeToTimeout - timeToWarning));
resetTimeouts();
}, [timeToWarning, timeToTimeout]);
// setup interval to check status every 30 seconds
useInterval(checkWarningStatus, checkStatusTime);
const handleSessionContinue = () => {
if (onSessionContinue) {
onSessionContinue();
}
setShowWarning(false);
setTimeInWarning(Math.ceil(timeToTimeout - timeToWarning));
resetTimeouts();
addEventListeners();
};
const handleSessionForcedEnd = () => {
if (onSessionForcedEnd) {
onSessionForcedEnd();
} else {
onTimeout();
}
clearTimeouts();
removeEventListeners();
setShowWarning(false);
};
return (
<IdleTimeoutDialog
continueSessionText={continueSessionText}
heading={heading}
endSessionButtonText={endSessionButtonText}
endSessionUrl={endSessionUrl}
message={formatMessage(timeInWarning)}
onSessionContinue={handleSessionContinue}
onSessionForcedEnd={handleSessionForcedEnd}
showSessionEndButton={showSessionEndButton}
onClose={handleSessionContinue}
isOpen={showWarning}
/>
);
};
export default IdleTimeout;