Skip to content

Commit

Permalink
fix(pwa): improve SW update UX
Browse files Browse the repository at this point in the history
Includes OfflineInterface and adapter refactor to use DHIS UI. LIBS-237
  • Loading branch information
KaiVandivier committed Oct 7, 2021
1 parent f0e57bb commit 79b5285
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 98 deletions.
31 changes: 29 additions & 2 deletions adapter/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2021-07-16T13:22:12.456Z\n"
"PO-Revision-Date: 2021-07-16T13:22:12.456Z\n"
"POT-Creation-Date: 2021-10-06T14:48:14.669Z\n"
"PO-Revision-Date: 2021-10-06T14:48:14.669Z\n"

msgid "An error occurred in the DHIS2 application."
msgstr "An error occurred in the DHIS2 application."
Expand Down Expand Up @@ -46,3 +46,30 @@ msgstr "Password"

msgid "Sign in"
msgstr "Sign in"

msgid "Save your data"
msgstr "Save your data"

msgid ""
"Updating will reload all {{n}} open instances of this app, and any unsaved "
"data will be lost. Save any data you need to, then click 'Reload' when "
"ready."
msgstr ""
"Updating will reload all {{n}} open instances of this app, and any unsaved "
"data will be lost. Save any data you need to, then click 'Reload' when "
"ready."

msgid "Cancel"
msgstr "Cancel"

msgid "Reload"
msgstr "Reload"

msgid "There's an update available for this app."
msgstr "There's an update available for this app."

msgid "Update and reload"
msgstr "Update and reload"

msgid "Not now"
msgstr "Not now"
5 changes: 4 additions & 1 deletion adapter/src/components/AppWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { useVerifyLatestUser } from '../utils/useVerifyLatestUser.js'
import { Alerts } from './Alerts.js'
import { ErrorBoundary } from './ErrorBoundary.js'
import { LoadingMask } from './LoadingMask.js'
import PWAUpdateManager from './PWAUpdateManager.js'
import { styles } from './styles/AppWrapper.style.js'

export const AppWrapper = ({ appName, children }) => {
export const AppWrapper = ({ appName, children, offlineInterface }) => {
const { loading: localeLoading } = useCurrentUserLocale()
const { loading: latestUserLoading } = useVerifyLatestUser()

Expand All @@ -26,11 +27,13 @@ export const AppWrapper = ({ appName, children }) => {
</ErrorBoundary>
</div>
<Alerts />
<PWAUpdateManager offlineInterface={offlineInterface} />
</div>
)
}

AppWrapper.propTypes = {
appName: PropTypes.string.isRequired,
children: PropTypes.node,
offlineInterface: PropTypes.shape({}),
}
96 changes: 96 additions & 0 deletions adapter/src/components/PWAUpdateManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useAlert } from '@dhis2/app-runtime'
import {
Button,
ButtonStrip,
Modal,
ModalActions,
ModalContent,
ModalTitle,
} from '@dhis2/ui'
import PropTypes from 'prop-types'
import React, { useState, useEffect } from 'react'
import i18n from '../locales'

function ConfirmReloadModal({ clientsCount, onCancel, onConfirm }) {
return (
<Modal position="middle">
<ModalTitle>{i18n.t('Save your data')}</ModalTitle>
<ModalContent>
{i18n.t(
"Updating will reload all {{n}} open instances of this app, and any unsaved data will be lost. Save any data you need to, then click 'Reload' when ready.",
{ n: clientsCount }
)}
</ModalContent>
<ModalActions>
<ButtonStrip end>
<Button onClick={onCancel}>{i18n.t('Cancel')}</Button>
<Button destructive onClick={onConfirm}>
{i18n.t('Reload')}
</Button>
</ButtonStrip>
</ModalActions>
</Modal>
)
}
ConfirmReloadModal.propTypes = {
clientsCount: PropTypes.number,
onCancel: PropTypes.func,
onConfirm: PropTypes.func,
}

