@stokr/components-library 3.0.65 → 3.0.66
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/README.md +16 -3
- package/dist/context/AuthContext.js +82 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,17 +127,30 @@ Missing overrides may trigger a **one-time** warning listing unresolved `VITE_*`
|
|
|
127
127
|
### Optional provider props
|
|
128
128
|
|
|
129
129
|
- **`inactivityTimeMs`** — Idle timeout before auto-logout + session modal (default 5 min).
|
|
130
|
+
- **`inactivityWarningBeforeMs`** — How long before that timeout to show the **“session about to expire”** warning modal (default 30 s). See [Inactivity auto-logout](#inactivity-auto-logout).
|
|
130
131
|
- **`accessTokenExpiryMs`** — Cookie TTL when **`Auth.setAccessToken`** runs (default **`DEFAULT_TOKEN_EXPIRY_MS`** = 1 h); not extended by **`getUser`** alone.
|
|
131
|
-
- **`hideInactivityModal`** — Suppress built-in session
|
|
132
|
+
- **`hideInactivityModal`** — Suppress both built-in inactivity modals (warning + session expired).
|
|
133
|
+
- **`onSessionExpiredLoginClick`** — Called when the user clicks **Log in** on the session-expired modal.
|
|
132
134
|
- **`customValidateGetUser(user)`** — Hook after **`user/get`** succeeds (see `src/context/AuthContext.js`).
|
|
133
135
|
|
|
136
|
+
### Inactivity auto-logout {#inactivity-auto-logout}
|
|
137
|
+
|
|
138
|
+
When a user is logged in, `AuthProvider` resets an idle timer on mouse, keyboard, click, scroll, and touch events.
|
|
139
|
+
|
|
140
|
+
1. **Warning modal** — Shown **`inactivityWarningBeforeMs`** before the idle deadline (default: 30 s before the 5 min timeout). The user can click **To continue, press here** to reset the idle timer, or close the modal (logout still happens at the deadline). Any activity also resets the timer and closes the warning.
|
|
141
|
+
2. **Session expired modal** — Shown when the idle deadline is reached (existing behaviour). The warning modal closes automatically at the same moment.
|
|
142
|
+
|
|
143
|
+
Neither modal is shown when the user is not logged in. Both are skipped when **`hideInactivityModal`** is set.
|
|
144
|
+
|
|
145
|
+
**Storybook:** `Authentication / Auto-logout (inactivity)` → **With warning modal** (20 s idle, warning at 10 s).
|
|
146
|
+
|
|
134
147
|
### AuthContext consumer {#authcontext-usecontext}
|
|
135
148
|
|
|
136
149
|
`import { AuthContext } from '…'` then `useContext(AuthContext)`.
|
|
137
150
|
|
|
138
151
|
- **Invalid Firebase guard:** value is **`{ user: null, isFetchingUser: false }`** only (no methods).
|
|
139
|
-
- **State (grouped):** `user` / `firebaseUser`, `isFetchingUser`, `avatar`; MFA (`waitingFor2fa`, `userMfaEnrollment`, `firebaseError`); verify-email (`verifyEmailError`, `isVerifyingEmail`); session UX (`loggedOutDueToInactivity`, `loggedOutDueToCookieExpiry`, `sessionExpiryPendingReason`).
|
|
140
|
-
- **Actions (grouped):** `userRef`, `loginUser`, `logoutUser`, `getUser`, `setUser`, `updateUser`, `refreshIdToken`; `checkUserIsValid`, `checkTokenIsValid`; `uploadPhoto`, `deletePhoto`, `checkUserPhoto`; MFA enroll / verify / unenroll + `reset2faFlow`; subscription/onboarding helpers; password & email (`handleResetPassword`, `handleVerifyEmail`, `requestUpdateEmail`, …); wallets / PoA (`uploaProofOfAddress` keeps the source spelling); **`dismissSessionExpiryModal`**.
|
|
152
|
+
- **State (grouped):** `user` / `firebaseUser`, `isFetchingUser`, `avatar`; MFA (`waitingFor2fa`, `userMfaEnrollment`, `firebaseError`); verify-email (`verifyEmailError`, `isVerifyingEmail`); session UX (`loggedOutDueToInactivity`, `loggedOutDueToCookieExpiry`, `sessionExpiryPendingReason`, `showInactivityWarningModal`).
|
|
153
|
+
- **Actions (grouped):** `userRef`, `loginUser`, `logoutUser`, `getUser`, `setUser`, `updateUser`, `refreshIdToken`; `checkUserIsValid`, `checkTokenIsValid`; `uploadPhoto`, `deletePhoto`, `checkUserPhoto`; MFA enroll / verify / unenroll + `reset2faFlow`; subscription/onboarding helpers; password & email (`handleResetPassword`, `handleVerifyEmail`, `requestUpdateEmail`, …); wallets / PoA (`uploaProofOfAddress` keeps the source spelling); **`dismissSessionExpiryModal`**, **`extendSessionFromInactivityWarning`**, **`dismissInactivityWarningModal`**.
|
|
141
154
|
|
|
142
155
|
Use **`AuthConsumer`** for the render-prop pattern.
|
|
143
156
|
|
|
@@ -20,10 +20,12 @@ import { SuccessModal as SuccessModalComponent } from "../components/Modal/Succe
|
|
|
20
20
|
const AuthContext = React__default.createContext();
|
|
21
21
|
const FALLBACK_AUTH_CONTEXT_VALUE = { user: null, isFetchingUser: false };
|
|
22
22
|
const INACTIVITY_TIME_MS = 5 * 60 * 1e3;
|
|
23
|
+
const INACTIVITY_WARNING_BEFORE_MS = 30 * 1e3;
|
|
23
24
|
const INACTIVITY_EVENTS = ["mousemove", "keydown", "click", "scroll", "touchstart"];
|
|
24
25
|
class AuthProviderClass extends Component {
|
|
25
26
|
userRef = React__default.createRef(null);
|
|
26
27
|
inactivityTimerRef = null;
|
|
28
|
+
inactivityWarningTimerRef = null;
|
|
27
29
|
cookieExpiryTimerRef = null;
|
|
28
30
|
// Timestamp of the last inactivity timer reset; used to throttle the reset
|
|
29
31
|
// to at most once per second so frequent DOM events (mousemove fires 30-60×/s)
|
|
@@ -47,7 +49,9 @@ class AuthProviderClass extends Component {
|
|
|
47
49
|
loggedOutDueToInactivity: false,
|
|
48
50
|
loggedOutDueToCookieExpiry: false,
|
|
49
51
|
/** When set, show session modal but keep session until user dismisses (avoids login + session modals at once). */
|
|
50
|
-
sessionExpiryPendingReason: null
|
|
52
|
+
sessionExpiryPendingReason: null,
|
|
53
|
+
/** Shown ~30s before inactivity logout; user can extend the session from the modal. */
|
|
54
|
+
showInactivityWarningModal: false
|
|
51
55
|
};
|
|
52
56
|
componentDidMount() {
|
|
53
57
|
if (this.props.config) {
|
|
@@ -88,6 +92,11 @@ class AuthProviderClass extends Component {
|
|
|
88
92
|
this.clearInactivityTimer();
|
|
89
93
|
this.clearCookieExpiryTimer();
|
|
90
94
|
}
|
|
95
|
+
const sessionJustEnded = hadSession && !hasSession;
|
|
96
|
+
const sessionExpiryJustPending = !prevState.sessionExpiryPendingReason && this.state.sessionExpiryPendingReason;
|
|
97
|
+
if (this.state.showInactivityWarningModal && (sessionJustEnded || sessionExpiryJustPending)) {
|
|
98
|
+
this.setState({ showInactivityWarningModal: false });
|
|
99
|
+
}
|
|
91
100
|
}
|
|
92
101
|
componentWillUnmount() {
|
|
93
102
|
INACTIVITY_EVENTS.forEach((event) => window.removeEventListener(event, this.resetInactivityTimer));
|
|
@@ -101,7 +110,14 @@ class AuthProviderClass extends Component {
|
|
|
101
110
|
}
|
|
102
111
|
return !!accessToken;
|
|
103
112
|
};
|
|
113
|
+
clearInactivityWarningTimer = () => {
|
|
114
|
+
if (this.inactivityWarningTimerRef) {
|
|
115
|
+
clearTimeout(this.inactivityWarningTimerRef);
|
|
116
|
+
this.inactivityWarningTimerRef = null;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
104
119
|
clearInactivityTimer = () => {
|
|
120
|
+
this.clearInactivityWarningTimer();
|
|
105
121
|
if (this.inactivityTimerRef) {
|
|
106
122
|
clearTimeout(this.inactivityTimerRef);
|
|
107
123
|
this.inactivityTimerRef = null;
|
|
@@ -138,6 +154,10 @@ class AuthProviderClass extends Component {
|
|
|
138
154
|
const { inactivityTimeMs } = this.props;
|
|
139
155
|
return typeof inactivityTimeMs === "number" && inactivityTimeMs > 0 ? inactivityTimeMs : INACTIVITY_TIME_MS;
|
|
140
156
|
};
|
|
157
|
+
getInactivityWarningBeforeMs = () => {
|
|
158
|
+
const { inactivityWarningBeforeMs } = this.props;
|
|
159
|
+
return typeof inactivityWarningBeforeMs === "number" && inactivityWarningBeforeMs > 0 ? inactivityWarningBeforeMs : INACTIVITY_WARNING_BEFORE_MS;
|
|
160
|
+
};
|
|
141
161
|
/** Cookie lifetime for `STOKR_ACCESS_TOKEN` — not the same as inactivity timeout. */
|
|
142
162
|
getAccessTokenExpiryMs = () => {
|
|
143
163
|
const n = Number(this.props.accessTokenExpiryMs);
|
|
@@ -167,20 +187,55 @@ class AuthProviderClass extends Component {
|
|
|
167
187
|
);
|
|
168
188
|
}
|
|
169
189
|
};
|
|
170
|
-
resetInactivityTimer = () => {
|
|
190
|
+
resetInactivityTimer = (force = false) => {
|
|
171
191
|
if (this.state.sessionExpiryPendingReason) return;
|
|
192
|
+
const { user } = this.state;
|
|
193
|
+
if (!hasLoggedInSession(user)) {
|
|
194
|
+
this.clearInactivityTimer();
|
|
195
|
+
if (this.state.showInactivityWarningModal) {
|
|
196
|
+
this.setState({ showInactivityWarningModal: false });
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
172
200
|
const now = Date.now();
|
|
173
|
-
if (now - this._lastActivityAt < 1e3) return;
|
|
201
|
+
if (!force && now - this._lastActivityAt < 1e3) return;
|
|
174
202
|
this._lastActivityAt = now;
|
|
175
203
|
this.clearInactivityTimer();
|
|
176
|
-
|
|
177
|
-
|
|
204
|
+
if (this.state.showInactivityWarningModal) {
|
|
205
|
+
this.setState({ showInactivityWarningModal: false });
|
|
206
|
+
}
|
|
178
207
|
const delay = this.getInactivityTimeMs();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.
|
|
183
|
-
|
|
208
|
+
const warningDelay = Math.max(0, delay - this.getInactivityWarningBeforeMs());
|
|
209
|
+
this.inactivityWarningTimerRef = setTimeout(() => {
|
|
210
|
+
this.inactivityWarningTimerRef = null;
|
|
211
|
+
if (!hasLoggedInSession(this.state.user) || this.state.sessionExpiryPendingReason) return;
|
|
212
|
+
this.setState({ showInactivityWarningModal: true });
|
|
213
|
+
}, warningDelay);
|
|
214
|
+
this.inactivityTimerRef = setTimeout(this.handleInactivityTimeout, delay);
|
|
215
|
+
};
|
|
216
|
+
/** Fires when the inactivity deadline is reached — always closes the warning modal first. */
|
|
217
|
+
handleInactivityTimeout = () => {
|
|
218
|
+
this.inactivityTimerRef = null;
|
|
219
|
+
this.clearInactivityWarningTimer();
|
|
220
|
+
if (!hasLoggedInSession(this.state.user)) {
|
|
221
|
+
this.setState({ showInactivityWarningModal: false });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
Auth.logout();
|
|
225
|
+
this.setState({
|
|
226
|
+
sessionExpiryPendingReason: "inactivity",
|
|
227
|
+
showInactivityWarningModal: false
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
/** User chose to stay signed in from the pre-logout warning modal. */
|
|
231
|
+
extendSessionFromInactivityWarning = () => {
|
|
232
|
+
this.setState({ showInactivityWarningModal: false }, () => {
|
|
233
|
+
this.resetInactivityTimer(true);
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
/** Close the pre-logout warning without extending the session (logout still scheduled). */
|
|
237
|
+
dismissInactivityWarningModal = () => {
|
|
238
|
+
this.setState({ showInactivityWarningModal: false });
|
|
184
239
|
};
|
|
185
240
|
loginUser = async (email, password, customToken) => {
|
|
186
241
|
try {
|
|
@@ -353,6 +408,7 @@ class AuthProviderClass extends Component {
|
|
|
353
408
|
avatar: avatarPlaceholder,
|
|
354
409
|
isFetchingUser: false,
|
|
355
410
|
sessionExpiryPendingReason: null,
|
|
411
|
+
showInactivityWarningModal: false,
|
|
356
412
|
loggedOutDueToInactivity: options.dueToInactivity === true,
|
|
357
413
|
loggedOutDueToCookieExpiry: options.dueToCookieExpiry === true
|
|
358
414
|
});
|
|
@@ -848,10 +904,24 @@ class AuthProviderClass extends Component {
|
|
|
848
904
|
clearLoggedOutDueToInactivity: this.clearLoggedOutDueToInactivity,
|
|
849
905
|
clearLoggedOutDueToCookieExpiry: this.clearLoggedOutDueToCookieExpiry,
|
|
850
906
|
dismissSessionExpiryModal: this.dismissSessionExpiryModal,
|
|
907
|
+
extendSessionFromInactivityWarning: this.extendSessionFromInactivityWarning,
|
|
908
|
+
dismissInactivityWarningModal: this.dismissInactivityWarningModal,
|
|
851
909
|
reset2faFlow: this.reset2faFlow
|
|
852
910
|
},
|
|
853
911
|
children: [
|
|
854
912
|
children,
|
|
913
|
+
!this.props.hideInactivityModal && this.state.showInactivityWarningModal && /* @__PURE__ */ jsx(
|
|
914
|
+
SuccessModalComponent,
|
|
915
|
+
{
|
|
916
|
+
isOpen: hasLoggedInSession(this.state.user) && !this.state.sessionExpiryPendingReason,
|
|
917
|
+
onClose: this.dismissInactivityWarningModal,
|
|
918
|
+
variant: "progress",
|
|
919
|
+
title: "Session is about to expire",
|
|
920
|
+
subtitle: "Your session will expire due to inactivity. Click below to stay signed in.",
|
|
921
|
+
content: /* @__PURE__ */ jsx(Text, { children: /* @__PURE__ */ jsx(Button, { onClick: this.extendSessionFromInactivityWarning, children: "To continue, press here" }) }),
|
|
922
|
+
maxWidth: "600px"
|
|
923
|
+
}
|
|
924
|
+
),
|
|
855
925
|
(this.state.loggedOutDueToInactivity || this.state.loggedOutDueToCookieExpiry || this.state.sessionExpiryPendingReason && hasLoggedInSession(this.state.user)) && !this.props.hideInactivityModal && /* @__PURE__ */ jsx(
|
|
856
926
|
SuccessModalComponent,
|
|
857
927
|
{
|
|
@@ -911,6 +981,8 @@ AuthProviderClass.propTypes = {
|
|
|
911
981
|
}),
|
|
912
982
|
/** Override inactivity timeout in ms (e.g. for Storybook). Production uses 5 minutes. */
|
|
913
983
|
inactivityTimeMs: PropTypes.number,
|
|
984
|
+
/** How long before inactivity logout to show the warning modal (ms). Default 30 seconds. */
|
|
985
|
+
inactivityWarningBeforeMs: PropTypes.number,
|
|
914
986
|
/** When true, the inactivity modal is not shown after auto-logout. Default false. */
|
|
915
987
|
hideInactivityModal: PropTypes.bool,
|
|
916
988
|
/** Override access token cookie lifetime in ms (e.g. for Storybook). Production uses 1 hour. */
|