Flipper as PWA

Summary:
^

Reference: https://docs.google.com/document/d/1flQJUzTe4AuQz3QCpvbloQycenHsu7ZxbKScov7K7ao

Reviewed By: passy

Differential Revision: D45693382

fbshipit-source-id: 5a2e6c213a7e7e2cf9cd5f3033cff3e5291a2a92
This commit is contained in:
Lorenzo Blasa
2023-05-16 04:32:47 -07:00
committed by Facebook GitHub Bot
parent 47a4c10c67
commit c6d5eb3334
13 changed files with 556 additions and 70 deletions

View File

@@ -30,10 +30,10 @@ export type {FlipperServer, FlipperServerCommands, FlipperServerExecOptions};
export function createFlipperServer( export function createFlipperServer(
host: string, host: string,
port: number, port: number,
args: string, args: URLSearchParams,
onStateChange: (state: FlipperServerState) => void, onStateChange: (state: FlipperServerState) => void,
): Promise<FlipperServer> { ): Promise<FlipperServer> {
const socket = new ReconnectingWebSocket(`ws://${host}:${port}${args}`); const socket = new ReconnectingWebSocket(`ws://${host}:${port}?${args}`);
return createFlipperServerWithSocket(socket as WebSocket, onStateChange); return createFlipperServerWithSocket(socket as WebSocket, onStateChange);
} }

View File

@@ -16,7 +16,7 @@ import {
} from './openssl-wrapper-with-promises'; } from './openssl-wrapper-with-promises';
import path from 'path'; import path from 'path';
import tmp, {FileOptions} from 'tmp'; import tmp, {FileOptions} from 'tmp';
import {reportPlatformFailures} from 'flipper-common'; import {FlipperServerConfig, reportPlatformFailures} from 'flipper-common';
import {isTest} from 'flipper-common'; import {isTest} from 'flipper-common';
import {flipperDataFolder} from './paths'; import {flipperDataFolder} from './paths';
import * as jwt from 'jsonwebtoken'; import * as jwt from 'jsonwebtoken';
@@ -265,38 +265,68 @@ const writeToTempFile = async (content: string): Promise<string> => {
return path; return path;
}; };
const getStaticFilePath = (filename: string): string => {
return path.resolve(getFlipperServerConfig().paths.staticPath, filename);
};
const tokenFilename = 'auth.token'; const tokenFilename = 'auth.token';
const getTokenPath = (): string => { const getTokenPath = (config: FlipperServerConfig): string => {
const config = getFlipperServerConfig();
if (config.environmentInfo.isHeadlessBuild) { if (config.environmentInfo.isHeadlessBuild) {
return getStaticFilePath(tokenFilename); return path.resolve(config.paths.staticPath, tokenFilename);
} }
return getFilePath(tokenFilename); return getFilePath(tokenFilename);
}; };
const manifestFilename = 'manifest.json';
const getManifestPath = (config: FlipperServerConfig): string => {
return path.resolve(config.paths.staticPath, manifestFilename);
};
const exportTokenToManifest = async (
config: FlipperServerConfig,
token: string,
) => {
const manifestPath = getManifestPath(config);
try {
const manifestData = await fs.readFile(manifestPath, {
encoding: 'utf-8',
});
const manifest = JSON.parse(manifestData);
manifest.token = token;
const newManifestData = JSON.stringify(manifest, null, 4);
await fs.writeFile(manifestPath, newManifestData);
} catch (e) {
console.error(
'Unable to export authentication token to manifest, may be non existent.',
);
}
};
export const generateAuthToken = async () => { export const generateAuthToken = async () => {
const config = getFlipperServerConfig();
const privateKey = await fs.readFile(serverKey); const privateKey = await fs.readFile(serverKey);
const token = jwt.sign({unixname: os.userInfo().username}, privateKey, { const token = jwt.sign({unixname: os.userInfo().username}, privateKey, {
algorithm: 'RS256', algorithm: 'RS256',
expiresIn: '21 days', expiresIn: '21 days',
}); });
await fs.writeFile(getTokenPath(), token); await fs.writeFile(getTokenPath(config), token);
if (config.environmentInfo.isHeadlessBuild) {
await exportTokenToManifest(config, token);
}
return token; return token;
}; };
export const getAuthToken = async () => { export const getAuthToken = async () => {
if (!(await fs.pathExists(getTokenPath()))) { const config = getFlipperServerConfig();
const tokenPath = getTokenPath(config);
if (!(await fs.pathExists(tokenPath))) {
return generateAuthToken(); return generateAuthToken();
} }
const token = await fs.readFile(getTokenPath()); const token = await fs.readFile(tokenPath);
return token.toString(); return token.toString();
}; };

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {FlipperServerImpl} from 'flipper-server-core';
import path from 'path';
import fs from 'fs-extra';
import os from 'os';
export async function findInstallation(
server: FlipperServerImpl,
): Promise<string | undefined> {
if (server.config.environmentInfo.os.platform !== 'darwin') {
return;
}
const appPath = path.join(
os.homedir(),
'Applications',
'Chrome Apps.localized',
'Flipper.app',
);
const appPlistPath = path.join(appPath, 'Contents', 'Info.plist');
if (await fs.pathExists(appPlistPath)) {
return appPath;
}
}

