From ecd8b21dc34f1eee7497331cb254c459252d355d Mon Sep 17 00:00:00 2001 From: Kai Vandivier Date: Thu, 3 Nov 2022 15:00:07 +0100 Subject: [PATCH 1/2] fix(pwa-boundary): catch errors --- adapter/src/components/PWALoadingBoundary.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/adapter/src/components/PWALoadingBoundary.js b/adapter/src/components/PWALoadingBoundary.js index 64e5e08e8..e35f4bf56 100644 --- a/adapter/src/components/PWALoadingBoundary.js +++ b/adapter/src/components/PWALoadingBoundary.js @@ -29,7 +29,10 @@ export const PWALoadingBoundary = ({ children }) => { setPWAReady(true) } } - checkRegistration() + checkRegistration().catch((err) => { + console.error(err) + setPWAReady(true) + }) }, [offlineInterface]) return pwaReady ? children : null From ad3e476273823c1c4983c50ac0500e3b9eb19c30 Mon Sep 17 00:00:00 2001 From: Kai Vandivier Date: Thu, 3 Nov 2022 16:10:03 +0100 Subject: [PATCH 2/2] fix(offline-interface): protect against SW errors --- .../offline-interface/offline-interface.js | 105 +++++++++++++----- 1 file changed, 75 insertions(+), 30 deletions(-) diff --git a/pwa/src/offline-interface/offline-interface.js b/pwa/src/offline-interface/offline-interface.js index 6fcfb3214..5cf0d2beb 100644 --- a/pwa/src/offline-interface/offline-interface.js +++ b/pwa/src/offline-interface/offline-interface.js @@ -8,6 +8,45 @@ import { } from '../lib/registration.js' import { openSectionsDB, SECTIONS_STORE } from '../lib/sections-db.js' +const PWA_ENABLED = process.env.REACT_APP_DHIS2_APP_PWA_ENABLED === 'true' + +/** + * 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() { + if ('serviceWorker' in navigator) { + return true + } + + const msg = + (!window.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 +} + +function testPWAEnabled() { + if (PWA_ENABLED) { + return true + } + + const msg = + 'PWA is not enabled in `d2.config.js`. No service worker will be registered and offline interface features will not work.' + console.error(new Error(msg)) + return false +} + +function testPWAAndSW() { + return testSWAvailable() && testPWAEnabled() +} + /** Helper to simplify SW message sending */ function swMessage(type, payload) { if (!navigator.serviceWorker.controller) { @@ -26,7 +65,8 @@ function swMessage(type, payload) { */ export class OfflineInterface { constructor() { - this.pwaEnabled = process.env.REACT_APP_DHIS2_APP_PWA_ENABLED === 'true' + // Helper property that consumers can check + this.pwaEnabled = PWA_ENABLED if (this.pwaEnabled) { register() @@ -34,7 +74,7 @@ export class OfflineInterface { unregister() } - if (!('serviceWorker' in navigator)) { + if (!testSWAvailable()) { return } @@ -78,6 +118,10 @@ export class OfflineInterface { * @returns {Promise} */ getClientsInfo() { + if (!testSWAvailable()) { + return Promise.resolve({ clientsCount: 0 }) + } + return new Promise((resolve, reject) => { navigator.serviceWorker.getRegistration().then((registration) => { const newestSW = registration?.waiting || registration?.active @@ -104,22 +148,28 @@ export class OfflineInterface { * or claim clients if it's the first SW activation */ useNewSW() { - navigator.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, - }) - } - }) + if (!testSWAvailable()) { + return Promise.resolve() + } + + return navigator.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, + }) + } + }) } /** @@ -146,11 +196,10 @@ export class OfflineInterface { onCompleted, onError, }) { - if (!this.pwaEnabled) { - throw new Error( - 'Offline features are not enabled. Make sure `pwa.enabled` is `true` in `d2.config.js`' - ) + if (!testPWAAndSW()) { + return } + if (!sectionId || !onStarted || !onCompleted || !onError) { throw new Error( '[Offline interface] The options { sectionId, onStarted, onCompleted, onError } are required when calling startRecording()' @@ -202,10 +251,8 @@ export class OfflineInterface { * @returns {Promise} A promise that resolves to an array of cached sections. */ async getCachedSections() { - if (!this.pwaEnabled) { - throw new Error( - 'Cannot get cached sections - PWA is not enabled in d2.config.js' - ) + if (!testPWAAndSW()) { + return [] } await navigator.serviceWorker.ready @@ -230,10 +277,8 @@ export class OfflineInterface { * @returns {Promise} A promise that resolves to `true` if at least one of the cache or the idb entry are deleted or `false` if neither were found. */ async removeSection(sectionId) { - if (!this.pwaEnabled) { - throw new Error( - 'Cannot remove section - PWA is not enabled in d2.config.js' - ) + if (!testPWAAndSW()) { + return false } if (!sectionId) { throw new Error('No section ID specified to delete')