-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { useCacheableSection, CacheableSection } from '@dhis2/app-runtime' | ||
import { CenteredContent, CircularLoader, Layer } from '@dhis2/ui' | ||
import postRobot from '@krakenjs/post-robot' | ||
import { debounce } from 'lodash/fp' | ||
import PropTypes from 'prop-types' | ||
import React, { useEffect, useLayoutEffect, useState } from 'react' | ||
import { Plugin } from './plugin/Plugin.js' | ||
import { getPWAInstallationStatus } from './util/getPWAInstallationStatus.js' | ||
|
||
const LoadingMask = () => { | ||
return ( | ||
<Layer> | ||
<CenteredContent> | ||
<CircularLoader /> | ||
</CenteredContent> | ||
</Layer> | ||
) | ||
} | ||
|
||
const CacheableSectionWrapper = ({ | ||
id, | ||
children, | ||
cacheNow, | ||
isParentCached, | ||
}) => { | ||
const { startRecording, isCached, remove } = useCacheableSection(id) | ||
|
||
useEffect(() => { | ||
if (cacheNow) { | ||
startRecording({ onError: console.error }) | ||
} | ||
|
||
// NB: Adding `startRecording` to dependencies causes | ||
// an infinite recording loop as-is (probably need to memoize it) | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [cacheNow]) | ||
|
||
useEffect(() => { | ||
const listener = postRobot.on( | ||
'removeCachedData', | ||
// todo: check domain too; differs based on deployment env though | ||
{ window: window.parent }, | ||
() => remove() | ||
) | ||
|
||
return () => listener.cancel() | ||
}, [remove]) | ||
|
||
useEffect(() => { | ||
// Synchronize cache state on load or prop update | ||
// -- a back-up to imperative `removeCachedData` | ||
if (!isParentCached && isCached) { | ||
remove() | ||
} | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [isParentCached]) | ||
|
||
return ( | ||
<CacheableSection id={id} loadingMask={LoadingMask}> | ||
{children} | ||
</CacheableSection> | ||
) | ||
} | ||
CacheableSectionWrapper.propTypes = { | ||
cacheNow: PropTypes.bool, | ||
children: PropTypes.node, | ||
id: PropTypes.string, | ||
isParentCached: PropTypes.bool, | ||
} | ||
|
||
const sendInstallationStatus = (installationStatus) => { | ||
postRobot.send(window.parent, 'installationStatus', { installationStatus }) | ||
} | ||
|
||
const PluginWrapper = () => { | ||
const [propsFromParent, setPropsFromParent] = useState() | ||
const [renderId, setRenderId] = useState(null) | ||
|
||
const receivePropsFromParent = (event) => setPropsFromParent(event.data) | ||
|
||
useEffect(() => { | ||
postRobot | ||
.send(window.parent, 'getProps') | ||
.then(receivePropsFromParent) | ||
.catch((err) => console.error(err)) | ||
|
||
// Get & send PWA installation status now, and also prepare to send | ||
// future updates (installing/ready) | ||
getPWAInstallationStatus({ | ||
onStateChange: sendInstallationStatus, | ||
}).then(sendInstallationStatus) | ||
|
||
// Allow parent to update props | ||
const listener = postRobot.on( | ||
'newProps', | ||
{ window: window.parent /* Todo: check domain */ }, | ||
receivePropsFromParent | ||
) | ||
|
||
return () => listener.cancel() | ||
}, []) | ||
|
||
useLayoutEffect(() => { | ||
const updateRenderId = debounce(300, () => | ||
setRenderId((renderId) => | ||
typeof renderId === 'number' ? renderId + 1 : 1 | ||
) | ||
) | ||
|
||
window.addEventListener('resize', updateRenderId) | ||
|
||
return () => window.removeEventListener('resize', updateRenderId) | ||
}, []) | ||
|
||
return propsFromParent ? ( | ||
<div | ||
style={{ | ||
display: 'flex', | ||
height: '100%', | ||
overflow: 'hidden', | ||
}} | ||
> | ||
<Plugin id={renderId} {...propsFromParent} /> | ||
</div> | ||
) : null | ||
} | ||
|
||
export default PluginWrapper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import React from 'react' | ||
import { App } from '../app/app' | ||
|
||
export const Plugin = (props) => { | ||
return <App /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
export const INSTALLATION_STATES = { | ||
READY: 'READY', | ||
INSTALLING: 'INSTALLING', | ||
} | ||
|
||
function handleInstallingWorker({ installingWorker, onStateChange }) { | ||
installingWorker.onstatechange = () => { | ||
if (installingWorker.state === 'activated') { | ||
// ... and update state to 'ready' | ||
onStateChange(INSTALLATION_STATES.READY) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Gets the current installation state of the PWA features, which is intended | ||
* to be reported from this plugin to the parent app to indicate that the | ||
* static assets are cached and ready to be accessed locally instead of over | ||
* the network. | ||
* | ||
* Returns either READY, INSTALLING, or `null` for not installed/won't install | ||
*/ | ||
export async function getPWAInstallationStatus({ onStateChange }) { | ||
console.log('debug:getPWAInstallationStatus') | ||
if (!navigator.serviceWorker) { | ||
// Nothing to do here | ||
return null | ||
} | ||
|
||
const registration = await navigator.serviceWorker.getRegistration() | ||
if (!registration) { | ||
// This shouldn't happen since this is a PWA app, but return null | ||
return null | ||
} | ||
|
||
if (registration.active) { | ||
return INSTALLATION_STATES.READY | ||
} | ||
// note that 'registration.waiting' is skipped - it implies there's an active one | ||
if (registration.installing) { | ||
handleInstallingWorker({ | ||
installingWorker: registration.installing, | ||
onStateChange, | ||
}) | ||
return INSTALLATION_STATES.INSTALLING | ||
} | ||
|
||
// It shouldn't normally be possible to get here, but just in case, | ||
// listen for installations | ||
registration.onupdatefound = () => { | ||
// update state for this plugin to 'installing' | ||
onStateChange(INSTALLATION_STATES.INSTALLING) | ||
|
||
// also listen for the installing worker to become active | ||
const installingWorker = registration.installing | ||
if (!installingWorker) { | ||
return | ||
} | ||
handleInstallingWorker({ installingWorker, onStateChange }) | ||
} | ||
|
||
// and in the mean time, return null to show 'not installed' | ||
return null | ||
} |