From 71db11b6ac669cdf237b1ead6cafa30d8343d836 Mon Sep 17 00:00:00 2001 From: Jordan Frankfurt Date: Thu, 18 Feb 2021 13:10:53 -0500 Subject: [PATCH] Revert "Revert "feature(service worker): add offline support (#1319)" (#1320)" (#1321) This reverts commit db3328c8d9d7ea7b7cf7e75dc658714798d007a4. --- package.json | 10 ++- public/manifest.json | 12 +-- src/index.tsx | 3 + src/service-worker.ts | 80 ++++++++++++++++++ src/serviceWorkerRegistration.ts | 139 +++++++++++++++++++++++++++++++ yarn.lock | 37 +++++++- 6 files changed, 273 insertions(+), 8 deletions(-) create mode 100644 src/service-worker.ts create mode 100644 src/serviceWorkerRegistration.ts diff --git a/package.json b/package.json index d426ee1915..6129850821 100644 --- a/package.json +++ b/package.json @@ -82,18 +82,24 @@ "react-window": "^1.8.5", "rebass": "^4.0.7", "redux-localstorage-simple": "^2.3.1", - "serve": "^11.3.0", + "serve": "^11.3.2", "start-server-and-test": "^1.11.0", "styled-components": "^4.2.0", "typescript": "^3.8.3", "use-count-up": "^2.2.5", - "wcag-contrast": "^3.0.0" + "wcag-contrast": "^3.0.0", + "workbox-core": "^6.1.0", + "workbox-expiration": "^6.1.0", + "workbox-precaching": "^6.1.0", + "workbox-routing": "^6.1.0", + "workbox-strategies": "^6.1.0" }, "resolutions": { "@walletconnect/web3-provider": "1.1.1-alpha.0" }, "scripts": { "start": "react-scripts start", + "start:service-worker": "yarn build && yarn serve -s build", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject", diff --git a/public/manifest.json b/public/manifest.json index 92811ec2f9..5489752e0d 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,7 @@ { - "short_name": "Uniswap", - "name": "Uniswap", + "background_color": "#fff", + "display": "standalone", + "homepage_url": "https://app.uniswap.org", "icons": [ { "src": "./images/192x192_App_Icon.png", @@ -16,7 +17,8 @@ } ], "orientation": "portrait", - "display": "standalone", - "theme_color": "#ff007a", - "background_color": "#fff" + "name": "Uniswap", + "short_name": "Uniswap", + "start_url": ".", + "theme_color": "#ff007a" } diff --git a/src/index.tsx b/src/index.tsx index b7f4450ae1..bff0050d1b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,6 +11,7 @@ import { NetworkContextName } from './constants' import './i18n' import App from './pages/App' import store from './state' +import * as serviceWorkerRegistration from './serviceWorkerRegistration' import ApplicationUpdater from './state/application/updater' import ListsUpdater from './state/lists/updater' import MulticallUpdater from './state/multicall/updater' @@ -75,3 +76,5 @@ ReactDOM.render( , document.getElementById('root') ) + +serviceWorkerRegistration.register() diff --git a/src/service-worker.ts b/src/service-worker.ts new file mode 100644 index 0000000000..1e63b1c1c7 --- /dev/null +++ b/src/service-worker.ts @@ -0,0 +1,80 @@ +/// +/* eslint-disable no-restricted-globals */ + +// This service worker can be customized! +// See https://developers.google.com/web/tools/workbox/modules +// for the list of available Workbox modules, or add any other +// code you'd like. +// You can also remove this file if you'd prefer not to use a +// service worker, and the Workbox build step will be skipped. + +import { clientsClaim } from 'workbox-core' +import { ExpirationPlugin } from 'workbox-expiration' +import { createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' +import { registerRoute } from 'workbox-routing' +import { StaleWhileRevalidate } from 'workbox-strategies' + +declare const self: ServiceWorkerGlobalScope + +clientsClaim() + +// Precache all of the assets generated by your build process. +// Their URLs are injected into the manifest variable below. +// This variable must be present somewhere in your service worker file, +// even if you decide not to use precaching. See https://cra.link/PWA +precacheAndRoute(self.__WB_MANIFEST) + +// Set up App Shell-style routing, so that all navigation requests +// are fulfilled with your index.html shell. Learn more at +// https://developers.google.com/web/fundamentals/architecture/app-shell +const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$') +registerRoute( + // Return false to exempt requests from being fulfilled by index.html. + ({ request, url }: { request: Request; url: URL }) => { + // If this isn't a navigation, skip. + if (request.mode !== 'navigate') { + return false + } + + // If this is a URL that starts with /_, skip. + if (url.pathname.startsWith('/_')) { + return false + } + + // If this looks like a URL for a resource, because it contains + // a file extension, skip. + if (url.pathname.match(fileExtensionRegexp)) { + return false + } + + // Return true to signal that we want to use the handler. + return true + }, + createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') +) + +// An example runtime caching route for requests that aren't handled by the +// precache, in this case same-origin .png requests like those from in public/ +registerRoute( + // Add in any other file extensions or routing criteria as needed. + ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), + // Customize this strategy as needed, e.g., by changing to CacheFirst. + new StaleWhileRevalidate({ + cacheName: 'images', + plugins: [ + // Ensure that once this runtime cache reaches a maximum size the + // least-recently used images are removed. + new ExpirationPlugin({ maxEntries: 50 }) + ] + }) +) + +// This allows the web app to trigger skipWaiting via +// registration.waiting.postMessage({type: 'SKIP_WAITING'}) +self.addEventListener('message', event => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } +}) + +// Any other custom service worker logic can go here. diff --git a/src/serviceWorkerRegistration.ts b/src/serviceWorkerRegistration.ts new file mode 100644 index 0000000000..b0e6312a08 --- /dev/null +++ b/src/serviceWorkerRegistration.ts @@ -0,0 +1,139 @@ +// This optional code is used to register a service worker. +// register() is not called by default. + +// This lets the app load faster on subsequent visits in production, and gives +// it offline capabilities. However, it also means that developers (and users) +// will only see deployed updates on subsequent visits to a page, after all the +// existing tabs open on the page have been closed, since previously cached +// resources are updated in the background. + +// To learn more about the benefits of this model and instructions on how to +// opt-in, read https://cra.link/PWA + +const isLocalhost = Boolean( + window.location.hostname === 'localhost' || + // [::1] is the IPv6 localhost address. + window.location.hostname === '[::1]' || + // 127.0.0.0/8 are considered localhost for IPv4. + window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) +) + +type Config = { + onSuccess?: (registration: ServiceWorkerRegistration) => void + onUpdate?: (registration: ServiceWorkerRegistration) => void +} + +function registerValidSW(swUrl: string, config?: Config) { + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing + if (installingWorker == null) { + return + } + installingWorker.onstatechange = () => { + if (installingWorker.state === 'installed') { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + 'New content is available and will be used when all ' + + 'tabs for this page are closed. See https://cra.link/PWA.' + ) + + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration) + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log('Content is cached for offline use.') + + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration) + } + } + } + } + } + }) + .catch(error => { + console.error('Error during service worker registration:', error) + }) +} + +function checkValidServiceWorker(swUrl: string, config?: Config) { + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl, { + headers: { 'Service-Worker': 'script' } + }) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get('content-type') + if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload() + }) + }) + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config) + } + }) + .catch(() => { + console.log('No internet connection found. App is running in offline mode.') + }) +} + +export function register(config?: Config) { + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return + } + + window.addEventListener('load', () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` + + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config) + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + 'This web app is being served cache-first by a service ' + + 'worker. To learn more, visit https://cra.link/PWA' + ) + }) + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config) + } + }) + } +} + +export function unregister() { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.ready + .then(registration => { + registration.unregister() + }) + .catch(error => { + console.error(error.message) + }) + } +} diff --git a/yarn.lock b/yarn.lock index f9445ec302..1622c46057 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13762,7 +13762,7 @@ serve-static@1.14.1: parseurl "~1.3.3" send "0.17.1" -serve@^11.3.0: +serve@^11.3.2: version "11.3.2" resolved "https://registry.yarnpkg.com/serve/-/serve-11.3.2.tgz#b905e980616feecd170e51c8f979a7b2374098f5" integrity sha512-yKWQfI3xbj/f7X1lTBg91fXBP0FqjJ4TEi+ilES5yzH0iKJpN5LjNb1YzIfQg9Rqn4ECUS2SOf2+Kmepogoa5w== @@ -15655,6 +15655,11 @@ workbox-core@^4.3.1: resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-4.3.1.tgz#005d2c6a06a171437afd6ca2904a5727ecd73be6" integrity sha512-I3C9jlLmMKPxAC1t0ExCq+QoAMd0vAAHULEgRZ7kieCdUd919n53WC0AfvokHNwqRhGn+tIIj7vcb5duCjs2Kg== +workbox-core@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-6.1.0.tgz#2671b64f76550e83a4c2202676b67ce372e10881" + integrity sha512-s3KqTJfBreO4xCZpR2LB5p/EknAx8eg0QumKiIgxM4hRO0RtwS2pJvTieNEM23X3RqxRhqweriLD8To19KUvjg== + workbox-expiration@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-4.3.1.tgz#d790433562029e56837f341d7f553c4a78ebe921" @@ -15662,6 +15667,13 @@ workbox-expiration@^4.3.1: dependencies: workbox-core "^4.3.1" +workbox-expiration@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-6.1.0.tgz#cf6bb384e49d0c92b79233c46671d9c6d82478a2" + integrity sha512-jp2xGk+LC4AhCoOxO/bC06GQkq/oVp0ZIf1zXLQh6OD2fWZPkXNjLLSuDnjXoGGPibYrq7gEE/xjAdYGjNWl1A== + dependencies: + workbox-core "^6.1.0" + workbox-google-analytics@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-4.3.1.tgz#9eda0183b103890b5c256e6f4ea15a1f1548519a" @@ -15686,6 +15698,15 @@ workbox-precaching@^4.3.1: dependencies: workbox-core "^4.3.1" +workbox-precaching@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-6.1.0.tgz#9ee3d28f27cd78daa62f5bd6a0d33f5682ac97a7" + integrity sha512-zjye8MVzieBVJ3sS0hFcbKLp7pTHMfJM17YqxCxB0KykXWnxLOpYnStQ9M+bjWJsKJOQvbkPqvq5u9+mtA923g== + dependencies: + workbox-core "^6.1.0" + workbox-routing "^6.1.0" + workbox-strategies "^6.1.0" + workbox-range-requests@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-4.3.1.tgz#f8a470188922145cbf0c09a9a2d5e35645244e74" @@ -15700,6 +15721,13 @@ workbox-routing@^4.3.1: dependencies: workbox-core "^4.3.1" +workbox-routing@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-6.1.0.tgz#f885cb7801e2c9c5678f197656cf27a2b649c1d5" + integrity sha512-FXQ5cwb6Mk90fC0rfQLX0pN+r/N4eBafwkh/QanJUq0e6jMPdDFLrlsikZL/0LcXEx+yAkWLytoiS+d2HOEBOw== + dependencies: + workbox-core "^6.1.0" + workbox-strategies@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-4.3.1.tgz#d2be03c4ef214c115e1ab29c9c759c9fe3e9e646" @@ -15707,6 +15735,13 @@ workbox-strategies@^4.3.1: dependencies: workbox-core "^4.3.1" +workbox-strategies@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-6.1.0.tgz#9ddcee44408d2fb403f22a7989803b5c58560590" + integrity sha512-HvUknzJdZWeV3x7Eq33a7TGAv9/r1TEiQK6kQ1QNzN+IKiqhIjnhKFHmMxb5hK1Gw9/aDSJTLNPDaLPfIJRQFQ== + dependencies: + workbox-core "^6.1.0" + workbox-streams@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-4.3.1.tgz#0b57da70e982572de09c8742dd0cb40a6b7c2cc3"