diff --git a/package.json b/package.json
index acd564e..618b46a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "global-shell-app",
- "version": "1.9.1",
+ "version": "1.11.0",
"description": "",
"license": "BSD-3-Clause",
"private": true,
@@ -51,9 +51,13 @@
"typescript": "^5.7.3"
},
"dependencies": {
- "@dhis2/app-runtime": "^3.14.0",
+ "@dhis2/app-runtime": "^3.16.0",
"@dhis2/pwa": "^12.3.0",
"@dhis2/ui": "^10.1.13",
+ "@types/js-cookie": "^3.0.6",
+ "@types/post-robot": "^10.0.6",
+ "js-cookie": "^3.0.5",
+ "post-robot": "^10.0.46",
"react-router": "^7.2.0",
"styled-jsx": "^4.0.1"
},
diff --git a/src/App.jsx b/src/App.jsx
index 15a9261..3781236 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -7,6 +7,7 @@ import styles from './App.module.css'
import { ConnectedHeaderBar } from './components/ConnectedHeaderbar.jsx'
import { PluginLoader } from './components/PluginLoader.jsx'
import { RedirectHandler } from './components/RedirectHandler.tsx'
+import { SessionHandler } from './components/session-handler/index.ts'
import { ClientPWAProvider } from './lib/clientPWAUpdateState.jsx'
const APPS_INFO_QUERY = {
@@ -27,8 +28,12 @@ const APPS_INFO_QUERY = {
}
const Layout = ({ appsInfoQuery }) => {
+ const config = useConfig()
+ const sessionTimeout = config?.systemInfo?.sessionTimeout
+
return (
+
{/* Skip the routes in dev; they don't make the same sense */}
{process.env.NODE_ENV !== 'development' ? : null}
diff --git a/src/components/session-handler/index.ts b/src/components/session-handler/index.ts
new file mode 100644
index 0000000..bb37be9
--- /dev/null
+++ b/src/components/session-handler/index.ts
@@ -0,0 +1 @@
+export { SessionHandler } from './session-handler'
diff --git a/src/components/session-handler/modal-countdown.tsx b/src/components/session-handler/modal-countdown.tsx
new file mode 100644
index 0000000..834a110
--- /dev/null
+++ b/src/components/session-handler/modal-countdown.tsx
@@ -0,0 +1,28 @@
+import i18n from '@dhis2/d2-i18n'
+import {
+ Button,
+ ButtonStrip,
+ Modal,
+ ModalActions,
+ ModalContent,
+ ModalTitle,
+} from '@dhis2/ui'
+import * as React from 'react'
+
+export const ExpirationCountdownModal = ({ countDown }) => {
+ return (
+
+ {i18n.t('Your session is about to expire')}
+
+ Your session will expire in {countDown} seconds.
+
+
+
+ {/* can this get into undesired state when offline - i.e. can't extend but want to dismiss */}
+
+ {/* */}
+
+
+
+ )
+}
diff --git a/src/components/session-handler/modal-expired.tsx b/src/components/session-handler/modal-expired.tsx
new file mode 100644
index 0000000..acee019
--- /dev/null
+++ b/src/components/session-handler/modal-expired.tsx
@@ -0,0 +1,47 @@
+import { useConfig } from '@dhis2/app-runtime'
+import i18n from '@dhis2/d2-i18n'
+import {
+ Button,
+ ButtonStrip,
+ Modal,
+ ModalActions,
+ ModalContent,
+ ModalTitle,
+} from '@dhis2/ui'
+import * as React from 'react'
+
+export const ExpiredModal = ({ dismissModal }) => {
+ const { baseUrl } = useConfig()
+
+ const goToLogin = () => {
+ window.open(baseUrl)
+ }
+ const dismiss = () => {
+ dismissModal()
+ }
+ // console.log(config)
+ return (
+
+ {i18n.t('Your session has expired')}
+
+ {/* {i18n.t('Your session has expired.')}{' '} */}
+ {/* {i18n.t(
+ 'You can go to the login page or dismiss the modal if the app supports working offline.'
+ )} */}
+
+ Eos omnis cumque quia quaerat aut. Neque consequuntur sed non a
+ quibusdam eligendi. Fugit eveniet expedita nihil ab maxime sequi
+ nihil quidem. Et aut nobis assumenda in iure.
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/session-handler/session-handler.tsx b/src/components/session-handler/session-handler.tsx
new file mode 100644
index 0000000..cfac844
--- /dev/null
+++ b/src/components/session-handler/session-handler.tsx
@@ -0,0 +1,53 @@
+import postRobot from 'post-robot'
+import * as React from 'react'
+import { ExpirationCountdownModal } from './modal-countdown'
+import { ExpiredModal } from './modal-expired'
+import { useCheckCookie } from './use-check-cookie'
+
+const globalShellBroadcast = new BroadcastChannel('global-shell')
+const broadCastMessage = 'BROADCAST_SESSION_EXPIRED'
+
+// ToDO: there is some re-rendering hell going on with the component - still needs some tightening up
+export const SessionHandler = ({ sessionTimeout }) => {
+ const { showWarning, time, expired, reset } = useCheckCookie({
+ sessionTimeout,
+ warningThreshold: Number(sessionTimeout) / 10,
+ })
+ const [modalHidden, hideModal] = React.useState(false)
+ const [received401, setReceived401] = React.useState(false)
+
+ globalShellBroadcast.addEventListener('message', (ev) => {
+ if (ev.data === broadCastMessage) {
+ console.log(
+ `[Session] Received broadcast message from another window: "${ev.data}" on channel "${globalShellBroadcast.name}"`
+ )
+ hideModal(false)
+ setReceived401(true)
+ }
+ })
+
+ React.useEffect(() => {
+ postRobot.on('notifyShell', (event) => {
+ console.log(
+ ` [Session] [API monitor]: ${event?.data?.resource} (${event?.data.status})`
+ )
+ // todo: check API call are to DHIS2 server specifically?
+ if (event?.data?.status == 401) {
+ hideModal(false)
+ setReceived401(true)
+ // todo: should broadcast 2xx to extend session across windows
+ globalShellBroadcast.postMessage(broadCastMessage)
+ } else {
+ reset()
+ }
+ })
+ }, [])
+
+ if (!expired && showWarning) {
+ return
+ }
+ if ((expired || received401) && !modalHidden) {
+ return hideModal(true)} />
+ }
+ return null
+}
diff --git a/src/components/session-handler/use-check-cookie.ts b/src/components/session-handler/use-check-cookie.ts
new file mode 100644
index 0000000..d3d55b1
--- /dev/null
+++ b/src/components/session-handler/use-check-cookie.ts
@@ -0,0 +1,42 @@
+import { useEffect, useRef, useState } from 'react'
+
+// const WARNING_THRESHOLD_IN_SECONDS = 20
+const CHECK_INTERVAL = 1000 // ms countdown every second
+export const useCheckCookie = ({ sessionTimeout, warningThreshold }) => {
+ const timeout = useRef(sessionTimeout)
+
+ // console.log('??? warningthreshold', warningThresholdSeconds)
+ const stime = useRef(timeout.current)
+ const [time, setTime] = useState(timeout.current)
+
+ const [expired, setExpired] = useState(false)
+ const [warningShown, setShowWarning] = useState(false)
+
+ const reset = () => {
+ stime.current = Math.round(sessionTimeout)
+ setExpired(false)
+ setShowWarning(false)
+ setTime(stime.current)
+ }
+ useEffect(() => {
+ const interval = setInterval(() => {
+ console.log(`[Session] Remaining seconds: ${stime.current}`)
+ setShowWarning(stime.current < warningThreshold)
+ if (stime.current === 1) {
+ setExpired(true)
+ clearInterval(interval)
+ }
+ stime.current = stime.current - 1
+ setTime(stime.current)
+ }, CHECK_INTERVAL)
+
+ return () => clearInterval(interval)
+ }, [])
+
+ return {
+ reset,
+ time,
+ expired,
+ showWarning: warningShown,
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 9afe7ed..ff498c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1845,47 +1845,63 @@
"@dhis2/pwa" "12.3.0"
moment "^2.24.0"
-"@dhis2/app-runtime@^3.12.0", "@dhis2/app-runtime@^3.14.0":
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-3.14.0.tgz#85a6f337036ba31868fd7193360208ef174d4345"
- integrity sha512-0qmP2QxhoK8dWCAOOomD+JDer44AC5rxOHuK5qINNt1K4BMZgG5axmkvk6/86SXTRtLj2+Iio7BLe9DBnh+Bxg==
- dependencies:
- "@dhis2/app-service-alerts" "3.14.0"
- "@dhis2/app-service-config" "3.14.0"
- "@dhis2/app-service-data" "3.14.0"
- "@dhis2/app-service-offline" "3.14.0"
- "@dhis2/app-service-plugin" "3.14.0"
-
-"@dhis2/app-service-alerts@3.14.0":
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-alerts/-/app-service-alerts-3.14.0.tgz#93eab29b3e42115e9de509bec7daa172cc50a0f4"
- integrity sha512-VhXc0w5fX+2Rd4J7MZmklnl7Fq7PLnCrSYDSWRZPYXbNo428QjVIiN/1g003gCatVPon3bl19LisZbn5vmNoRg==
-
-"@dhis2/app-service-config@3.14.0":
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-config/-/app-service-config-3.14.0.tgz#823ce5e96c6deb8beec92bd65180ca611f0551a0"
- integrity sha512-v/gnPMG7IZ6pEutYnHJff4S93w1DAQbeDiGtRqMffEiNxa9Bx5wc5MPTGMyeUGeEw8on3qaeP/2rQa+PGaQvcg==
-
-"@dhis2/app-service-data@3.14.0":
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-data/-/app-service-data-3.14.0.tgz#7109852154af0b02240e077df9ae5e65a727a593"
- integrity sha512-7DQVlVSCdJauduw+9LXrb13IsNagGzPNN2Iy9e2jVeE+eNqP6j0PeI6enHTIjaYA6OOSx/Ob7/HXkBVH8wyLlg==
+"@dhis2/app-runtime@^3.12.0", "@dhis2/app-runtime@^3.16.0":
+ version "3.16.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-3.16.0.tgz#50bf19b1d8f648940d5c5022cc4cd2f9a23cd5b5"
+ integrity sha512-4PbXIpjoclQwVPkZrZwKUUPqN4UWbTE6nuC4h70q2fY6rHEE+QPSBIEDno+rxpQIMy+HtZm0hrDEUK387EMKjA==
+ dependencies:
+ "@dhis2/app-service-alerts" "3.16.0"
+ "@dhis2/app-service-config" "3.16.0"
+ "@dhis2/app-service-data" "3.16.0"
+ "@dhis2/app-service-offline" "3.16.0"
+ "@dhis2/app-service-plugin" "3.16.0"
+ "@dhis2/app-service-user" "3.16.0"
+ prop-types "^15.7.2"
+
+"@dhis2/app-service-alerts@3.16.0":
+ version "3.16.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-alerts/-/app-service-alerts-3.16.0.tgz#373d9ac1aee462cc2f12a9fa7ccfe2d4c986022b"
+ integrity sha512-aTC47gbYebuf0yJfSX7POpmaC+ejG7DCBmAm+qBL3NkBqlsmNI3A0sO9EjztEvkuxnLA/WRuKhu1an+0jsmpTw==
+ dependencies:
+ prop-types "^15.7.2"
+
+"@dhis2/app-service-config@3.16.0":
+ version "3.16.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-config/-/app-service-config-3.16.0.tgz#5b2bd81f638dd4f02e811679d60b56bbd1147d7a"
+ integrity sha512-kU9aavV8GBjRP8NU/ZsmOuQSSs8p5kcX3bTaCvlw7dgRA4eHscdG24JaHYqAfPFQI0ntPT88Qf8m86Obz375cA==
+ dependencies:
+ prop-types "^15.7.2"
+
+"@dhis2/app-service-data@3.16.0":
+ version "3.16.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-data/-/app-service-data-3.16.0.tgz#8f47a9de9004288f0c21c34d22a4cabf93313671"
+ integrity sha512-RFF4wg2m/6dIEl1KmiaX2bw3TPMgB6R9gu27HyOCFNYT808wtFkvyrpa8TAWjF4m+y+yLZyKtHO0yEXp7otfHQ==
dependencies:
"@tanstack/react-query" "^4.36.1"
+ prop-types "^15.7.2"
-"@dhis2/app-service-offline@3.14.0":
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-offline/-/app-service-offline-3.14.0.tgz#9164f4ef1ffef18d52a77f36bfd3683b5ccbdb81"
- integrity sha512-sgp7AtaPIvNt5TeXCrVR2/ndtgx9p+TstQWiBzRq75R894P34nqBX9jWMVUoNDZU35OfcGqtxHfOrpoLrdZ0XA==
+"@dhis2/app-service-offline@3.16.0":
+ version "3.16.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-offline/-/app-service-offline-3.16.0.tgz#73848e8d3e29c7eca0b29b6d2504e65be93463aa"
+ integrity sha512-I67msCV3miwh4La4Dpb0HI2dab5Hs20mNmJGPbaN7WLizxbgVMvyrqtTXBGOT7iZulTgc0fB4zUSAaeE3r7pvA==
dependencies:
lodash "^4.17.21"
+ prop-types "^15.7.2"
-"@dhis2/app-service-plugin@3.14.0":
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-plugin/-/app-service-plugin-3.14.0.tgz#ea6ed4afc805072352e861e5412223b056e2edcf"
- integrity sha512-3D9iPG6/HFT1dY7G6Wwwunv2bQ3hMzW2gsw8rFAcOphFHMujzQYUYuaZQmxg7rs/Hh/DPzLVOzsNAqOi1cS7wA==
+"@dhis2/app-service-plugin@3.16.0":
+ version "3.16.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-plugin/-/app-service-plugin-3.16.0.tgz#eb191e39eb0f183936073b998505be3035fdc29c"
+ integrity sha512-pjjlHLEdLFFr+fuDKjBBjeYM8nyZuBOl1Pi/jCzsAq1HdmxkPI4hIhsCbRkoRWpIs7s9EW0ONI/vf2bkxnct1Q==
dependencies:
post-robot "^10.0.46"
+ prop-types "^15.7.2"
+
+"@dhis2/app-service-user@3.16.0":
+ version "3.16.0"
+ resolved "https://registry.yarnpkg.com/@dhis2/app-service-user/-/app-service-user-3.16.0.tgz#9033187955ad9b2f62793da7134d41c707595a7d"
+ integrity sha512-3bFeiy4NDtwvzo+mF9q70B6GslNKf0lbr9Ujb6PXsp9IqJpahzqFvzMDruTjP/eAdOCNE6o/UL8ZHgSux7aVTg==
+ dependencies:
+ prop-types "^15.7.2"
"@dhis2/app-shell@12.3.0":
version "12.3.0"
@@ -3030,6 +3046,11 @@
dependencies:
"@types/istanbul-lib-report" "*"
+"@types/js-cookie@^3.0.6":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.6.tgz#a04ca19e877687bd449f5ad37d33b104b71fdf95"
+ integrity sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==
+
"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
@@ -3079,6 +3100,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239"
integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==
+"@types/post-robot@^10.0.6":
+ version "10.0.6"
+ resolved "https://registry.yarnpkg.com/@types/post-robot/-/post-robot-10.0.6.tgz#9c8b1db55dd109cc36aa5a0d8edbd46827fd572c"
+ integrity sha512-x+LaZPQ4TYW8aBbKpFZsJn7TGipka7tC2ZokE8gpCAlh4E4XFgxxcvt19LD41BeExmlzAtjHh700r3r8SdfX9A==
+
"@types/prettier@^2.1.5":
version "2.7.3"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f"
@@ -7743,6 +7769,11 @@ jiti@^1.20.0:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
+js-cookie@^3.0.5:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
+ integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"