Last active
March 10, 2020 18:24
-
-
Save khaledosman/de3535c8873831153efdf6c10a4b4080 to your computer and use it in GitHub Desktop.
create-react-app PWA example with epilog to update autogenerated service worker to skipInstall and show notifications using material-ui
This file contains hidden or 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
const axios = require('axios') | |
const ms = require('ms') | |
function cachedGet (url, config, { cacheKey = url, isOnlineFirst = false, dontShowErrorMessage = false, showErrorMessage } = { cacheKey: url, isOnlineFirst: false, dontShowErrorMessage: false, showErrorMessage: () => {} }) { | |
// Get from cache and resolve if the access token is valid for best online performance as well as offline / lie-fi support, but make the call to the network anyway to update the cache for next visit. if there's nothing in the cache, fallback to the network | |
if (isOnlineFirst) { | |
// Online first approach | |
// Get from network then fallback to cache | |
return getFromNetworkAndSaveToCache(url, config, cacheKey) | |
.catch(error => { | |
if (!error.response) { | |
// Network error | |
if (!dontShowErrorMessage && error.message !== 'No cached response found' && error.message !== 'no token found' && error.message !== 'token expired') { | |
showErrorMessage({ message: `Couldn't complete request, please try again later` }) | |
} | |
return getFromCache(url, config, cacheKey) | |
} else { | |
throw error | |
} | |
}) | |
} else { | |
// Offline first approach | |
// Get from cache first, make the request anyway to update the cache then fallback to network | |
return Promise.race([ | |
Promise.all([getFromCache(url, config, cacheKey), isTokenValid()]).then(([p1Res, p2Res]) => p1Res), | |
getFromNetworkAndSaveToCache(url, config, cacheKey) | |
]) | |
.catch(error => { | |
console.warn('error', error) | |
if (!error.response) { // Network error or Results are not in cache | |
if (error.message !== 'No cached response found' && error.message !== 'no token found' && error.message !== 'token expired') { | |
// Network error | |
if (!dontShowErrorMessage) { | |
showErrorMessage({ message: `Couldn't complete request, please try again later` }) | |
} | |
} | |
return getFromNetworkAndSaveToCache(url, config) | |
} else { | |
// let the consumer catch and handle the error | |
throw error | |
} | |
}) | |
} | |
} | |
function isTokenValid () { | |
return new Promise((resolve, reject) => { | |
const authInfo = window.localStorage.getItem('authInfo') | |
const token = window.localStorage.getItem('token') | |
if (token && authInfo) { | |
const { access_token, createdAt, expiresIn } = JSON.parse(authInfo) | |
if (Date.now() >= new Date(createdAt).getTime() + ms(expiresIn)) { | |
reject(new Error('token expired')) | |
} else { | |
resolve(access_token) | |
} | |
} else { | |
reject(new Error('no token found')) | |
} | |
}) | |
} | |
function getFromCache (url, config, cacheKey = url) { | |
return new Promise((resolve, reject) => { | |
const cachedResponse = window.localStorage.getItem(cacheKey) | |
if (cachedResponse) { | |
const response = JSON.parse(cachedResponse) | |
resolve(response) | |
} else { | |
reject(new Error('No cached response found')) | |
} | |
}) | |
} | |
function getFromNetworkAndSaveToCache (url, config, cacheKey = url) { | |
return axios.get(url, { ...config, timeout: 40000 }) | |
// .then(res => res.data) | |
.then(response => { | |
window.localStorage.setItem(cacheKey, JSON.stringify(response)) | |
return response | |
}) | |
} | |
module.exports.cachedGet = cachedGet |
This file contains hidden or 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 hidden or 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
import { openSnackbar } from './components/notifier/Notifier' | |
import Button from '@material-ui/core/Button' | |
import * as serviceWorker from './serviceWorker' | |
serviceWorker.register({ | |
onUpdate: registration => { | |
const onButtonClick = registration => e => { | |
if (registration.waiting) { | |
// When the user asks to refresh the UI, we'll need to reload the window | |
let isRefreshing | |
navigator.serviceWorker.addEventListener('controllerchange', function (event) { | |
// Ensure refresh is only called once. | |
// This works around a bug in "force update on reload". | |
if (isRefreshing) { | |
return | |
} | |
isRefreshing = true | |
console.log('Controller loaded') | |
window.location.reload() | |
}) | |
// Send a message to the new serviceWorker to activate itself | |
registration.waiting.postMessage('skipWaiting') | |
} | |
} | |
openSnackbar({ | |
message: 'A new version of this app is available.', | |
action: <Button color='secondary' size='small' onClick={onButtonClick(registration)}> Load new version </Button> | |
}) | |
}, | |
onSuccess: registration => { | |
openSnackbar({ message: 'This application works offline! Content has been cached for offline usage.' }) | |
}, | |
onOffline: () => { | |
openSnackbar({ message: 'No internet connection available.. The application is running in offline mode!' }) | |
}, | |
onOnline: () => { | |
closeSnackbar() | |
} | |
}) |
This file contains hidden or 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
import React, { useState, useEffect, useCallback } from 'react' | |
import Snackbar from '@material-ui/core/Snackbar' | |
let openSnackbarFn | |
let closeSnackbarFn | |
export const Notifier = React.memo(function Notifier (props) { | |
const [open, setOpen] = useState(false) | |
const [message, setMessage] = useState('') | |
const [action, setAction] = useState('null') | |
const _closeSnackbar = () => { | |
setOpen(false) | |
} | |
const _openSnackbar = ({ message, action }) => { | |
setOpen(true) | |
setMessage(message) | |
setAction(action) | |
} | |
useEffect(() => { | |
openSnackbarFn = _openSnackbar | |
closeSnackbarFn = _closeSnackbar | |
}, []) | |
const handleSnackbarClose = () => { | |
setOpen(false) | |
setMessage('') | |
} | |
return ( | |
<Snackbar | |
anchorOrigin={{ vertical: 'top', horizontal: 'right' }} | |
message={<span | |
id='snackbar-message-id' | |
dangerouslySetInnerHTML={{ __html: message }} | |
/>} | |
action={action} | |
autoHideDuration={20000} | |
onClose={useCallback(handleSnackbarClose)} | |
open={open} | |
SnackbarContentProps={{ | |
'aria-describedby': 'snackbar-message-id' | |
}} | |
/> | |
) | |
}) | |
export function openSnackbar ({ message, action }) { | |
openSnackbarFn({ message, action }) | |
} | |
export function closeSnackbar () { | |
closeSnackbarFn() | |
} |
This file contains hidden or 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
"scripts": { | |
"build": "rm -rf build/ && react-scripts build && npm run-script sw-epilog", | |
"sw-epilog": "cat src/sw-epilog.js >> build/service-worker.js", | |
}, | |
"dependencies": { | |
"@material-ui/core":"" | |
} | |
// app.js | |
<Notifier /> |
This file contains hidden or 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
// Add a listener to receive messages from clients | |
self.addEventListener('message', function(event) { | |
// Force SW upgrade (activation of new installed SW version) | |
if ( event.data === 'skipWaiting' ) { | |
self.skipWaiting(); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment