diff --git a/src/App.jsx b/src/App.jsx
index 2a72ce1..93e7356 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,4 +1,5 @@
import { PureComponent, lazy, Suspense } from 'react';
+import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import authAutomat from './automats/authAutomat';
@@ -34,7 +35,13 @@ class App extends PureComponent {
}
componentDidMount() {
- this.unsubscribe = authAutomat.subscribe(this, ['status', 'user', 'hasAuth', 'hasCompany']);
+ this.unsubscribe = authAutomat.subscribe(this, [
+ 'status',
+ 'user',
+ 'hasAuth',
+ 'hasCompany',
+ 'retryNotification',
+ ]);
}
componentWillUnmount() {
@@ -42,7 +49,7 @@ class App extends PureComponent {
}
render() {
- const { status, user, hasAuth, hasCompany } = this.state;
+ const { status, user, hasAuth, hasCompany, retryNotification } = this.state;
const isAuthed = Boolean(hasAuth ?? (status === 'authed' && user));
const isCompanySelected = Boolean(hasCompany ?? (isAuthed && user?.selectedCompanyId != null));
@@ -51,12 +58,29 @@ class App extends PureComponent {
+ {retryNotification ? (
+
+ {retryNotification}
+
+ ) : null}
);
diff --git a/src/automats/authAutomat.js b/src/automats/authAutomat.js
index bbef708..8ea8d6c 100644
--- a/src/automats/authAutomat.js
+++ b/src/automats/authAutomat.js
@@ -33,6 +33,7 @@ import { auth, company, number as numberApi } from '../api';
* @property {'loading'|'anon'|'authed'} status Current authentication lifecycle state.
* @property {boolean} hasAuth True when the session has a valid authenticated user.
* @property {boolean} hasCompany True when `hasAuth` is true AND a company is selected.
+ * @property {string|null} retryNotification Message indicating a pending retry (e.g. 502 Bad Gateway).
*/
/** Emitted after an authenticated session has been established. */
@@ -49,11 +50,24 @@ const initialState = {
status: 'loading',
hasAuth: false,
hasCompany: false,
+ retryNotification: null,
};
let emit = () => { };
// #region helpers
+/**
+ * Detects whether an error represents an HTTP 502 Bad Gateway response.
+ *
+ * @param {any} err
+ * @returns {boolean}
+ */
+export function is502BadGateway(err) {
+ if (!err) return false;
+ if (Number(err.status) === 502) return true;
+ const msg = String(err.message || '');
+ return /502|bad\s*gateway/i.test(msg);
+}
/**
* Registers the application-level dispatcher used for authentication events.
*
@@ -108,6 +122,7 @@ function buildAuthState(user, status, extra = {}) {
status,
hasAuth,
hasCompany,
+ retryNotification: null,
...extra,
};
}
@@ -126,52 +141,86 @@ function syncSse(state) {
}
}
}
+
+function applySession(nextSession) {
+ const googleClientId =
+ nextSession.googleClientId ?? authAutomat.state.googleClientId;
+
+ if (!nextSession.user) {
+ if (authAutomat.state.status !== 'authed') {
+ const nextState = authAutomat.setState(
+ buildAuthState(null, 'anon', { googleClientId, retryNotification: null })
+ );
+ syncSse(nextState);
+ } else {
+ authAutomat.setState({ googleClientId, retryNotification: null });
+ }
+ return nextSession;
+ }
+
+ const user = mergeUser(authAutomat.state.user, nextSession.user);
+ const nextState = authAutomat.setState(
+ buildAuthState(user, 'authed', { googleClientId, retryNotification: null })
+ );
+ syncSse(nextState);
+ emit({ type: AUTH_SESSION_ESTABLISHED });
+ return nextSession;
+}
// #endregion
let authAutomat;
+let inFlightBootstrap = null;
const actions = {
// #region bootstrapSession
// MARK: calls auth.me from ../api
/**
* Restores an existing session or requests it from the authentication API.
+ * Retries automatically every 5 seconds if a 502 Bad Gateway is received.
*
* @param {{user?: AuthUser|null, googleClientId?: string|null}|null} [session]
* Optional preloaded session.
* @returns {Promise