View File

@@ -26,6 +26,7 @@ import {
import {isTest} from 'flipper-common'; import {isTest} from 'flipper-common';
import exitHook from 'exit-hook'; import exitHook from 'exit-hook';
import {getAuthToken} from 'flipper-server-core'; import {getAuthToken} from 'flipper-server-core';
import {findInstallation} from './findInstallation';
const argv = yargs const argv = yargs
.usage('yarn flipper-server [args]') .usage('yarn flipper-server [args]')
@@ -158,6 +159,8 @@ async function start() {
await attachDevServer(app, server, socket, rootPath); await attachDevServer(app, server, socket, rootPath);
} }
await readyForIncomingConnections(flipperServer, companionEnv); await readyForIncomingConnections(flipperServer, companionEnv);
return flipperServer;
} }
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
@@ -178,19 +181,28 @@ process.on('unhandledRejection', (reason, promise) => {
}); });
start() start()
.then(async () => { .then(async (flipperServer) => {
if (!argv.tcp) { if (!argv.tcp) {
console.log('Flipper server started and listening'); console.log('Flipper server started');
return; return;
} }
console.log( console.log(
'Flipper server started and listening at port ' + chalk.green(argv.port), 'Flipper server started and is listening at port ' +
chalk.green(argv.port),
); );
const token = await getAuthToken(); const token = await getAuthToken();
const url = `http://localhost:${argv.port}?token=${token}`; const searchParams = new URLSearchParams({token: token ?? ''});
const url = new URL(`http://localhost:${argv.port}?${searchParams}`);
console.log('Go to: ' + chalk.green(chalk.bold(url))); console.log('Go to: ' + chalk.green(chalk.bold(url)));
if (argv.open) { if (!argv.open) {
open(url); return;
}
if (argv.bundler) {
open(url.toString());
} else {
const path = await findInstallation(flipperServer);
open(path ?? url.toString());
} }
}) })
.catch((e) => { .catch((e) => {

View File

@@ -11,7 +11,10 @@ import {getLogger, Logger, setLoggerInstance} from 'flipper-common';
import {initializeRenderHost} from './initializeRenderHost'; import {initializeRenderHost} from './initializeRenderHost';
import {createFlipperServer, FlipperServerState} from 'flipper-server-client'; import {createFlipperServer, FlipperServerState} from 'flipper-server-client';
document.getElementById('root')!.innerText = 'flipper-ui-browser started'; const loadingContainer = document.getElementById('loading');
if (loadingContainer) {
loadingContainer.innerText = 'Loading...';
}
async function start() { async function start() {
// @ts-ignore // @ts-ignore
@@ -27,20 +30,30 @@ async function start() {
const logger = createDelegatedLogger(); const logger = createDelegatedLogger();
setLoggerInstance(logger); setLoggerInstance(logger);
const params = new URL(location.href).searchParams;
let token = params.get('token');
if (!token) {
const manifestResponse = await fetch('manifest.json');
const manifest = await manifestResponse.json();
token = manifest.token;
}
const searchParams = new URLSearchParams({token: token ?? ''});
const flipperServer = await createFlipperServer( const flipperServer = await createFlipperServer(
location.hostname, location.hostname,
parseInt(location.port, 10), parseInt(location.port, 10),
location.search, searchParams,
(state: FlipperServerState) => { (state: FlipperServerState) => {
switch (state) { switch (state) {
case FlipperServerState.CONNECTING: case FlipperServerState.CONNECTING:
window.flipperShowError?.('Connecting to flipper-server...'); window.flipperShowError?.('Connecting to server...');
break; break;
case FlipperServerState.CONNECTED: case FlipperServerState.CONNECTED:
window?.flipperHideError?.(); window?.flipperHideError?.();
break; break;
case FlipperServerState.DISCONNECTED: case FlipperServerState.DISCONNECTED:
window?.flipperShowError?.('Lost connection to flipper-server'); window?.flipperShowError?.('Lost connection to server');
break; break;
} }
}, },

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React from 'react';
import {Modal, Button} from 'antd';
import {Layout, _NuxManagerContext} from 'flipper-plugin';
type Props = {
onHide: () => void;
};
const lastShownTimestampKey = 'flipper-pwa-wizard-last-shown-timestamp';
export function shouldShowPWAInstallationWizard(): boolean {
if (window.matchMedia('(display-mode: standalone)').matches) {
return false;
}
let lastShownTimestampFromStorage = undefined;
try {
lastShownTimestampFromStorage = window.localStorage.getItem(
lastShownTimestampKey,
);
} catch (e) {}
if (lastShownTimestampFromStorage) {
const withinOneDay = (timestamp: number) => {
const Day = 1 * 24 * 60 * 60 * 1000;
const DayAgo = Date.now() - Day;
return timestamp > DayAgo;
};
const lastShownTimestamp = Number(lastShownTimestampFromStorage);
return !withinOneDay(lastShownTimestamp);
}
const lastShownTimestamp = Date.now();
try {
window.localStorage.setItem(
lastShownTimestampKey,
String(lastShownTimestamp),
);
} catch (e) {}
return true;
}
async function install(event: any) {
event.prompt();
(event.userChoice as any)
.then((choiceResult: any) => {
if (choiceResult.outcome === 'accepted') {
console.log('PWA installation, user accepted the prompt.');
} else {
console.log('PWA installation, user dismissed the prompt.');
}
(globalThis as any).PWAppInstallationEvent = null;
})
.catch((e: Error) => {
console.error('PWA failed to install with error', e);
});
}
export default function PWAInstallationWizard(props: Props) {
const contents = (
<Layout.Container gap>
<Layout.Container style={{width: '100%', paddingBottom: 15}}>
<>
Please install Flipper as a PWA. Installed Progressive Web Apps run in
a standalone window instead of a browser tab. They're launchable from
your home screen, dock, taskbar, or shelf. It's possible to search for
and jump between them with the app switcher, making them feel like
part of the device they're installed on. New capabilities open up
after a web app is installed. Keyboard shortcuts, usually reserved
when running in the browser, become available too.
</>
</Layout.Container>
</Layout.Container>
);
const footer = (
<>
<Button
type="ghost"
onClick={async () => {
props.onHide();
}}>
Not now
</Button>
<Button
type="primary"
onClick={async () => {
const installEvent = (globalThis as any).PWAppInstallationEvent;
if (installEvent) {
await install(installEvent).then(props.onHide);
}
}}>
Install
</Button>
</>
);
return (
<Modal
visible
centered
onCancel={() => {
props.onHide();
}}
width={570}
title="Install Flipper to Desktop"
footer={footer}>
{contents}
</Modal>
);
}

View File

@@ -33,6 +33,9 @@ import {showChangelog} from '../chrome/ChangelogSheet';
import PlatformSelectWizard, { import PlatformSelectWizard, {
hasPlatformWizardBeenDone, hasPlatformWizardBeenDone,
} from '../chrome/PlatformSelectWizard'; } from '../chrome/PlatformSelectWizard';
import PWAInstallationWizard, {
shouldShowPWAInstallationWizard,
} from '../chrome/PWAppInstallationWizard';
import {getVersionString} from '../utils/versionString'; import {getVersionString} from '../utils/versionString';
import config from '../fb-stubs/config'; import config from '../fb-stubs/config';
import {WelcomeScreenStaticView} from './WelcomeScreen'; import {WelcomeScreenStaticView} from './WelcomeScreen';
@@ -110,6 +113,11 @@ export function SandyApp() {
)); ));
} }
if (shouldShowPWAInstallationWizard()) {
console.info('Attempt to install PWA, launch installation wizard.');
Dialog.showModal((onHide) => <PWAInstallationWizard onHide={onHide} />);
}
showChangelog(true); showChangelog(true);
// don't warn about logger, even with a new logger we don't want to re-register // don't warn about logger, even with a new logger we don't want to re-register

View File

@@ -224,6 +224,9 @@ async function copyStaticResources(outDir: string, versionNumber: string) {
'icons.json', 'icons.json',
'index.web.dev.html', 'index.web.dev.html',
'index.web.html', 'index.web.html',
'manifest.json',
'offline.html',
'service-worker.js',
'style.css', 'style.css',
]; ];
if (isFB) { if (isFB) {

View File

@@ -4,9 +4,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<link rel="icon" href="icon.png"> <link rel="icon" href="icon.png">
<link rel="apple-touch-icon" href="/icon.png">
<link rel="stylesheet" href="style.css">
<link rel="manifest" href="manifest.json">
<link id="flipper-theme-import" rel="stylesheet"> <link id="flipper-theme-import" rel="stylesheet">
<title>Flipper</title> <title>Flipper</title>
<script> <script>
window.flipperConfig = { window.flipperConfig = {
@@ -34,38 +39,32 @@
text-align: center; text-align: center;
} }
.__infinity-dev-box-error {
background-color: red;
font-family: monospace;
font-size: 12px;
z-index: 10;
bottom: 0;
left: 0;
position: absolute;
}
</style> </style>
</head> </head>
<body> <body>
<div id="root"> <div id="root">
<div id="loading"> <div id="loading">
Loading... Connecting...
</div> </div>
</div> </div>
<div class="__infinity-dev-box __infinity-dev-box-error" hidden>
</div>
<script> <script>
(function () { (async function () {
// FIXME: needed to make Metro work // Line below needed to make Metro work. Alternatives could be considered.
window.global = window; window.global = window;
let suppressErrors = false; let suppressErrors = false;
let connected = false;
const socket = new WebSocket(`ws://${location.host}${location.search}`); const params = new URL(location.href).searchParams;
let token = params.get('token');
if (!token) {
const manifestResponse = await fetch('manifest.json');
const manifest = await manifestResponse.json();
token = manifest.token;
}
const socket = new WebSocket(`ws://${location.host}?token=${token}`);
window.devSocket = socket; window.devSocket = socket;
socket.addEventListener('message', ({ data: dataRaw }) => { socket.addEventListener('message', ({ data: dataRaw }) => {
@@ -89,31 +88,30 @@
} }
}) })
socket.addEventListener('error', () => { socket.addEventListener('error', (e) => {
openError('WebSocket -> error'); if (!connected) {
suppressErrors = true; openError('Socket failed to connect. Is the server running? Have you provided a valid authentication token?');
}) }
else {
openError('Socket failed with error.');
}
suppressErrors = true;
});
socket.addEventListener('open', () => {
connected = true;
})
function openError(text) { function openError(text) {
if (suppressErrors) { if (suppressErrors) {
return; return;
} }
const box = document.querySelector('.__infinity-dev-box-error'); const box = document.getElementById('loading');
box.removeAttribute('hidden');
box.textContent = text; box.textContent = text;
box.appendChild(closeButton);
} }
window.flipperShowError = openError; window.flipperShowError = openError;
window.flipperHideError = () => {
const box = document.querySelector('.__infinity-dev-box-error');
box.setAttribute('hidden', true);
}
const closeButton = document.createElement('button');
closeButton.addEventListener('click', window.flipperHideError);
closeButton.textContent = 'X';
// load correct theme (n.b. this doesn't handle system value specifically, will assume light in such cases) // load correct theme (n.b. this doesn't handle system value specifically, will assume light in such cases)
try { try {
@@ -132,11 +130,31 @@
script.src = window.flipperConfig.entryPoint; script.src = window.flipperConfig.entryPoint;
script.onerror = (e) => { script.onerror = (e) => {
openError('Script failure. Check Chrome console for more info.'); openError('Failed to load entry point. Check Chrome console for more info.');
}; };
document.body.appendChild(script); document.body.appendChild(script);
} }
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('Flipper Service Worker has been registered');
})
.catch((e) => {
console.error('Flipper failed to register Service Worker', e);
});
}
window.addEventListener('beforeinstallprompt', (e) => {
console.log('Flipper PWA before install prompt with event', e);
// Prevent Chrome 67 and earlier from automatically showing the prompt.
e.preventDefault();
// Stash the event so it can be triggered later.
global.PWAppInstallationEvent = e;
});
init(); init();
})(); })();
</script> </script>

