From fcbe234bf73783b6ba46f5f075d46733c87696eb Mon Sep 17 00:00:00 2001
From: Kai Vandivier <49666798+KaiVandivier@users.noreply.github.com>
Date: Mon, 3 Mar 2025 14:06:14 +0100
Subject: [PATCH] feat: watch for pwa updates in plugin (#5)
* feat: track PWA update status from global shell
* fix: new deps
* chore: add typescript to project
* fix: update to latest @dhis2/pwa changes
* chore: a bit of clean-up
* refactor: remove old "pluginified app" way for PWA updates
* refactor: clean up which update state is used
* chore: edge case loop warning
* chore: comment
* chore: remove unused patch
---
package.json | 11 +-
patches/@dhis2+app-adapter+12.3.0.patch | 591 ------------------
.../@dhis2+app-service-plugin+3.13.2.patch | 97 ---
patches/@dhis2+pwa+12.3.0.patch | 512 ++++++++++++++-
src/App.jsx | 73 +--
...UpdateModal.jsx => ConfirmUpdateModal.tsx} | 18 +-
src/components/ConnectedHeaderbar.jsx | 95 ++-
src/components/PluginLoader.jsx | 38 +-
src/lib/clientPWAUpdateState.jsx | 98 +++
tsconfig.json | 29 +
yarn.lock | 91 +--
11 files changed, 783 insertions(+), 870 deletions(-)
delete mode 100644 patches/@dhis2+app-adapter+12.3.0.patch
delete mode 100644 patches/@dhis2+app-service-plugin+3.13.2.patch
rename src/components/{ConfirmUpdateModal.jsx => ConfirmUpdateModal.tsx} (84%)
create mode 100644 src/lib/clientPWAUpdateState.jsx
create mode 100644 tsconfig.json
diff --git a/package.json b/package.json
index 916be5f..3c5bb40 100644
--- a/package.json
+++ b/package.json
@@ -12,27 +12,32 @@
"postinstall": "patch-package",
"bd": "yarn build && yarn deploy",
"del-adapter": "rm -rf node_modules/@dhis2/app-adapter/build",
+ "del-pwa": "rm -rf node_modules/@dhis2/pwa/build",
"del-headerbar": "rm -rf node_modules/@dhis2-ui/header-bar/build",
"del-offline": "rm -rf node_modules/@dhis2/app-service-offline/build",
"del-plugin": "rm -rf node_modules/@dhis2/app-service-plugin/build",
"del-nm-cache": "rm -rf .d2/shell/node_modules/.cache",
"cp-adapter": "cp -r ../app-platform/adapter/build node_modules/@dhis2/app-adapter/build",
+ "cp-pwa": "cp -r ../app-platform/pwa/build node_modules/@dhis2/pwa/build",
"cp-headerbar": "cp -r ../ui/components/header-bar/build node_modules/@dhis2-ui/header-bar/build",
"cp-offline": "cp -r ../app-runtime/services/offline/build node_modules/@dhis2/app-service-offline/build",
"cp-plugin": "cp -r ../app-runtime/services/plugin/build node_modules/@dhis2/app-service-plugin/build",
"import-adapter": "yarn del-adapter && yarn del-nm-cache && yarn cp-adapter && yarn patch-package @dhis2/app-adapter",
+ "import-pwa": "yarn del-pwa && yarn del-nm-cache && yarn cp-pwa && yarn patch-package @dhis2/pwa",
"import-headerbar": "yarn del-headerbar && yarn del-nm-cache && yarn cp-headerbar && yarn patch-package @dhis2-ui/header-bar",
"import-plugin": "yarn del-plugin && yarn del-nm-cache && yarn cp-plugin && yarn patch-package @dhis2/app-service-plugin",
"import-offline": "yarn del-offline && yarn del-nm-cache && yarn cp-offline"
},
"devDependencies": {
"@dhis2/cli-app-scripts": "^12.3.0",
- "@dhis2/cli-style": "^10.7.5",
+ "@dhis2/cli-style": "^10.7.6",
+ "@types/react": "^19.0.10",
"patch-package": "^8.0.0",
- "postinstall-postinstall": "^2.1.0"
+ "postinstall-postinstall": "^2.1.0",
+ "typescript": "^5.7.3"
},
"dependencies": {
- "@dhis2/app-runtime": "^3.13.2",
+ "@dhis2/app-runtime": "^3.14.0",
"@dhis2/pwa": "^12.3.0",
"@dhis2/ui": "^10.1.13",
"react-router-dom": "^6.22.0"
diff --git a/patches/@dhis2+app-adapter+12.3.0.patch b/patches/@dhis2+app-adapter+12.3.0.patch
deleted file mode 100644
index 51f16e6..0000000
--- a/patches/@dhis2+app-adapter+12.3.0.patch
+++ /dev/null
@@ -1,591 +0,0 @@
-diff --git a/node_modules/@dhis2/app-adapter/build/cjs/components/AppWrapper.js b/node_modules/@dhis2/app-adapter/build/cjs/components/AppWrapper.js
-index 3673f9a..8d830fa 100644
---- a/node_modules/@dhis2/app-adapter/build/cjs/components/AppWrapper.js
-+++ b/node_modules/@dhis2/app-adapter/build/cjs/components/AppWrapper.js
-@@ -13,6 +13,7 @@ var _Alerts = require("./Alerts.js");
- var _ConnectedHeaderBar = require("./ConnectedHeaderBar.js");
- var _ErrorBoundary = require("./ErrorBoundary.js");
- var _LoadingMask = require("./LoadingMask.js");
-+var _PluginPWAUpdateManager = require("./PluginPWAUpdateManager.js");
- var _AppWrapperStyle = require("./styles/AppWrapper.style.js");
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
- const AppWrapper = _ref => {
-@@ -21,7 +22,8 @@ const AppWrapper = _ref => {
- plugin,
- onPluginError,
- clearPluginError,
-- direction: configDirection
-+ direction: configDirection,
-+ reportPWAUpdateStatus
- } = _ref;
- const {
- loading: localeLoading,
-@@ -38,7 +40,9 @@ const AppWrapper = _ref => {
- className: `jsx-${_AppWrapperStyle.styles.__hash}` + " " + "app-shell-adapter"
- }, /*#__PURE__*/_react.default.createElement(_style.default, {
- id: _AppWrapperStyle.styles.__hash
-- }, _AppWrapperStyle.styles), /*#__PURE__*/_react.default.createElement("div", {
-+ }, _AppWrapperStyle.styles), /*#__PURE__*/_react.default.createElement(_PluginPWAUpdateManager.PluginPWAUpdateManager, {
-+ reportPWAUpdateStatus: reportPWAUpdateStatus
-+ }), /*#__PURE__*/_react.default.createElement("div", {
- className: `jsx-${_AppWrapperStyle.styles.__hash}` + " " + "app-shell-app"
- }, /*#__PURE__*/_react.default.createElement(_ErrorBoundary.ErrorBoundary, {
- plugin: true,
-@@ -70,5 +74,6 @@ AppWrapper.propTypes = {
- clearPluginError: _propTypes.default.func,
- direction: _propTypes.default.oneOf(['ltr', 'rtl', 'auto']),
- plugin: _propTypes.default.bool,
-+ reportPWAUpdateStatus: _propTypes.default.func,
- onPluginError: _propTypes.default.func
- };
-\ No newline at end of file
-diff --git a/node_modules/@dhis2/app-adapter/build/cjs/components/ConnectedHeaderBar.js b/node_modules/@dhis2/app-adapter/build/cjs/components/ConnectedHeaderBar.js
-index 19da430..ff8af53 100644
---- a/node_modules/@dhis2/app-adapter/build/cjs/components/ConnectedHeaderBar.js
-+++ b/node_modules/@dhis2/app-adapter/build/cjs/components/ConnectedHeaderBar.js
-@@ -5,9 +5,9 @@ Object.defineProperty(exports, "__esModule", {
- });
- exports.ConnectedHeaderBar = ConnectedHeaderBar;
- var _appRuntime = require("@dhis2/app-runtime");
-+var _pwa = require("@dhis2/pwa");
- var _ui = require("@dhis2/ui");
- var _react = _interopRequireDefault(require("react"));
--var _usePWAUpdateState = require("../utils/usePWAUpdateState");
- var _ConfirmUpdateModal = require("./ConfirmUpdateModal");
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
- /**
-@@ -29,7 +29,7 @@ function ConnectedHeaderBar() {
- clientsCount,
- onConfirmUpdate,
- onCancelUpdate
-- } = (0, _usePWAUpdateState.usePWAUpdateState)();
-+ } = (0, _pwa.usePWAUpdateState)();
- return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement(_ui.HeaderBar, {
- appName: appName,
- updateAvailable: updateAvailable,
-diff --git a/node_modules/@dhis2/app-adapter/build/cjs/components/OfflineInterfaceContext.js b/node_modules/@dhis2/app-adapter/build/cjs/components/OfflineInterfaceContext.js
-deleted file mode 100644
-index 39751fa..0000000
---- a/node_modules/@dhis2/app-adapter/build/cjs/components/OfflineInterfaceContext.js
-+++ /dev/null
-@@ -1,28 +0,0 @@
--"use strict";
--
--Object.defineProperty(exports, "__esModule", {
-- value: true
--});
--exports.useOfflineInterface = exports.OfflineInterfaceProvider = void 0;
--var _pwa = require("@dhis2/pwa");
--var _propTypes = _interopRequireDefault(require("prop-types"));
--var _react = _interopRequireWildcard(require("react"));
--function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
--function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
--function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
--const theOfflineInterface = new _pwa.OfflineInterface();
--const OfflineInterfaceContext = /*#__PURE__*/(0, _react.createContext)(theOfflineInterface);
--const OfflineInterfaceProvider = _ref => {
-- let {
-- children
-- } = _ref;
-- return /*#__PURE__*/_react.default.createElement(OfflineInterfaceContext.Provider, {
-- value: theOfflineInterface
-- }, children);
--};
--exports.OfflineInterfaceProvider = OfflineInterfaceProvider;
--OfflineInterfaceProvider.propTypes = {
-- children: _propTypes.default.node
--};
--const useOfflineInterface = () => (0, _react.useContext)(OfflineInterfaceContext);
--exports.useOfflineInterface = useOfflineInterface;
-\ No newline at end of file
-diff --git a/node_modules/@dhis2/app-adapter/build/cjs/components/PWALoadingBoundary.js b/node_modules/@dhis2/app-adapter/build/cjs/components/PWALoadingBoundary.js
-index 4837c52..720df23 100644
---- a/node_modules/@dhis2/app-adapter/build/cjs/components/PWALoadingBoundary.js
-+++ b/node_modules/@dhis2/app-adapter/build/cjs/components/PWALoadingBoundary.js
-@@ -7,14 +7,13 @@ exports.PWALoadingBoundary = void 0;
- var _pwa = require("@dhis2/pwa");
- var _propTypes = _interopRequireDefault(require("prop-types"));
- var _react = require("react");
--var _OfflineInterfaceContext = require("./OfflineInterfaceContext");
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
- const PWALoadingBoundary = _ref => {
- let {
- children
- } = _ref;
- const [pwaReady, setPWAReady] = (0, _react.useState)(false);
-- const offlineInterface = (0, _OfflineInterfaceContext.useOfflineInterface)();
-+ const offlineInterface = (0, _pwa.useOfflineInterface)();
- (0, _react.useEffect)(() => {
- const checkRegistration = async () => {
- const registrationState = await offlineInterface.getRegistrationState();
-diff --git a/node_modules/@dhis2/app-adapter/build/cjs/components/PluginPWAUpdateManager.js b/node_modules/@dhis2/app-adapter/build/cjs/components/PluginPWAUpdateManager.js
-new file mode 100644
-index 0000000..69178fb
---- /dev/null
-+++ b/node_modules/@dhis2/app-adapter/build/cjs/components/PluginPWAUpdateManager.js
-@@ -0,0 +1,55 @@
-+"use strict";
-+
-+Object.defineProperty(exports, "__esModule", {
-+ value: true
-+});
-+exports.PluginPWAUpdateManager = PluginPWAUpdateManager;
-+var _pwa = require("@dhis2/pwa");
-+var _propTypes = _interopRequireDefault(require("prop-types"));
-+var _react = _interopRequireWildcard(require("react"));
-+var _ConfirmUpdateModal = require("./ConfirmUpdateModal");
-+function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
-+function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
-+function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
-+/**
-+ * Analogous to the ConnectedHeaderbar, for use in plugins since they don't
-+ * use a header bar. See the ConnectedHeaderBar for more
-+ */
-+
-+/**
-+ * Check for SW updates or a first activation, sending a message to the host
-+ * app if an update is ready. When an update is applied, if there are
-+ * multiple tabs of this app open, there's anadditional warning step because all
-+ * clients of the service worker will reload when there's an update, which may
-+ * cause data loss.
-+ */
-+
-+function PluginPWAUpdateManager(_ref) {
-+ let {
-+ reportPWAUpdateStatus
-+ } = _ref;
-+ const {
-+ updateAvailable,
-+ confirmReload,
-+ confirmationRequired,
-+ clientsCount,
-+ onConfirmUpdate,
-+ onCancelUpdate
-+ } = (0, _pwa.usePWAUpdateState)();
-+ (0, _react.useEffect)(() => {
-+ if (reportPWAUpdateStatus) {
-+ reportPWAUpdateStatus({
-+ updateAvailable,
-+ onApplyUpdate: updateAvailable ? confirmReload : null
-+ });
-+ }
-+ }, [updateAvailable, confirmReload, reportPWAUpdateStatus]);
-+ return confirmationRequired ? /*#__PURE__*/_react.default.createElement(_ConfirmUpdateModal.ConfirmUpdateModal, {
-+ clientsCount: clientsCount,
-+ onConfirm: onConfirmUpdate,
-+ onCancel: onCancelUpdate
-+ }) : null;
-+}
-+PluginPWAUpdateManager.propTypes = {
-+ reportPWAUpdateStatus: _propTypes.default.func
-+};
-\ No newline at end of file
-diff --git a/node_modules/@dhis2/app-adapter/build/cjs/components/ServerVersionProvider.js b/node_modules/@dhis2/app-adapter/build/cjs/components/ServerVersionProvider.js
-index 1003086..6678085 100644
---- a/node_modules/@dhis2/app-adapter/build/cjs/components/ServerVersionProvider.js
-+++ b/node_modules/@dhis2/app-adapter/build/cjs/components/ServerVersionProvider.js
-@@ -12,7 +12,6 @@ var _api = require("../utils/api.js");
- var _parseVersion = require("../utils/parseVersion.js");
- var _LoadingMask = require("./LoadingMask.js");
- var _LoginModal = require("./LoginModal.js");
--var _OfflineInterfaceContext = require("./OfflineInterfaceContext.js");
- function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
- function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
-@@ -21,7 +20,7 @@ const ServerVersionProvider = _ref => {
- appName,
- appVersion,
- url,
-- // url from env vars
-+ // url from env vars or index.html interpolation
- apiVersion,
- pwaEnabled,
- plugin,
-@@ -30,7 +29,7 @@ const ServerVersionProvider = _ref => {
- loginApp,
- children
- } = _ref;
-- const offlineInterface = (0, _OfflineInterfaceContext.useOfflineInterface)();
-+ const offlineInterface = (0, _pwa.useOfflineInterface)();
- const [systemInfoState, setSystemInfoState] = (0, _react.useState)({
- loading: true,
- error: undefined,
-diff --git a/node_modules/@dhis2/app-adapter/build/cjs/index.js b/node_modules/@dhis2/app-adapter/build/cjs/index.js
-index 173de2b..55421ae 100644
---- a/node_modules/@dhis2/app-adapter/build/cjs/index.js
-+++ b/node_modules/@dhis2/app-adapter/build/cjs/index.js
-@@ -10,7 +10,6 @@ var _react = _interopRequireDefault(require("react"));
- var _AppWrapper = require("./components/AppWrapper.js");
- var _ErrorBoundary = require("./components/ErrorBoundary.js");
- var _LoginAppWrapper = require("./components/LoginAppWrapper.js");
--var _OfflineInterfaceContext = require("./components/OfflineInterfaceContext.js");
- var _PWALoadingBoundary = require("./components/PWALoadingBoundary.js");
- var _ServerVersionProvider = require("./components/ServerVersionProvider.js");
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
-@@ -27,6 +26,7 @@ const AppAdapter = _ref => {
- showAlertsInPlugin,
- onPluginError,
- clearPluginError,
-+ reportPWAUpdateStatus,
- loginApp,
- children
- } = _ref;
-@@ -53,7 +53,7 @@ const AppAdapter = _ref => {
- plugin: plugin,
- fullscreen: true,
- onRetry: _pwa.checkForSWUpdateAndReload
-- }, /*#__PURE__*/_react.default.createElement(_OfflineInterfaceContext.OfflineInterfaceProvider, null, /*#__PURE__*/_react.default.createElement(_PWALoadingBoundary.PWALoadingBoundary, null, /*#__PURE__*/_react.default.createElement(_ServerVersionProvider.ServerVersionProvider, {
-+ }, /*#__PURE__*/_react.default.createElement(_pwa.OfflineInterfaceProvider, null, /*#__PURE__*/_react.default.createElement(_PWALoadingBoundary.PWALoadingBoundary, null, /*#__PURE__*/_react.default.createElement(_ServerVersionProvider.ServerVersionProvider, {
- appName: appName,
- appVersion: appVersion,
- url: url,
-@@ -66,7 +66,8 @@ const AppAdapter = _ref => {
- plugin: plugin,
- onPluginError: onPluginError,
- clearPluginError: clearPluginError,
-- direction: direction
-+ direction: direction,
-+ reportPWAUpdateStatus: reportPWAUpdateStatus
- }, children)))));
- };
- AppAdapter.propTypes = {
-@@ -80,6 +81,7 @@ AppAdapter.propTypes = {
- parentAlertsAdd: _propTypes.default.func,
- plugin: _propTypes.default.bool,
- pwaEnabled: _propTypes.default.bool,
-+ reportPWAUpdateStatus: _propTypes.default.func,
- showAlertsInPlugin: _propTypes.default.bool,
- url: _propTypes.default.string,
- onPluginError: _propTypes.default.func
-diff --git a/node_modules/@dhis2/app-adapter/build/cjs/utils/usePWAUpdateState.js b/node_modules/@dhis2/app-adapter/build/cjs/utils/usePWAUpdateState.js
-deleted file mode 100644
-index 3b757f9..0000000
---- a/node_modules/@dhis2/app-adapter/build/cjs/utils/usePWAUpdateState.js
-+++ /dev/null
-@@ -1,56 +0,0 @@
--"use strict";
--
--Object.defineProperty(exports, "__esModule", {
-- value: true
--});
--exports.usePWAUpdateState = void 0;
--var _react = require("react");
--var _OfflineInterfaceContext = require("../components/OfflineInterfaceContext");
--const usePWAUpdateState = () => {
-- const offlineInterface = (0, _OfflineInterfaceContext.useOfflineInterface)();
-- const [updateAvailable, setUpdateAvailable] = (0, _react.useState)(false);
-- const [clientsCount, setClientsCount] = (0, _react.useState)(null);
-- const onConfirmUpdate = () => {
-- offlineInterface.useNewSW();
-- };
-- const onCancelUpdate = () => {
-- setClientsCount(null);
-- };
-- const confirmReload = () => {
-- offlineInterface.getClientsInfo().then(_ref => {
-- let {
-- clientsCount
-- } = _ref;
-- if (clientsCount === 1) {
-- // Just one client; go ahead and reload
-- onConfirmUpdate();
-- } else {
-- // Multiple clients; warn about data loss before reloading
-- setClientsCount(clientsCount);
-- }
-- }).catch(reason => {
-- // Didn't get clients info
-- console.warn(reason);
--
-- // Go ahead with confirmation modal with `0` as clientsCount
-- setClientsCount(0);
-- });
-- };
-- (0, _react.useEffect)(() => {
-- offlineInterface.checkForNewSW({
-- onNewSW: () => {
-- setUpdateAvailable(true);
-- }
-- });
-- }, [offlineInterface]);
-- const confirmationRequired = clientsCount !== null;
-- return {
-- updateAvailable,
-- confirmReload,
-- confirmationRequired,
-- clientsCount,
-- onConfirmUpdate,
-- onCancelUpdate
-- };
--};
--exports.usePWAUpdateState = usePWAUpdateState;
-\ No newline at end of file
-diff --git a/node_modules/@dhis2/app-adapter/build/es/components/AppWrapper.js b/node_modules/@dhis2/app-adapter/build/es/components/AppWrapper.js
-index 292218e..e4f34e5 100644
---- a/node_modules/@dhis2/app-adapter/build/es/components/AppWrapper.js
-+++ b/node_modules/@dhis2/app-adapter/build/es/components/AppWrapper.js
-@@ -7,6 +7,7 @@ import { Alerts } from './Alerts.js';
- import { ConnectedHeaderBar } from './ConnectedHeaderBar.js';
- import { ErrorBoundary } from './ErrorBoundary.js';
- import { LoadingMask } from './LoadingMask.js';
-+import { PluginPWAUpdateManager } from './PluginPWAUpdateManager.js';
- import { styles } from './styles/AppWrapper.style.js';
- const AppWrapper = _ref => {
- let {
-@@ -14,7 +15,8 @@ const AppWrapper = _ref => {
- plugin,
- onPluginError,
- clearPluginError,
-- direction: configDirection
-+ direction: configDirection,
-+ reportPWAUpdateStatus
- } = _ref;
- const {
- loading: localeLoading,
-@@ -31,7 +33,9 @@ const AppWrapper = _ref => {
- className: `jsx-${styles.__hash}` + " " + "app-shell-adapter"
- }, /*#__PURE__*/React.createElement(_JSXStyle, {
- id: styles.__hash
-- }, styles), /*#__PURE__*/React.createElement("div", {
-+ }, styles), /*#__PURE__*/React.createElement(PluginPWAUpdateManager, {
-+ reportPWAUpdateStatus: reportPWAUpdateStatus
-+ }), /*#__PURE__*/React.createElement("div", {
- className: `jsx-${styles.__hash}` + " " + "app-shell-app"
- }, /*#__PURE__*/React.createElement(ErrorBoundary, {
- plugin: true,
-@@ -62,6 +66,7 @@ AppWrapper.propTypes = {
- clearPluginError: PropTypes.func,
- direction: PropTypes.oneOf(['ltr', 'rtl', 'auto']),
- plugin: PropTypes.bool,
-+ reportPWAUpdateStatus: PropTypes.func,
- onPluginError: PropTypes.func
- };
- export { AppWrapper };
-\ No newline at end of file
-diff --git a/node_modules/@dhis2/app-adapter/build/es/components/ConnectedHeaderBar.js b/node_modules/@dhis2/app-adapter/build/es/components/ConnectedHeaderBar.js
-index 950f05c..809a074 100644
---- a/node_modules/@dhis2/app-adapter/build/es/components/ConnectedHeaderBar.js
-+++ b/node_modules/@dhis2/app-adapter/build/es/components/ConnectedHeaderBar.js
-@@ -1,7 +1,7 @@
- import { useConfig } from '@dhis2/app-runtime';
-+import { usePWAUpdateState } from '@dhis2/pwa';
- import { HeaderBar } from '@dhis2/ui';
- import React from 'react';
--import { usePWAUpdateState } from '../utils/usePWAUpdateState';
- import { ConfirmUpdateModal } from './ConfirmUpdateModal';
-
- /**
-diff --git a/node_modules/@dhis2/app-adapter/build/es/components/OfflineInterfaceContext.js b/node_modules/@dhis2/app-adapter/build/es/components/OfflineInterfaceContext.js
-deleted file mode 100644
-index d2c52f3..0000000
---- a/node_modules/@dhis2/app-adapter/build/es/components/OfflineInterfaceContext.js
-+++ /dev/null
-@@ -1,17 +0,0 @@
--import { OfflineInterface } from '@dhis2/pwa';
--import PropTypes from 'prop-types';
--import React, { createContext, useContext } from 'react';
--const theOfflineInterface = new OfflineInterface();
--const OfflineInterfaceContext = /*#__PURE__*/createContext(theOfflineInterface);
--export const OfflineInterfaceProvider = _ref => {
-- let {
-- children
-- } = _ref;
-- return /*#__PURE__*/React.createElement(OfflineInterfaceContext.Provider, {
-- value: theOfflineInterface
-- }, children);
--};
--OfflineInterfaceProvider.propTypes = {
-- children: PropTypes.node
--};
--export const useOfflineInterface = () => useContext(OfflineInterfaceContext);
-\ No newline at end of file
-diff --git a/node_modules/@dhis2/app-adapter/build/es/components/PWALoadingBoundary.js b/node_modules/@dhis2/app-adapter/build/es/components/PWALoadingBoundary.js
-index d0e225c..db3f72e 100644
---- a/node_modules/@dhis2/app-adapter/build/es/components/PWALoadingBoundary.js
-+++ b/node_modules/@dhis2/app-adapter/build/es/components/PWALoadingBoundary.js
-@@ -1,7 +1,6 @@
--import { REGISTRATION_STATE_WAITING, REGISTRATION_STATE_FIRST_ACTIVATION } from '@dhis2/pwa';
-+import { useOfflineInterface, REGISTRATION_STATE_WAITING, REGISTRATION_STATE_FIRST_ACTIVATION } from '@dhis2/pwa';
- import PropTypes from 'prop-types';
- import { useEffect, useState } from 'react';
--import { useOfflineInterface } from './OfflineInterfaceContext';
- export const PWALoadingBoundary = _ref => {
- let {
- children
-diff --git a/node_modules/@dhis2/app-adapter/build/es/components/PluginPWAUpdateManager.js b/node_modules/@dhis2/app-adapter/build/es/components/PluginPWAUpdateManager.js
-new file mode 100644
-index 0000000..cb9bdd7
---- /dev/null
-+++ b/node_modules/@dhis2/app-adapter/build/es/components/PluginPWAUpdateManager.js
-@@ -0,0 +1,47 @@
-+import { usePWAUpdateState } from '@dhis2/pwa';
-+import PropTypes from 'prop-types';
-+import React, { useEffect } from 'react';
-+import { ConfirmUpdateModal } from './ConfirmUpdateModal';
-+
-+/**
-+ * Analogous to the ConnectedHeaderbar, for use in plugins since they don't
-+ * use a header bar. See the ConnectedHeaderBar for more
-+ */
-+
-+/**
-+ * Check for SW updates or a first activation, sending a message to the host
-+ * app if an update is ready. When an update is applied, if there are
-+ * multiple tabs of this app open, there's anadditional warning step because all
-+ * clients of the service worker will reload when there's an update, which may
-+ * cause data loss.
-+ */
-+
-+export function PluginPWAUpdateManager(_ref) {
-+ let {
-+ reportPWAUpdateStatus
-+ } = _ref;
-+ const {
-+ updateAvailable,
-+ confirmReload,
-+ confirmationRequired,
-+ clientsCount,
-+ onConfirmUpdate,
-+ onCancelUpdate
-+ } = usePWAUpdateState();
-+ useEffect(() => {
-+ if (reportPWAUpdateStatus) {
-+ reportPWAUpdateStatus({
-+ updateAvailable,
-+ onApplyUpdate: updateAvailable ? confirmReload : null
-+ });
-+ }
-+ }, [updateAvailable, confirmReload, reportPWAUpdateStatus]);
-+ return confirmationRequired ? /*#__PURE__*/React.createElement(ConfirmUpdateModal, {
-+ clientsCount: clientsCount,
-+ onConfirm: onConfirmUpdate,
-+ onCancel: onCancelUpdate
-+ }) : null;
-+}
-+PluginPWAUpdateManager.propTypes = {
-+ reportPWAUpdateStatus: PropTypes.func
-+};
-\ No newline at end of file
-diff --git a/node_modules/@dhis2/app-adapter/build/es/components/ServerVersionProvider.js b/node_modules/@dhis2/app-adapter/build/es/components/ServerVersionProvider.js
-index 6d0e69b..e437669 100644
---- a/node_modules/@dhis2/app-adapter/build/es/components/ServerVersionProvider.js
-+++ b/node_modules/@dhis2/app-adapter/build/es/components/ServerVersionProvider.js
-@@ -1,18 +1,17 @@
- import { Provider } from '@dhis2/app-runtime';
--import { getBaseUrlByAppName, setBaseUrlByAppName } from '@dhis2/pwa';
-+import { getBaseUrlByAppName, setBaseUrlByAppName, useOfflineInterface } from '@dhis2/pwa';
- import PropTypes from 'prop-types';
- import React, { useEffect, useState } from 'react';
- import { get } from '../utils/api.js';
- import { parseDHIS2ServerVersion, parseVersion } from '../utils/parseVersion.js';
- import { LoadingMask } from './LoadingMask.js';
- import { LoginModal } from './LoginModal.js';
--import { useOfflineInterface } from './OfflineInterfaceContext.js';
- export const ServerVersionProvider = _ref => {
- let {
- appName,
- appVersion,
- url,
-- // url from env vars
-+ // url from env vars or index.html interpolation
- apiVersion,
- pwaEnabled,
- plugin,
-diff --git a/node_modules/@dhis2/app-adapter/build/es/index.js b/node_modules/@dhis2/app-adapter/build/es/index.js
-index f440aee..a3f9d52 100644
---- a/node_modules/@dhis2/app-adapter/build/es/index.js
-+++ b/node_modules/@dhis2/app-adapter/build/es/index.js
-@@ -1,10 +1,9 @@
--import { checkForSWUpdateAndReload } from '@dhis2/pwa';
-+import { checkForSWUpdateAndReload, OfflineInterfaceProvider } from '@dhis2/pwa';
- import PropTypes from 'prop-types';
- import React from 'react';
- import { AppWrapper } from './components/AppWrapper.js';
- import { ErrorBoundary } from './components/ErrorBoundary.js';
- import { LoginAppWrapper } from './components/LoginAppWrapper.js';
--import { OfflineInterfaceProvider } from './components/OfflineInterfaceContext.js';
- import { PWALoadingBoundary } from './components/PWALoadingBoundary.js';
- import { ServerVersionProvider } from './components/ServerVersionProvider.js';
- const AppAdapter = _ref => {
-@@ -20,6 +19,7 @@ const AppAdapter = _ref => {
- showAlertsInPlugin,
- onPluginError,
- clearPluginError,
-+ reportPWAUpdateStatus,
- loginApp,
- children
- } = _ref;
-@@ -59,7 +59,8 @@ const AppAdapter = _ref => {
- plugin: plugin,
- onPluginError: onPluginError,
- clearPluginError: clearPluginError,
-- direction: direction
-+ direction: direction,
-+ reportPWAUpdateStatus: reportPWAUpdateStatus
- }, children)))));
- };
- AppAdapter.propTypes = {
-@@ -73,6 +74,7 @@ AppAdapter.propTypes = {
- parentAlertsAdd: PropTypes.func,
- plugin: PropTypes.bool,
- pwaEnabled: PropTypes.bool,
-+ reportPWAUpdateStatus: PropTypes.func,
- showAlertsInPlugin: PropTypes.bool,
- url: PropTypes.string,
- onPluginError: PropTypes.func
-diff --git a/node_modules/@dhis2/app-adapter/build/es/utils/usePWAUpdateState.js b/node_modules/@dhis2/app-adapter/build/es/utils/usePWAUpdateState.js
-deleted file mode 100644
-index 4c5c55a..0000000
---- a/node_modules/@dhis2/app-adapter/build/es/utils/usePWAUpdateState.js
-+++ /dev/null
-@@ -1,49 +0,0 @@
--import { useEffect, useState } from 'react';
--import { useOfflineInterface } from '../components/OfflineInterfaceContext';
--export const usePWAUpdateState = () => {
-- const offlineInterface = useOfflineInterface();
-- const [updateAvailable, setUpdateAvailable] = useState(false);
-- const [clientsCount, setClientsCount] = useState(null);
-- const onConfirmUpdate = () => {
-- offlineInterface.useNewSW();
-- };
-- const onCancelUpdate = () => {
-- setClientsCount(null);
-- };
-- const confirmReload = () => {
-- offlineInterface.getClientsInfo().then(_ref => {
-- let {
-- clientsCount
-- } = _ref;
-- if (clientsCount === 1) {
-- // Just one client; go ahead and reload
-- onConfirmUpdate();
-- } else {
-- // Multiple clients; warn about data loss before reloading
-- setClientsCount(clientsCount);
-- }
-- }).catch(reason => {
-- // Didn't get clients info
-- console.warn(reason);
--
-- // Go ahead with confirmation modal with `0` as clientsCount
-- setClientsCount(0);
-- });
-- };
-- useEffect(() => {
-- offlineInterface.checkForNewSW({
-- onNewSW: () => {
-- setUpdateAvailable(true);
-- }
-- });
-- }, [offlineInterface]);
-- const confirmationRequired = clientsCount !== null;
-- return {
-- updateAvailable,
-- confirmReload,
-- confirmationRequired,
-- clientsCount,
-- onConfirmUpdate,
-- onCancelUpdate
-- };
--};
-\ No newline at end of file
diff --git a/patches/@dhis2+app-service-plugin+3.13.2.patch b/patches/@dhis2+app-service-plugin+3.13.2.patch
deleted file mode 100644
index dbeadd3..0000000
--- a/patches/@dhis2+app-service-plugin+3.13.2.patch
+++ /dev/null
@@ -1,97 +0,0 @@
-diff --git a/node_modules/@dhis2/app-service-plugin/build/cjs/Plugin.js b/node_modules/@dhis2/app-service-plugin/build/cjs/Plugin.js
-index 49f6732..21a8e23 100644
---- a/node_modules/@dhis2/app-service-plugin/build/cjs/Plugin.js
-+++ b/node_modules/@dhis2/app-service-plugin/build/cjs/Plugin.js
-@@ -36,6 +36,7 @@ const Plugin = _ref3 => {
- let {
- pluginSource,
- pluginShortName,
-+ onLoad,
- height,
- width,
- className,
-@@ -121,6 +122,7 @@ const Plugin = _ref3 => {
- {
- window: iframeRef.current.contentWindow
- }, () => {
-+ console.log('sending updated props');
- communicationReceivedRef.current = true;
- return iframeProps;
- });
-@@ -129,6 +131,7 @@ const Plugin = _ref3 => {
-
- // If iframe has sent initial request, send new props
- if (communicationReceivedRef.current && iframeRef.current.contentWindow && !inErrorState) {
-+ console.log('sending updated props');
- _postRobot.default.send(iframeRef.current.contentWindow, 'updated', iframeProps).catch(err => {
- // log postRobot errors, but do not bubble them up
- console.error(err);
-@@ -159,7 +162,8 @@ const Plugin = _ref3 => {
- height: height !== null && height !== void 0 ? height : resizedHeight,
- style: {
- border: 'none'
-- }
-+ },
-+ onLoad: onLoad
- });
- };
- exports.Plugin = Plugin;
-\ No newline at end of file
-diff --git a/node_modules/@dhis2/app-service-plugin/build/es/Plugin.js b/node_modules/@dhis2/app-service-plugin/build/es/Plugin.js
-index 7adc7cf..7f10539 100644
---- a/node_modules/@dhis2/app-service-plugin/build/es/Plugin.js
-+++ b/node_modules/@dhis2/app-service-plugin/build/es/Plugin.js
-@@ -27,6 +27,7 @@ export const Plugin = _ref3 => {
- let {
- pluginSource,
- pluginShortName,
-+ onLoad,
- height,
- width,
- className,
-@@ -112,6 +113,7 @@ export const Plugin = _ref3 => {
- {
- window: iframeRef.current.contentWindow
- }, () => {
-+ console.log('sending updated props');
- communicationReceivedRef.current = true;
- return iframeProps;
- });
-@@ -120,6 +122,7 @@ export const Plugin = _ref3 => {
-
- // If iframe has sent initial request, send new props
- if (communicationReceivedRef.current && iframeRef.current.contentWindow && !inErrorState) {
-+ console.log('sending updated props');
- postRobot.send(iframeRef.current.contentWindow, 'updated', iframeProps).catch(err => {
- // log postRobot errors, but do not bubble them up
- console.error(err);
-@@ -150,6 +153,7 @@ export const Plugin = _ref3 => {
- height: height !== null && height !== void 0 ? height : resizedHeight,
- style: {
- border: 'none'
-- }
-+ },
-+ onLoad: onLoad
- });
- };
-\ No newline at end of file
-diff --git a/node_modules/@dhis2/app-service-plugin/build/types/Plugin.d.ts b/node_modules/@dhis2/app-service-plugin/build/types/Plugin.d.ts
-index 23b9a60..75cbab3 100644
---- a/node_modules/@dhis2/app-service-plugin/build/types/Plugin.d.ts
-+++ b/node_modules/@dhis2/app-service-plugin/build/types/Plugin.d.ts
-@@ -1,4 +1,4 @@
--///
-+import { ReactEventHandler } from 'react';
- type PluginProps = {
- /** URL to provide to iframe `src` */
- pluginSource?: string;
-@@ -41,6 +41,8 @@ type PluginProps = {
- clientWidth?: string | number;
- /** Props that will be sent to the plugin */
- propsToPassNonMemoized?: any;
-+ /** Event callback that will be called during the iframe's Load event */
-+ onLoad?: ReactEventHandler;
- };
--export declare const Plugin: ({ pluginSource, pluginShortName, height, width, className, clientWidth, ...propsToPassNonMemoized }: PluginProps) => JSX.Element;
-+export declare const Plugin: ({ pluginSource, pluginShortName, onLoad, height, width, className, clientWidth, ...propsToPassNonMemoized }: PluginProps) => JSX.Element;
- export {};
diff --git a/patches/@dhis2+pwa+12.3.0.patch b/patches/@dhis2+pwa+12.3.0.patch
index 96836fa..88bd3de 100644
--- a/patches/@dhis2+pwa+12.3.0.patch
+++ b/patches/@dhis2+pwa+12.3.0.patch
@@ -1,8 +1,8 @@
diff --git a/node_modules/@dhis2/pwa/build/cjs/index.js b/node_modules/@dhis2/pwa/build/cjs/index.js
-index 571bd0e..1ca95ba 100644
+index 571bd0e..163b12e 100644
--- a/node_modules/@dhis2/pwa/build/cjs/index.js
+++ b/node_modules/@dhis2/pwa/build/cjs/index.js
-@@ -9,6 +9,12 @@ Object.defineProperty(exports, "OfflineInterface", {
+@@ -9,6 +9,18 @@ Object.defineProperty(exports, "OfflineInterface", {
return _offlineInterface.OfflineInterface;
}
});
@@ -11,11 +11,17 @@ index 571bd0e..1ca95ba 100644
+ get: function () {
+ return _OfflineInterfaceContext.OfflineInterfaceProvider;
+ }
++});
++Object.defineProperty(exports, "PWAUpdateOfflineInterface", {
++ enumerable: true,
++ get: function () {
++ return _pwaUpdateOfflineInterface.PWAUpdateOfflineInterface;
++ }
+});
Object.defineProperty(exports, "REGISTRATION_STATE_ACTIVE", {
enumerable: true,
get: function () {
-@@ -69,6 +75,18 @@ Object.defineProperty(exports, "setUpServiceWorker", {
+@@ -69,7 +81,22 @@ Object.defineProperty(exports, "setUpServiceWorker", {
return _setUpServiceWorker.setUpServiceWorker;
}
});
@@ -33,7 +39,234 @@ index 571bd0e..1ca95ba 100644
+});
var _setUpServiceWorker = require("./service-worker/set-up-service-worker.js");
var _offlineInterface = require("./offline-interface/offline-interface.js");
++var _pwaUpdateOfflineInterface = require("./offline-interface/pwa-update-offline-interface.js");
var _registration = require("./lib/registration.js");
+ var _baseUrlDb = require("./lib/base-url-db.js");
++var _OfflineInterfaceContext = require("./react/OfflineInterfaceContext.js");
++var _usePWAUpdateState = require("./react/usePWAUpdateState.js");
+diff --git a/node_modules/@dhis2/pwa/build/cjs/lib/registration.js b/node_modules/@dhis2/pwa/build/cjs/lib/registration.js
+index 1900021..7e3574b 100644
+--- a/node_modules/@dhis2/pwa/build/cjs/lib/registration.js
++++ b/node_modules/@dhis2/pwa/build/cjs/lib/registration.js
+@@ -36,16 +36,32 @@ async function getRegistrationState() {
+ return REGISTRATION_STATE_ACTIVE;
+ }
+ }
++
++/**
++ * Can receive a specific SW instance to check for updates on, e.g. for a
++ * plugin window. Defaults to this window's navigator.serviceWorker.
++ * onUpdate is called with `{ registration }`
++ */
+ async function checkForUpdates(_ref) {
+ let {
+- onUpdate
++ onUpdate,
++ targetServiceWorker
+ } = _ref;
+- if (!('serviceWorker' in navigator)) {
++ if (!('serviceWorker' in navigator) && !targetServiceWorker) {
+ return;
+ }
+- const registration = await navigator.serviceWorker.getRegistration();
++ const serviceWorker = targetServiceWorker || navigator.serviceWorker;
++ let registration = await serviceWorker.getRegistration();
+ if (registration === undefined) {
+- return;
++ // This could have raced before the call to `serviceWorker.register()`;
++ // wait and try again. Testing with a 20x CPU throttling in Chrome,
++ // 500 ms works on an M3 max macbook pro
++ await new Promise(r => setTimeout(r, 500));
++ registration = await serviceWorker.getRegistration();
++ if (registration === undefined) {
++ // Still didn't find it; probably not a PWA app.
++ return;
++ }
+ }
+ function handleWaitingSW() {
+ console.log('New content is available and will be used when all tabs for this page are closed.');
+@@ -76,21 +92,21 @@ async function checkForUpdates(_ref) {
+ // callback doesn't get called in that case. Handle that here:
+ if (registration.waiting) {
+ handleWaitingSW();
+- } else if (registration.active && navigator.serviceWorker.controller === null) {
++ } else if (registration.active && serviceWorker.controller === null) {
+ handleFirstSWActivation();
+ }
+ function handleInstallingWorker() {
+ const installingWorker = registration.installing;
+ if (installingWorker) {
+- installingWorker.onstatechange = () => {
+- if (installingWorker.state === 'installed' && navigator.serviceWorker.controller) {
++ installingWorker.addEventListener('statechange', () => {
++ if (installingWorker.state === 'installed' && serviceWorker.controller) {
+ // SW is waiting to become active
+ handleWaitingSW();
+- } else if (installingWorker.state === 'activated' && !navigator.serviceWorker.controller) {
++ } else if (installingWorker.state === 'activated' && !serviceWorker.controller) {
+ // First SW is installed and active
+ handleFirstSWActivation();
+ }
+- };
++ });
+ }
+ }
+
+@@ -100,7 +116,7 @@ async function checkForUpdates(_ref) {
+ }
+
+ // If a new service worker will be installed:
+- registration.onupdatefound = handleInstallingWorker;
++ registration.addEventListener('updatefound', handleInstallingWorker);
+ }
+
+ /**
+diff --git a/node_modules/@dhis2/pwa/build/cjs/offline-interface/pwa-update-offline-interface.js b/node_modules/@dhis2/pwa/build/cjs/offline-interface/pwa-update-offline-interface.js
+new file mode 100644
+index 0000000..334c888
+--- /dev/null
++++ b/node_modules/@dhis2/pwa/build/cjs/offline-interface/pwa-update-offline-interface.js
+@@ -0,0 +1,140 @@
++"use strict";
++
++Object.defineProperty(exports, "__esModule", {
++ value: true
++});
++exports.PWAUpdateOfflineInterface = void 0;
++var _events = _interopRequireDefault(require("events"));
++var _constants = require("../lib/constants.js");
++var _registration = require("../lib/registration.js");
++function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
++// todo: this could be used as a base for the other, larger offline interface
++
++/**
++ * This and the following 'test' functions test for PWA features and log errors
++ * if there's an issue so they can be reused in the Offline Interface methods.
++ *
++ * Known situations when navigator.serviceWorker is not available:
++ * 1. Private browsing in firefox
++ * 2. Insecure contexts (e.g. http that's not local host)
++ */
++function testSWAvailable(_ref) {
++ let {
++ targetWindow = window
++ } = _ref;
++ if ('serviceWorker' in targetWindow.navigator) {
++ return true;
++ }
++ const msg = (!targetWindow.isSecureContext ? 'This window is not a secure context -- see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts.' : '`serviceWorker` is not available on `navigator`.') + ' PWA features will not work.';
++ console.error(new Error(msg));
++ return false;
++}
++
++/** This exposes an interface to check a */
++class PWAUpdateOfflineInterface {
++ constructor(_ref2) {
++ let {
++ targetWindow = window
++ } = _ref2;
++ if (!testSWAvailable({
++ targetWindow
++ })) {
++ return;
++ }
++ this.targetWindow = targetWindow;
++ this.serviceWorker = targetWindow.navigator.serviceWorker;
++
++ // This event emitter helps coordinate with service worker messages
++ this.offlineEvents = new _events.default();
++ // Receives messages from service worker and forwards to event emitter
++ const handleSWMessage = event => {
++ if (!event.data) {
++ return;
++ }
++ const {
++ type,
++ payload
++ } = event.data;
++ this.offlineEvents.emit(type, payload);
++ };
++ this.serviceWorker.addEventListener('message', handleSWMessage);
++ }
++
++ /** Basically `checkForUpdates` from registration.js exposed here */
++ checkForNewSW(_ref3) {
++ let {
++ onNewSW
++ } = _ref3;
++ // Check for SW updates (or first activation)
++ (0, _registration.checkForUpdates)({
++ onUpdate: onNewSW,
++ targetServiceWorker: this.serviceWorker
++ });
++ }
++
++ /**
++ * Requests clients info from the active service worker.
++ * @returns {Promise}
++ */
++ getClientsInfo() {
++ if (!testSWAvailable({
++ targetWindow: this.targetWindow
++ })) {
++ return Promise.resolve({
++ clientsCount: 0
++ });
++ }
++ return new Promise((resolve, reject) => {
++ this.serviceWorker.getRegistration().then(registration => {
++ const newestSW = (registration === null || registration === void 0 ? void 0 : registration.waiting) || (registration === null || registration === void 0 ? void 0 : registration.active);
++ if (!newestSW) {
++ resolve({
++ clientsCount: 0
++ });
++ return;
++ }
++
++ // Send request message to newest SW
++ newestSW.postMessage({
++ type: _constants.swMsgs.getClientsInfo
++ });
++ // Resolve with payload received from SW `clientsInfo` message
++ this.offlineEvents.once(_constants.swMsgs.clientsInfo, resolve);
++ // Clean up potentially unused listeners eventually
++ setTimeout(() => {
++ reject('Request for clients info timed out');
++ this.offlineEvents.removeAllListeners(_constants.swMsgs.clientsInfo);
++ }, 2000);
++ });
++ });
++ }
++
++ /**
++ * Makes a new SW either skip waiting if it's an update,
++ * or claim clients if it's the first SW activation
++ */
++ useNewSW() {
++ if (!testSWAvailable({
++ targetWindow: this.targetWindow
++ })) {
++ return Promise.resolve();
++ }
++ return this.serviceWorker.getRegistration().then(registration => {
++ if (!registration) {
++ throw new Error('No service worker is registered');
++ }
++ if (registration.waiting) {
++ // Update existing service worker
++ registration.waiting.postMessage({
++ type: _constants.swMsgs.skipWaiting
++ });
++ } else if (registration.active) {
++ // (First SW activation) Have SW take control of clients
++ registration.active.postMessage({
++ type: _constants.swMsgs.claimClients
++ });
++ }
++ });
++ }
++}
++exports.PWAUpdateOfflineInterface = PWAUpdateOfflineInterface;
diff --git a/node_modules/@dhis2/pwa/build/cjs/react/OfflineInterfaceContext.js b/node_modules/@dhis2/pwa/build/cjs/react/OfflineInterfaceContext.js
new file mode 100644
index 0000000..2d465d7
@@ -77,7 +310,6 @@ index 0000000..2d465d7
+};
+const useOfflineInterface = () => (0, _react.useContext)(OfflineInterfaceContext);
+exports.useOfflineInterface = useOfflineInterface;
-\ No newline at end of file
diff --git a/node_modules/@dhis2/pwa/build/cjs/react/usePWAUpdateState.js b/node_modules/@dhis2/pwa/build/cjs/react/usePWAUpdateState.js
new file mode 100644
index 0000000..7dea937
@@ -140,16 +372,257 @@ index 0000000..7dea937
+ };
+};
+exports.usePWAUpdateState = usePWAUpdateState;
+diff --git a/node_modules/@dhis2/pwa/build/cjs/service-worker/set-up-service-worker.js b/node_modules/@dhis2/pwa/build/cjs/service-worker/set-up-service-worker.js
+index 6fbacf8..948b93a 100644
+--- a/node_modules/@dhis2/pwa/build/cjs/service-worker/set-up-service-worker.js
++++ b/node_modules/@dhis2/pwa/build/cjs/service-worker/set-up-service-worker.js
+@@ -56,6 +56,8 @@ function setUpServiceWorker() {
+ } = _ref;
+ return url.endsWith('index.html');
+ });
++ // Make sure that this request doesn't redirect to a global shell
++ indexHtmlManifestEntry.url += '?redirect=false';
+ (0, _workboxPrecaching.precache)([indexHtmlManifestEntry]);
+
+ // Custom strategy for handling app navigation, specifically to allow
+@@ -87,7 +89,8 @@ function setUpServiceWorker() {
+ // Return true to signal that we want to use the handler.
+ return true;
+ };
+- const indexUrl = process.env.PUBLIC_URL + '/index.html';
++ // Above, the index entry had the redirect param added:
++ const indexUrl = process.env.PUBLIC_URL + '/index.html?redirect=false';
+ const navigationRouteHandler = _ref3 => {
+ let {
+ request
diff --git a/node_modules/@dhis2/pwa/build/es/index.js b/node_modules/@dhis2/pwa/build/es/index.js
-index 960e2b2..5bbcbff 100644
+index 960e2b2..bacd6a9 100644
--- a/node_modules/@dhis2/pwa/build/es/index.js
+++ b/node_modules/@dhis2/pwa/build/es/index.js
-@@ -2,3 +2,5 @@ export { setUpServiceWorker } from './service-worker/set-up-service-worker.js';
+@@ -1,4 +1,7 @@
+ export { setUpServiceWorker } from './service-worker/set-up-service-worker.js';
export { OfflineInterface } from './offline-interface/offline-interface.js';
++export { PWAUpdateOfflineInterface } from './offline-interface/pwa-update-offline-interface.js';
export { checkForUpdates, checkForSWUpdateAndReload, getRegistrationState, REGISTRATION_STATE_UNREGISTERED, REGISTRATION_STATE_WAITING, REGISTRATION_STATE_ACTIVE, REGISTRATION_STATE_FIRST_ACTIVATION } from './lib/registration.js';
export { getBaseUrlByAppName, setBaseUrlByAppName } from './lib/base-url-db.js';
+export { OfflineInterfaceProvider, useOfflineInterface } from './react/OfflineInterfaceContext.js';
-+export { usePWAUpdateState } from './react/usePWAUpdateState.js'
++export { usePWAUpdateState } from './react/usePWAUpdateState.js';
+diff --git a/node_modules/@dhis2/pwa/build/es/lib/registration.js b/node_modules/@dhis2/pwa/build/es/lib/registration.js
+index dcbe2e9..a3d91e8 100644
+--- a/node_modules/@dhis2/pwa/build/es/lib/registration.js
++++ b/node_modules/@dhis2/pwa/build/es/lib/registration.js
+@@ -25,16 +25,32 @@ export async function getRegistrationState() {
+ return REGISTRATION_STATE_ACTIVE;
+ }
+ }
++
++/**
++ * Can receive a specific SW instance to check for updates on, e.g. for a
++ * plugin window. Defaults to this window's navigator.serviceWorker.
++ * onUpdate is called with `{ registration }`
++ */
+ export async function checkForUpdates(_ref) {
+ let {
+- onUpdate
++ onUpdate,
++ targetServiceWorker
+ } = _ref;
+- if (!('serviceWorker' in navigator)) {
++ if (!('serviceWorker' in navigator) && !targetServiceWorker) {
+ return;
+ }
+- const registration = await navigator.serviceWorker.getRegistration();
++ const serviceWorker = targetServiceWorker || navigator.serviceWorker;
++ let registration = await serviceWorker.getRegistration();
+ if (registration === undefined) {
+- return;
++ // This could have raced before the call to `serviceWorker.register()`;
++ // wait and try again. Testing with a 20x CPU throttling in Chrome,
++ // 500 ms works on an M3 max macbook pro
++ await new Promise(r => setTimeout(r, 500));
++ registration = await serviceWorker.getRegistration();
++ if (registration === undefined) {
++ // Still didn't find it; probably not a PWA app.
++ return;
++ }
+ }
+ function handleWaitingSW() {
+ console.log('New content is available and will be used when all tabs for this page are closed.');
+@@ -65,21 +81,21 @@ export async function checkForUpdates(_ref) {
+ // callback doesn't get called in that case. Handle that here:
+ if (registration.waiting) {
+ handleWaitingSW();
+- } else if (registration.active && navigator.serviceWorker.controller === null) {
++ } else if (registration.active && serviceWorker.controller === null) {
+ handleFirstSWActivation();
+ }
+ function handleInstallingWorker() {
+ const installingWorker = registration.installing;
+ if (installingWorker) {
+- installingWorker.onstatechange = () => {
+- if (installingWorker.state === 'installed' && navigator.serviceWorker.controller) {
++ installingWorker.addEventListener('statechange', () => {
++ if (installingWorker.state === 'installed' && serviceWorker.controller) {
+ // SW is waiting to become active
+ handleWaitingSW();
+- } else if (installingWorker.state === 'activated' && !navigator.serviceWorker.controller) {
++ } else if (installingWorker.state === 'activated' && !serviceWorker.controller) {
+ // First SW is installed and active
+ handleFirstSWActivation();
+ }
+- };
++ });
+ }
+ }
+
+@@ -89,7 +105,7 @@ export async function checkForUpdates(_ref) {
+ }
+
+ // If a new service worker will be installed:
+- registration.onupdatefound = handleInstallingWorker;
++ registration.addEventListener('updatefound', handleInstallingWorker);
+ }
+
+ /**
+diff --git a/node_modules/@dhis2/pwa/build/es/offline-interface/pwa-update-offline-interface.js b/node_modules/@dhis2/pwa/build/es/offline-interface/pwa-update-offline-interface.js
+new file mode 100644
+index 0000000..b5e5147
+--- /dev/null
++++ b/node_modules/@dhis2/pwa/build/es/offline-interface/pwa-update-offline-interface.js
+@@ -0,0 +1,133 @@
++import EventEmitter from 'events';
++import { swMsgs } from '../lib/constants.js';
++import { checkForUpdates } from '../lib/registration.js';
++
++// todo: this could be used as a base for the other, larger offline interface
++
++/**
++ * This and the following 'test' functions test for PWA features and log errors
++ * if there's an issue so they can be reused in the Offline Interface methods.
++ *
++ * Known situations when navigator.serviceWorker is not available:
++ * 1. Private browsing in firefox
++ * 2. Insecure contexts (e.g. http that's not local host)
++ */
++function testSWAvailable(_ref) {
++ let {
++ targetWindow = window
++ } = _ref;
++ if ('serviceWorker' in targetWindow.navigator) {
++ return true;
++ }
++ const msg = (!targetWindow.isSecureContext ? 'This window is not a secure context -- see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts.' : '`serviceWorker` is not available on `navigator`.') + ' PWA features will not work.';
++ console.error(new Error(msg));
++ return false;
++}
++
++/** This exposes an interface to check a */
++export class PWAUpdateOfflineInterface {
++ constructor(_ref2) {
++ let {
++ targetWindow = window
++ } = _ref2;
++ if (!testSWAvailable({
++ targetWindow
++ })) {
++ return;
++ }
++ this.targetWindow = targetWindow;
++ this.serviceWorker = targetWindow.navigator.serviceWorker;
++
++ // This event emitter helps coordinate with service worker messages
++ this.offlineEvents = new EventEmitter();
++ // Receives messages from service worker and forwards to event emitter
++ const handleSWMessage = event => {
++ if (!event.data) {
++ return;
++ }
++ const {
++ type,
++ payload
++ } = event.data;
++ this.offlineEvents.emit(type, payload);
++ };
++ this.serviceWorker.addEventListener('message', handleSWMessage);
++ }
++
++ /** Basically `checkForUpdates` from registration.js exposed here */
++ checkForNewSW(_ref3) {
++ let {
++ onNewSW
++ } = _ref3;
++ // Check for SW updates (or first activation)
++ checkForUpdates({
++ onUpdate: onNewSW,
++ targetServiceWorker: this.serviceWorker
++ });
++ }
++
++ /**
++ * Requests clients info from the active service worker.
++ * @returns {Promise}
++ */
++ getClientsInfo() {
++ if (!testSWAvailable({
++ targetWindow: this.targetWindow
++ })) {
++ return Promise.resolve({
++ clientsCount: 0
++ });
++ }
++ return new Promise((resolve, reject) => {
++ this.serviceWorker.getRegistration().then(registration => {
++ const newestSW = (registration === null || registration === void 0 ? void 0 : registration.waiting) || (registration === null || registration === void 0 ? void 0 : registration.active);
++ if (!newestSW) {
++ resolve({
++ clientsCount: 0
++ });
++ return;
++ }
++
++ // Send request message to newest SW
++ newestSW.postMessage({
++ type: swMsgs.getClientsInfo
++ });
++ // Resolve with payload received from SW `clientsInfo` message
++ this.offlineEvents.once(swMsgs.clientsInfo, resolve);
++ // Clean up potentially unused listeners eventually
++ setTimeout(() => {
++ reject('Request for clients info timed out');
++ this.offlineEvents.removeAllListeners(swMsgs.clientsInfo);
++ }, 2000);
++ });
++ });
++ }
++
++ /**
++ * Makes a new SW either skip waiting if it's an update,
++ * or claim clients if it's the first SW activation
++ */
++ useNewSW() {
++ if (!testSWAvailable({
++ targetWindow: this.targetWindow
++ })) {
++ return Promise.resolve();
++ }
++ return this.serviceWorker.getRegistration().then(registration => {
++ if (!registration) {
++ throw new Error('No service worker is registered');
++ }
++ if (registration.waiting) {
++ // Update existing service worker
++ registration.waiting.postMessage({
++ type: swMsgs.skipWaiting
++ });
++ } else if (registration.active) {
++ // (First SW activation) Have SW take control of clients
++ registration.active.postMessage({
++ type: swMsgs.claimClients
++ });
++ }
++ });
++ }
++}
diff --git a/node_modules/@dhis2/pwa/build/es/react/OfflineInterfaceContext.js b/node_modules/@dhis2/pwa/build/es/react/OfflineInterfaceContext.js
new file mode 100644
index 0000000..ed7d292
@@ -182,7 +655,6 @@ index 0000000..ed7d292
+ children: PropTypes.node
+};
+export const useOfflineInterface = () => useContext(OfflineInterfaceContext);
-\ No newline at end of file
diff --git a/node_modules/@dhis2/pwa/build/es/react/usePWAUpdateState.js b/node_modules/@dhis2/pwa/build/es/react/usePWAUpdateState.js
new file mode 100644
index 0000000..4f11c97
@@ -238,4 +710,26 @@ index 0000000..4f11c97
+ onCancelUpdate
+ };
+};
-\ No newline at end of file
+diff --git a/node_modules/@dhis2/pwa/build/es/service-worker/set-up-service-worker.js b/node_modules/@dhis2/pwa/build/es/service-worker/set-up-service-worker.js
+index d6cd040..1b82e6d 100644
+--- a/node_modules/@dhis2/pwa/build/es/service-worker/set-up-service-worker.js
++++ b/node_modules/@dhis2/pwa/build/es/service-worker/set-up-service-worker.js
+@@ -50,6 +50,8 @@ export function setUpServiceWorker() {
+ } = _ref;
+ return url.endsWith('index.html');
+ });
++ // Make sure that this request doesn't redirect to a global shell
++ indexHtmlManifestEntry.url += '?redirect=false';
+ precache([indexHtmlManifestEntry]);
+
+ // Custom strategy for handling app navigation, specifically to allow
+@@ -81,7 +83,8 @@ export function setUpServiceWorker() {
+ // Return true to signal that we want to use the handler.
+ return true;
+ };
+- const indexUrl = process.env.PUBLIC_URL + '/index.html';
++ // Above, the index entry had the redirect param added:
++ const indexUrl = process.env.PUBLIC_URL + '/index.html?redirect=false';
+ const navigationRouteHandler = _ref3 => {
+ let {
+ request
diff --git a/src/App.jsx b/src/App.jsx
index ecb6769..6b2942c 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -5,6 +5,7 @@ import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom'
import styles from './App.module.css'
import { ConnectedHeaderBar } from './components/ConnectedHeaderbar.jsx'
import { PluginLoader } from './components/PluginLoader.jsx'
+import { ClientPWAProvider } from './lib/clientPWAUpdateState.jsx'
const APPS_INFO_QUERY = {
appMenu: {
@@ -18,37 +19,18 @@ const APPS_INFO_QUERY = {
// need to extend app-runtime to get that
}
-const Layout = ({
- clientPWAUpdateAvailable,
- onApplyClientUpdate,
- appsInfoQuery,
-}) => {
+const Layout = ({ appsInfoQuery }) => {
return (
-
+
)
}
-Layout.propTypes = {
- appsInfoQuery: PropTypes.object,
- clientPWAUpdateAvailable: PropTypes.bool,
- onApplyClientUpdate: PropTypes.func,
-}
+Layout.propTypes = { appsInfoQuery: PropTypes.object }
-// todo: also listen to navigations inside iframe (e.g. "Open this dashboard item in DV" links)
-// (Could the `window` prop on BrowserRouter help here?)
const MyApp = () => {
const { baseUrl } = useConfig()
- // todo: maybe pare this down to just onApplyUpdate?
- // todo: reset upon switching to a new client app
- const [clientPWAUpdateAvailable, setClientPWAUpdateAvailable] =
- React.useState(false)
- const [onApplyClientUpdate, setOnApplyClientUpdate] = React.useState()
const appsInfoQuery = useDataQuery(APPS_INFO_QUERY)
// todo: work on this to get the right URL when landing on an app URL
@@ -64,36 +46,25 @@ const MyApp = () => {
// (see getAppDisplayName in ConnectedHeaderBar.jsx and getAppDefaultAction in PluginLoader.jsx)
return (
-
-
-
+
+
+ }>
+ Local App}
+ />
+
+ }
/>
- }
- >
- Local App}
- />
-
- }
- />
-
-
-
+
+
+
+
)
}
diff --git a/src/components/ConfirmUpdateModal.jsx b/src/components/ConfirmUpdateModal.tsx
similarity index 84%
rename from src/components/ConfirmUpdateModal.jsx
rename to src/components/ConfirmUpdateModal.tsx
index d5b0bc2..06ae5a8 100644
--- a/src/components/ConfirmUpdateModal.jsx
+++ b/src/components/ConfirmUpdateModal.tsx
@@ -6,11 +6,20 @@ import {
ModalContent,
ModalTitle,
} from '@dhis2/ui'
-import PropTypes from 'prop-types'
import React from 'react'
import i18n from '../locales/index.js'
-export function ConfirmUpdateModal({ clientsCount, onCancel, onConfirm }) {
+interface ConfirmUpdateModalProps {
+ clientsCount: number
+ onCancel: () => void
+ onConfirm: () => void
+}
+
+export function ConfirmUpdateModal({
+ clientsCount,
+ onCancel,
+ onConfirm,
+}: ConfirmUpdateModalProps) {
return (
{i18n.t('Save your data')}
@@ -36,8 +45,3 @@ export function ConfirmUpdateModal({ clientsCount, onCancel, onConfirm }) {
)
}
-ConfirmUpdateModal.propTypes = {
- clientsCount: PropTypes.number,
- onCancel: PropTypes.func,
- onConfirm: PropTypes.func,
-}
diff --git a/src/components/ConnectedHeaderbar.jsx b/src/components/ConnectedHeaderbar.jsx
index 125137f..a9fea3b 100644
--- a/src/components/ConnectedHeaderbar.jsx
+++ b/src/components/ConnectedHeaderbar.jsx
@@ -1,18 +1,10 @@
import { usePWAUpdateState } from '@dhis2/pwa'
import { HeaderBar } from '@dhis2/ui'
import PropTypes from 'prop-types'
-import React from 'react'
+import React, { useMemo, useEffect } from 'react'
import { useParams } from 'react-router-dom'
-import { ConfirmUpdateModal } from './ConfirmUpdateModal.jsx'
-
-/**
- * Copied from ConnectedHeaderBar in app adapter:
- * Check for SW updates or a first activation, displaying an update notification
- * message in the HeaderBar profile menu. When an update is applied, if there are
- * multiple tabs of this app open, there's anadditional warning step because all
- * clients of the service worker will reload when there's an update, which may
- * cause data loss.
- */
+import { useClientPWAUpdateState } from '../lib/clientPWAUpdateState.jsx'
+import { ConfirmUpdateModal } from './ConfirmUpdateModal.tsx'
const getAppDisplayName = (appName, modules) => {
// todo: check this out if core apps get a different naming scheme
@@ -27,22 +19,32 @@ const getAppVersion = (appName, apps) => {
return apps.find((a) => a.short_name === parsedAppName)?.version
}
-export function ConnectedHeaderBar({
- clientPWAUpdateAvailable,
- onApplyClientUpdate,
- appsInfoQuery,
-}) {
+// todo:
+// type PWAUpdateState = {
+// updateAvailable: boolean
+// confirmReload: () => void
+// confirmationRequired: boolean
+// clientsCount: number | null
+// onConfirmUpdate: () => void
+// onCancelUpdate: () => void
+// }
+
+/**
+ * Copied from ConnectedHeaderBar in app adapter:
+ * Check for SW updates or a first activation, displaying an update notification
+ * message in the HeaderBar profile menu. Does this both for this app itself,
+ * and for the client app.
+ *
+ * When an update is applied, if there are multiple tabs of this app open,
+ * there's anadditional warning step because all clients of the service worker
+ * will reload when there's an update, which may cause data loss.
+ */
+export function ConnectedHeaderBar({ appsInfoQuery }) {
const params = useParams()
- const {
- updateAvailable: selfUpdateAvailable,
- confirmReload,
- confirmationRequired,
- clientsCount,
- onConfirmUpdate,
- onCancelUpdate,
- } = usePWAUpdateState()
+ const clientPWAUpdateState = useClientPWAUpdateState()
+ const selfPWAUpdateState = usePWAUpdateState()
- const appName = React.useMemo(() => {
+ const appName = useMemo(() => {
if (!params.appName || !appsInfoQuery.data) {
// `undefined` defaults to app title in header bar component, i.e. "Global Shell"
return
@@ -57,36 +59,33 @@ export function ConnectedHeaderBar({
}, [appsInfoQuery.data, params.appName])
// Set new displayname to page title when it updates
- React.useEffect(() => {
+ useEffect(() => {
if (appName) {
document.title = `${appName} | DHIS2`
}
}, [appName])
- const appVersion = React.useMemo(() => {
+ const appVersion = useMemo(() => {
if (!params.appName || !appsInfoQuery.data) {
return
}
return getAppVersion(params.appName, appsInfoQuery.data.apps)
}, [appsInfoQuery.data, params.appName])
- // Choose the right handler
- const handleApplyAvailableUpdate = React.useMemo(() => {
- if (clientPWAUpdateAvailable && !selfUpdateAvailable) {
- return onApplyClientUpdate
- }
- // If there's an update ready for both the global shell and the client,
- // updating the global shell will handle the client updates as they
- // will all get reloaded
- return confirmReload
- }, [
- clientPWAUpdateAvailable,
- selfUpdateAvailable,
+ // For now, the header bar can only show one "Update available" badge, so
+ // choose the right values based on which update(s) is/are available:
+ // By default, use client's PWA update state. If there's an update available for
+ // the global shell, though, the self PWA update state takes precedent
+ const {
+ updateAvailable,
confirmReload,
- onApplyClientUpdate,
- ])
-
- const updateAvailable = selfUpdateAvailable || clientPWAUpdateAvailable
+ confirmationRequired,
+ clientsCount,
+ onConfirmUpdate,
+ onCancelUpdate,
+ } = selfPWAUpdateState.updateAvailable
+ ? selfPWAUpdateState
+ : clientPWAUpdateState
return (
<>
@@ -96,10 +95,8 @@ export function ConnectedHeaderBar({
// todo: currently not used by the component
appVersion={appVersion}
updateAvailable={updateAvailable}
- onApplyAvailableUpdate={handleApplyAvailableUpdate}
+ onApplyAvailableUpdate={confirmReload}
/>
- {/* The following is used for global shell updates -- */}
- {/* the client app will handle its own confirmation modal */}
{confirmationRequired ? (
)
}
-ConnectedHeaderBar.propTypes = {
- appsInfoQuery: PropTypes.object,
- clientPWAUpdateAvailable: PropTypes.bool,
- onApplyClientUpdate: PropTypes.func,
-}
+ConnectedHeaderBar.propTypes = { appsInfoQuery: PropTypes.object }
diff --git a/src/components/PluginLoader.jsx b/src/components/PluginLoader.jsx
index b6c459e..440725b 100644
--- a/src/components/PluginLoader.jsx
+++ b/src/components/PluginLoader.jsx
@@ -4,6 +4,7 @@ import { Plugin } from '@dhis2/app-runtime/experimental'
import PropTypes from 'prop-types'
import React from 'react'
import { useLocation, useParams } from 'react-router-dom'
+import { useClientOfflineInterface } from '../lib/clientPWAUpdateState.jsx'
import i18n from '../locales/index.js'
import styles from './PluginLoader.module.css'
@@ -75,6 +76,7 @@ const watchForHashRouteChanges = (event) => {
* in the onLoad handler)
* 2. The navigation is to another app: the backend will reroute to the global
* shell with the right app
+ * 3. The backend redirects to the login page: send the app there
*
* This should be called on `load` events in the iframe, which indicates some
* kind of page navigation; this won't trigger on hash route changes.
@@ -89,11 +91,7 @@ const handleExternalNavigation = (iframeLoadEvent, pluginHref) => {
}
}
-export const PluginLoader = ({
- setClientPWAUpdateAvailable,
- setOnApplyClientUpdate,
- appsInfoQuery,
-}) => {
+export const PluginLoader = ({ appsInfoQuery }) => {
const params = useParams()
const location = useLocation()
const { baseUrl } = useConfig()
@@ -105,6 +103,7 @@ export const PluginLoader = ({
),
{ warning: true }
)
+ const initClientOfflineInterface = useClientOfflineInterface()
// test prop messaging and updates
const [color, setColor] = React.useState('blue')
@@ -146,7 +145,10 @@ export const PluginLoader = ({
const handleLoad = React.useCallback(
(event) => {
// If we can't access the new page's Document, this is a cross-domain page.
- // Disallow that; return to previous plugin state
+ // Disallow that; return to previous plugin state.
+ // todo: can cause an infinite reload if the current pluginHref loads
+ // an entirely broken page -- this is a rare case though; one example is
+ // a PWA app where the precache has been deleted. 404s are fine.
if (!event.target.contentDocument) {
setRerenderKey((k) => k + 1)
showNavigationWarning()
@@ -162,8 +164,11 @@ export const PluginLoader = ({
}
injectHeaderbarHidingStyles(event)
watchForHashRouteChanges(event)
+ initClientOfflineInterface({
+ clientWindow: event.target.contentWindow,
+ })
},
- [pluginHref, showNavigationWarning]
+ [pluginHref, showNavigationWarning, initClientOfflineInterface]
)
if (!pluginHref) {
@@ -178,25 +183,10 @@ export const PluginLoader = ({
onLoad={handleLoad}
key={rerenderKey}
// Other props
- reportPWAUpdateStatus={(data) => {
- const { updateAvailable, onApplyUpdate } = data
- console.log('recieved PWA status', { data })
-
- setClientPWAUpdateAvailable(updateAvailable)
- if (onApplyUpdate) {
- // Return function from a function -- otherwise, setState tries to invoke the function
- // to evaluate its next state
- setOnApplyClientUpdate(() => onApplyUpdate)
- }
- }}
- // props test
+ // (for testing:)
color={color}
toggleColor={toggleColor}
/>
)
}
-PluginLoader.propTypes = {
- appsInfoQuery: PropTypes.object,
- setClientPWAUpdateAvailable: PropTypes.func,
- setOnApplyClientUpdate: PropTypes.func,
-}
+PluginLoader.propTypes = { appsInfoQuery: PropTypes.object }
diff --git a/src/lib/clientPWAUpdateState.jsx b/src/lib/clientPWAUpdateState.jsx
new file mode 100644
index 0000000..bc38075
--- /dev/null
+++ b/src/lib/clientPWAUpdateState.jsx
@@ -0,0 +1,98 @@
+import { PWAUpdateOfflineInterface } from '@dhis2/pwa'
+import PropTypes from 'prop-types'
+import React, {
+ useState,
+ useCallback,
+ createContext,
+ useContext,
+ useMemo,
+} from 'react'
+
+const ClientPWAUpdateStateContext = createContext()
+const ClientOfflineInterfaceContext = createContext()
+
+export const ClientPWAProvider = ({ children }) => {
+ const [offlineInterface, setOfflineInterface] = useState()
+ const [updateAvailable, setUpdateAvailable] = useState(false)
+ const [clientsCount, setClientsCount] = useState(null)
+
+ const onConfirmUpdate = useCallback(() => {
+ offlineInterface.useNewSW()
+ }, [offlineInterface])
+ const onCancelUpdate = useCallback(() => {
+ setClientsCount(null)
+ }, [])
+
+ const confirmReload = useCallback(() => {
+ offlineInterface
+ .getClientsInfo()
+ .then(({ clientsCount }) => {
+ if (clientsCount === 1) {
+ // Just one client; go ahead and reload
+ onConfirmUpdate()
+ } else {
+ // Multiple clients; warn about data loss before reloading
+ setClientsCount(clientsCount)
+ }
+ })
+ .catch((reason) => {
+ // Didn't get clients info
+ console.warn(reason)
+
+ // Go ahead with confirmation modal with `0` as clientsCount
+ setClientsCount(0)
+ })
+ }, [offlineInterface, onConfirmUpdate])
+
+ const confirmationRequired = clientsCount !== null
+ const clientPWAUpdateState = useMemo(
+ () => ({
+ updateAvailable,
+ confirmReload,
+ confirmationRequired,
+ clientsCount,
+ onConfirmUpdate,
+ onCancelUpdate,
+ }),
+ [
+ updateAvailable,
+ confirmReload,
+ confirmationRequired,
+ clientsCount,
+ onConfirmUpdate,
+ onCancelUpdate,
+ ]
+ )
+
+ const initClientOfflineInterface = useCallback(({ clientWindow }) => {
+ const newOfflineInterface = new PWAUpdateOfflineInterface({
+ targetWindow: clientWindow,
+ })
+ // Reset this, if it's keeping a dialog open from a previous reload
+ setClientsCount(null)
+ setOfflineInterface(newOfflineInterface)
+ newOfflineInterface.checkForNewSW({
+ onNewSW: () => {
+ setUpdateAvailable(true)
+ },
+ })
+ }, [])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
+ClientPWAProvider.propTypes = { children: PropTypes.node }
+
+export const useClientPWAUpdateState = () =>
+ useContext(ClientPWAUpdateStateContext)
+
+/** Returns initClientOfflineInterface() */
+export const useClientOfflineInterface = () =>
+ useContext(ClientOfflineInterfaceContext)
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..aafc527
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,29 @@
+// drawn from https://www.totaltypescript.com/tsconfig-cheat-sheet
+{
+ "compilerOptions": {
+ /* Base Options: */
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "target": "es2022",
+ "allowJs": true,
+ "resolveJsonModule": true,
+ "moduleDetection": "force",
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true,
+
+ /* Strictness */
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+
+ /* If NOT transpiling with TypeScript: */
+ "module": "preserve",
+ "noEmit": true,
+
+ /* If your code runs in the DOM: */
+ "lib": ["es2022", "dom", "dom.iterable"],
+
+ // Other things relevant to this projext:
+ "jsx": "react",
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index b011cc7..ce8b8de 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1840,45 +1840,45 @@
"@dhis2/pwa" "12.3.0"
moment "^2.24.0"
-"@dhis2/app-runtime@^3.12.0", "@dhis2/app-runtime@^3.13.2":
- version "3.13.2"
- resolved "https://registry.yarnpkg.com/@dhis2/app-runtime/-/app-runtime-3.13.2.tgz#c3eded902b457fb35d949e05ccb5c17191f05e56"
- integrity sha512-rY0DfADBytx5WD6cf8nYnjyzUmsEORxMUL20iIFgAQQ64H3073FI4GOJB11Xt9NF5cCJhtQLJsehAq8zhI7GUQ==
- dependencies:
- "@dhis2/app-service-alerts" "3.13.2"
- "@dhis2/app-service-config" "3.13.2"
- "@dhis2/app-service-data" "3.13.2"
- "@dhis2/app-service-offline" "3.13.2"
- "@dhis2/app-service-plugin" "3.13.2"
-
-"@dhis2/app-service-alerts@3.13.2":
- version "3.13.2"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-alerts/-/app-service-alerts-3.13.2.tgz#f8850bbd6c36d1fd62fbd9c1502e73f6def9f9ce"
- integrity sha512-7q87wKQAy+xxHtxhsowCL28bncNKsyX1wLkMiwuVQvDwWwgzdAN90csF3RtBRIoV3FUqdTiMymyqrqiu7gDgSg==
-
-"@dhis2/app-service-config@3.13.2":
- version "3.13.2"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-config/-/app-service-config-3.13.2.tgz#350cb36d3bb19fbc24e801038cefabb39b3756eb"
- integrity sha512-dejWJUwQJTr+3Nxt/NRlcuCpIHV6HD8YYCSXFcL+yCxIM8GCyUAWzjR/B9nja3K6/MbN74yGMuozmyYqScBdUw==
-
-"@dhis2/app-service-data@3.13.2":
- version "3.13.2"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-data/-/app-service-data-3.13.2.tgz#e3e0201a916dfe9cca8b5caa3d1b5eabda154534"
- integrity sha512-awwta3LQywTjGQF4McUYr1oJFoTc8SSBPH5FJ4L60aivNkMFXoUTUJ629eC+xaPmuuvn5O712jGDFPGlTJO4bg==
+"@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==
dependencies:
"@tanstack/react-query" "^4.36.1"
-"@dhis2/app-service-offline@3.13.2":
- version "3.13.2"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-offline/-/app-service-offline-3.13.2.tgz#76a42f5b501045872f7d0527c8891e701d33bf3c"
- integrity sha512-yBvEL+ZpALfnJIkEFhfd6oYGx6esq4QSbs604O1BpajR6CjlPzmSTwKqEp9QGcXv+PEyoKnwR0w/t/gxPKtkXw==
+"@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==
dependencies:
lodash "^4.17.21"
-"@dhis2/app-service-plugin@3.13.2":
- version "3.13.2"
- resolved "https://registry.yarnpkg.com/@dhis2/app-service-plugin/-/app-service-plugin-3.13.2.tgz#7d71c909c433fc7078700e7511552716adf7e86c"
- integrity sha512-3RDdbslRj4yU3XTvpoaV+F0Fg1CrM74zByn0so3NzIizKhjtzAuVBdRpluml/W6qkwrkCLk3U0oqssIURapDfg==
+"@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==
dependencies:
post-robot "^10.0.46"
@@ -1977,10 +1977,10 @@
update-notifier "^3.0.0"
yargs "^13.1.0"
-"@dhis2/cli-style@^10.7.5":
- version "10.7.5"
- resolved "https://registry.yarnpkg.com/@dhis2/cli-style/-/cli-style-10.7.5.tgz#d2794436723500d02d017b1849ec0e39d195b53b"
- integrity sha512-M8K0vmtC8nHwpb7D4Njte8z7MJgpMrgl8BkxI3Y4NeCwun+QVMszCDF0xZQG32LGnsQTNwLvPDilyXTWN/NWLA==
+"@dhis2/cli-style@^10.7.6":
+ version "10.7.6"
+ resolved "https://registry.yarnpkg.com/@dhis2/cli-style/-/cli-style-10.7.6.tgz#70bd7f1995ae31d6aecaaf4233ecff290a3840ef"
+ integrity sha512-MAu7zEVH5JaENlfwnIjRrrEiNUqpYBDLuOg+y209zpc9YNT+Cn1mnAXRzzdIa4Tlg0eGn+fGdlL9e+pOpb9IUA==
dependencies:
"@commitlint/cli" "^12.1.4"
"@commitlint/config-conventional" "^13.1.0"
@@ -3045,6 +3045,13 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
+"@types/react@^19.0.10":
+ version "19.0.10"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb"
+ integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==
+ dependencies:
+ csstype "^3.0.2"
+
"@types/resolve@1.20.2":
version "1.20.2"
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
@@ -4806,6 +4813,11 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
+csstype@^3.0.2:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
+ integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+
dargs@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc"
@@ -11030,6 +11042,11 @@ typescript@^5.6.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b"
integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==
+typescript@^5.7.3:
+ version "5.7.3"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e"
+ integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==
+
uglify-js@^3.1.4:
version "3.17.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"