@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 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 modal.
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
- const { user } = this.state;
177
- if (!hasLoggedInSession(user)) return;
204
+ if (this.state.showInactivityWarningModal) {
205
+ this.setState({ showInactivityWarningModal: false });
206
+ }
178
207
  const delay = this.getInactivityTimeMs();
179
- this.inactivityTimerRef = setTimeout(() => {
180
- this.inactivityTimerRef = null;
181
- Auth.logout();
182
- this.setState({ sessionExpiryPendingReason: "inactivity" });
183
- }, delay);
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stokr/components-library",
3
- "version": "3.0.65",
3
+ "version": "3.0.66",
4
4
  "description": "STOKR - Components Library",
5
5
  "author": "Bilal Hodzic <bilal@stokr.io>",
6
6
  "license": "MIT",