View File

@@ -4,9 +4,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<link rel="icon" href="icon.png"> <link rel="icon" href="icon.png">
<link rel="apple-touch-icon" href="/icon.png">
<link rel="stylesheet" href="style.css">
<link rel="manifest" href="manifest.json">
<link id="flipper-theme-import" rel="stylesheet"> <link id="flipper-theme-import" rel="stylesheet">
<title>Flipper</title> <title>Flipper</title>
<script> <script>
window.flipperConfig = { window.flipperConfig = {
@@ -43,6 +48,7 @@
left: 0; left: 0;
position: absolute; position: absolute;
} }
</style> </style>
</head> </head>
@@ -53,11 +59,7 @@
</div> </div>
</div> </div>
<div class="__infinity-dev-box __infinity-dev-box-error" hidden />
<div class="__infinity-dev-box __infinity-dev-box-error" hidden>
</div>
<script> <script>
(function () { (function () {
// FIXME: needed to make Metro work // FIXME: needed to make Metro work
@@ -69,11 +71,17 @@
return; return;
} }
const box = document.querySelector('.__infinity-dev-box-error'); let box = document.getElementById('loading');
if (box) {
box.innerText = text;
}
else {
box = document.querySelector('.__infinity-dev-box-error');
box.removeAttribute('hidden'); box.removeAttribute('hidden');
box.textContent = text; box.innerText = text;
box.appendChild(closeButton); box.appendChild(closeButton);
} }
}
window.flipperShowError = openError; window.flipperShowError = openError;
window.flipperHideError = () => { window.flipperHideError = () => {
const box = document.querySelector('.__infinity-dev-box-error'); const box = document.querySelector('.__infinity-dev-box-error');
@@ -106,6 +114,25 @@
document.body.appendChild(script); document.body.appendChild(script);
} }
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/service-worker.js')
.then(() => {
console.log('Flipper Service Worker has been registered');
})
.catch((e) => {
console.error('Flipper failed to register Service Worker', e);
});
}
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt.
e.preventDefault();
// Stash the event so it can be triggered later.
global.PWAppInstallationEvent = e;
});
init(); init();
})(); })();
</script> </script>