/**
* Uses the offlineInterface to check for SW updates or a first activation,
* then prompts the user to reload the page to use the new SW and access new
* app updates. If there are multiple tabs of this app open, there's an
* additional warning step because all clients of the service worker will
* reload when there's an update, which may cause data loss.
*/
export default function PWAUpdateManager({ offlineInterface }) {
const [confirmReloadModalOpen, setConfirmReloadModalOpen] = useState(false)
const [clientsCountState, setClientsCountState] = useState(1)
const { show } = useAlert(
i18n.t("There's an update available for this app."),
({ onConfirm }) => ({
permanent: true,
actions: [
{ label: i18n.t('Update and reload'), onClick: onConfirm },
{ label: i18n.t('Not now'), onClick: () => {} },
],
})
)

const confirmReload = async () => {
const { clientsCount } = await offlineInterface.getClientsInfo()
setClientsCountState(clientsCount)
if (clientsCount <= 1) {
// Just one client; go ahead and reload
offlineInterface.useNewSW()
} else {
// Multiple clients open; warn about data loss before reloading
setConfirmReloadModalOpen(true)
}
}

useEffect(() => {
offlineInterface.checkForNewSW({
onNewSW: () => {
show({ onConfirm: confirmReload })
},
})
}, [])

return confirmReloadModalOpen ? (
<ConfirmReloadModal
onConfirm={() => offlineInterface.useNewSW()}
onCancel={() => setConfirmReloadModalOpen(false)}
clientsCount={clientsCountState}
/>
) : null
}
PWAUpdateManager.propTypes = {
offlineInterface: PropTypes.shape({
checkForNewSW: PropTypes.func,
getClientsInfo: PropTypes.func,
useNewSW: PropTypes.func,
}),
}
4 changes: 3 additions & 1 deletion adapter/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const AppAdapter = ({ url, apiVersion, appName, pwaEnabled, children }) => (
pwaEnabled={pwaEnabled}
offlineInterface={offlineInterface}
>
<AppWrapper appName={appName}>{children}</AppWrapper>
<AppWrapper appName={appName} offlineInterface={offlineInterface}>
{children}
</AppWrapper>
</ServerVersionProvider>
</ErrorBoundary>
)
Expand Down
18 changes: 0 additions & 18 deletions pwa/i18n/en.pot

This file was deleted.

3 changes: 3 additions & 0 deletions pwa/src/lib/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const swMsgs = Object.freeze({
getClientsInfo: 'GET_CLIENTS_INFO',
clientsInfo: 'CLIENTS_INFO',
claimClients: 'CLAIM_CLIENTS',
skipWaiting: 'SKIP_WAITING',
startRecording: 'START_RECORDING',
recordingStarted: 'RECORDING_STARTED',
Expand Down
39 changes: 30 additions & 9 deletions pwa/src/lib/registration.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,24 @@ export async function checkForUpdates({ onUpdate }) {

function handleWaitingSW() {
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://cra.link/PWA.'
'New content is available and will be used when all tabs for this page are closed.'
)

// Execute callback
if (onUpdate) {
onUpdate(registration)
onUpdate({ registration })
}
}

// Handle active but not-controlling worker
// (which happens on first installation, if clients.claim() is not used)
function handleFirstSWActivation() {
console.log(
'This app is ready for offline usage. Refresh to use offline features.'
)

if (onUpdate) {
onUpdate({ registration, firstActivation: true })
}
}

Expand All @@ -26,19 +37,29 @@ export async function checkForUpdates({ onUpdate }) {
// callback doesn't get called in that case. Handle that here:
if (registration.waiting) {
handleWaitingSW()
} else if (
registration.active &&
navigator.serviceWorker.controller === null
) {
handleFirstSWActivation()
}

function handleInstallingWorker() {
const installingWorker = registration.installing
if (installingWorker) {
installingWorker.onstatechange = () => {
if (installingWorker.state !== 'installed') {
return
}
if (navigator.serviceWorker.controller) {
if (
installingWorker.state === 'installed' &&
navigator.serviceWorker.controller
) {
// SW is waiting to become active
handleWaitingSW()
} else {
console.log('Content is cached for offline use.')
} else if (
installingWorker.state === 'activated' &&
!navigator.serviceWorker.controller
) {
// First SW is installed and active
handleFirstSWActivation()
}
}
}
Expand Down
Loading

0 comments on commit 79b5285

Please sign in to comment.