View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"name": "Flipper",
"description": "Flipper is a platform for debugging iOS, Android and React Native apps",
"short_name": "Flipper",
"start_url": "index.web.html",
"display": "standalone",
"background_color": "#fff",
"theme_color": "#4267B2",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icon.png",
"type": "image/png",
"sizes": "256x256"
}
]
}

133
desktop/static/offline.html Normal file
View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>You are offline</title>
<!-- Inline the page's stylesheet. -->
<style>
body {
font-family: system-ui;
font-size: 13px;
cursor: default;
overflow: hidden;
line-height: 1;
}
#container {
-webkit-app-region: drag;
z-index: 999999;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 50px;
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
color: #525252;
text-align: center;
}
.console {
font-family: 'Fira Mono';
width: 600px;
height: 250px;
box-sizing: border-box;
margin: auto;
}
.console header {
border-top-left-radius: 15px;
border-top-right-radius: 15px;
background-color: #4267B2;
height: 45px;
line-height: 45px;
text-align: center;
color: #DDD;
}
.console .consolebody {
border-bottom-left-radius: 15px;
border-bottom-right-radius: 15px;
box-sizing: border-box;
padding: 0px 10px;
height: calc(100% - 40px);
overflow: scroll;
background-color: #000;
color: white;
text-align: left;
}
</style>
</head>
<body>
<div id="container">
<div>
<b>Oops! It seems Flipper is not running in the background</b>
<p>
Flipper will automatically reload once the connection is re-established.
Click the button below to try reloading manually.
</p>
<button type="button">⤾ Reload</button>
<p>Also, you can manually start Flipper.
From the terminal, please run:</p>
<div class="console">
<header>
<p>shell</p>
</header>
<div class="consolebody">
<p>> open -a 'Flipper' --args '--server'</p>
</div>
</div>
</div>
</div>
<script>
document.querySelector('button').addEventListener('click', () => {
window.location.reload();
});
// Listen to changes in the network state, reload when online.
// This handles the case when the device is completely offline
// i.e. no network connection.
window.addEventListener('online', () => {
window.location.reload();
});
// Check if the server is responding & reload the page if it is.
// This handles the case when the device is online, but the server
// is offline or misbehaving.
async function checkNetworkAndReload() {
try {
const response = await fetch('.');
if (response.status >= 200 && response.status < 500) {
window.location.reload();
return;
}
} catch {
// Unable to connect to the server, ignore.
}
window.setTimeout(checkNetworkAndReload, 2500);
}
checkNetworkAndReload();
</script>
</body>
</html>

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// OFFLINE_VERSION is used as an update marker so that on subsequent installations
// the newer version of the file gets updated.
// eslint-disable-next-line header/header, no-unused-vars
const OFFLINE_VERSION = 1;
const CACHE_NAME = 'offline';
const OFFLINE_URL = 'offline.html';
self.addEventListener('install', (event) => {
console.log('Flipper service worker installed');
event.waitUntil((async () => {
const cache = await caches.open(CACHE_NAME);
// Setting {cache: 'reload'} in the new request will ensure that the response
// isn't fulfilled from the HTTP cache; i.e., it will be from the network.
await cache.add(new Request(OFFLINE_URL, {cache: 'reload'}));
})());
// Force the waiting service worker to become the active service worker.
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
console.log('Flipper service worker activate');
event.waitUntil((async () => {
// Enable navigation preload if it's supported.
// See https://developers.google.com/web/updates/2017/02/navigation-preload
if ('navigationPreload' in self.registration) {
await self.registration.navigationPreload.enable();
}
})());
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
if (event.request.mode === 'navigate') {
event.respondWith((async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// Always try the network first (try flipper server)
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// Catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log('Fetch failed; returning offline page instead.', error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})());
}
});