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:
committed by
Facebook GitHub Bot
parent
47a4c10c67
commit
c6d5eb3334
@@ -30,10 +30,10 @@ export type {FlipperServer, FlipperServerCommands, FlipperServerExecOptions};
|
||||
export function createFlipperServer(
|
||||
host: string,
|
||||
port: number,
|
||||
args: string,
|
||||
args: URLSearchParams,
|
||||
onStateChange: (state: FlipperServerState) => void,
|
||||
): Promise<FlipperServer> {
|
||||
const socket = new ReconnectingWebSocket(`ws://${host}:${port}${args}`);
|
||||
const socket = new ReconnectingWebSocket(`ws://${host}:${port}?${args}`);
|
||||
return createFlipperServerWithSocket(socket as WebSocket, onStateChange);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from './openssl-wrapper-with-promises';
|
||||
import path from 'path';
|
||||
import tmp, {FileOptions} from 'tmp';
|
||||
import {reportPlatformFailures} from 'flipper-common';
|
||||
import {FlipperServerConfig, reportPlatformFailures} from 'flipper-common';
|
||||
import {isTest} from 'flipper-common';
|
||||
import {flipperDataFolder} from './paths';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
@@ -265,38 +265,68 @@ const writeToTempFile = async (content: string): Promise<string> => {
|
||||
return path;
|
||||
};
|
||||
|
||||
const getStaticFilePath = (filename: string): string => {
|
||||
return path.resolve(getFlipperServerConfig().paths.staticPath, filename);
|
||||
};
|
||||
|
||||
const tokenFilename = 'auth.token';
|
||||
const getTokenPath = (): string => {
|
||||
const config = getFlipperServerConfig();
|
||||
const getTokenPath = (config: FlipperServerConfig): string => {
|
||||
if (config.environmentInfo.isHeadlessBuild) {
|
||||
return getStaticFilePath(tokenFilename);
|
||||
return path.resolve(config.paths.staticPath, 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 () => {
|
||||
const config = getFlipperServerConfig();
|
||||
|
||||
const privateKey = await fs.readFile(serverKey);
|
||||
const token = jwt.sign({unixname: os.userInfo().username}, privateKey, {
|
||||
algorithm: 'RS256',
|
||||
expiresIn: '21 days',
|
||||
});
|
||||
|
||||
await fs.writeFile(getTokenPath(), token);
|
||||
await fs.writeFile(getTokenPath(config), token);
|
||||
|
||||
if (config.environmentInfo.isHeadlessBuild) {
|
||||
await exportTokenToManifest(config, token);
|
||||
}
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
export const getAuthToken = async () => {
|
||||
if (!(await fs.pathExists(getTokenPath()))) {
|
||||
const config = getFlipperServerConfig();
|
||||
const tokenPath = getTokenPath(config);
|
||||
|
||||
if (!(await fs.pathExists(tokenPath))) {
|
||||
return generateAuthToken();
|
||||
}
|
||||
|
||||
const token = await fs.readFile(getTokenPath());
|
||||
const token = await fs.readFile(tokenPath);
|
||||
return token.toString();
|
||||
};
|
||||
|
||||
|
||||
32
desktop/flipper-server/src/findInstallation.tsx
Normal file
32
desktop/flipper-server/src/findInstallation.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import {isTest} from 'flipper-common';
|
||||
import exitHook from 'exit-hook';
|
||||
import {getAuthToken} from 'flipper-server-core';
|
||||
import {findInstallation} from './findInstallation';
|
||||
|
||||
const argv = yargs
|
||||
.usage('yarn flipper-server [args]')
|
||||
@@ -158,6 +159,8 @@ async function start() {
|
||||
await attachDevServer(app, server, socket, rootPath);
|
||||
}
|
||||
await readyForIncomingConnections(flipperServer, companionEnv);
|
||||
|
||||
return flipperServer;
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
@@ -178,19 +181,28 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||
});
|
||||
|
||||
start()
|
||||
.then(async () => {
|
||||
.then(async (flipperServer) => {
|
||||
if (!argv.tcp) {
|
||||
console.log('Flipper server started and listening');
|
||||
console.log('Flipper server started');
|
||||
return;
|
||||
}
|
||||
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 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)));
|
||||
if (argv.open) {
|
||||
open(url);
|
||||
if (!argv.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argv.bundler) {
|
||||
open(url.toString());
|
||||
} else {
|
||||
const path = await findInstallation(flipperServer);
|
||||
open(path ?? url.toString());
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -11,7 +11,10 @@ import {getLogger, Logger, setLoggerInstance} from 'flipper-common';
|
||||
import {initializeRenderHost} from './initializeRenderHost';
|
||||
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() {
|
||||
// @ts-ignore
|
||||
@@ -27,20 +30,30 @@ async function start() {
|
||||
const logger = createDelegatedLogger();
|
||||
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(
|
||||
location.hostname,
|
||||
parseInt(location.port, 10),
|
||||
location.search,
|
||||
searchParams,
|
||||
(state: FlipperServerState) => {
|
||||
switch (state) {
|
||||
case FlipperServerState.CONNECTING:
|
||||
window.flipperShowError?.('Connecting to flipper-server...');
|
||||
window.flipperShowError?.('Connecting to server...');
|
||||
break;
|
||||
case FlipperServerState.CONNECTED:
|
||||
window?.flipperHideError?.();
|
||||
break;
|
||||
case FlipperServerState.DISCONNECTED:
|
||||
window?.flipperShowError?.('Lost connection to flipper-server');
|
||||
window?.flipperShowError?.('Lost connection to server');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
122
desktop/flipper-ui-core/src/chrome/PWAppInstallationWizard.tsx
Normal file
122
desktop/flipper-ui-core/src/chrome/PWAppInstallationWizard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -33,6 +33,9 @@ import {showChangelog} from '../chrome/ChangelogSheet';
|
||||
import PlatformSelectWizard, {
|
||||
hasPlatformWizardBeenDone,
|
||||
} from '../chrome/PlatformSelectWizard';
|
||||
import PWAInstallationWizard, {
|
||||
shouldShowPWAInstallationWizard,
|
||||
} from '../chrome/PWAppInstallationWizard';
|
||||
import {getVersionString} from '../utils/versionString';
|
||||
import config from '../fb-stubs/config';
|
||||
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);
|
||||
|
||||
// don't warn about logger, even with a new logger we don't want to re-register
|
||||
|
||||
@@ -224,6 +224,9 @@ async function copyStaticResources(outDir: string, versionNumber: string) {
|
||||
'icons.json',
|
||||
'index.web.dev.html',
|
||||
'index.web.html',
|
||||
'manifest.json',
|
||||
'offline.html',
|
||||
'service-worker.js',
|
||||
'style.css',
|
||||
];
|
||||
if (isFB) {
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<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">
|
||||
|
||||
<title>Flipper</title>
|
||||
<script>
|
||||
window.flipperConfig = {
|
||||
@@ -34,38 +39,32 @@
|
||||
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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root">
|
||||
<div id="loading">
|
||||
Loading...
|
||||
Connecting...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="__infinity-dev-box __infinity-dev-box-error" hidden>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// FIXME: needed to make Metro work
|
||||
(async function () {
|
||||
// Line below needed to make Metro work. Alternatives could be considered.
|
||||
window.global = window;
|
||||
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;
|
||||
|
||||
socket.addEventListener('message', ({ data: dataRaw }) => {
|
||||
@@ -89,31 +88,30 @@
|
||||
}
|
||||
})
|
||||
|
||||
socket.addEventListener('error', () => {
|
||||
openError('WebSocket -> error');
|
||||
suppressErrors = true;
|
||||
})
|
||||
socket.addEventListener('error', (e) => {
|
||||
if (!connected) {
|
||||
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) {
|
||||
if (suppressErrors) {
|
||||
return;
|
||||
}
|
||||
|
||||
const box = document.querySelector('.__infinity-dev-box-error');
|
||||
box.removeAttribute('hidden');
|
||||
const box = document.getElementById('loading');
|
||||
box.textContent = text;
|
||||
box.appendChild(closeButton);
|
||||
}
|
||||
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)
|
||||
try {
|
||||
@@ -132,11 +130,31 @@
|
||||
script.src = window.flipperConfig.entryPoint;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<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">
|
||||
|
||||
<title>Flipper</title>
|
||||
<script>
|
||||
window.flipperConfig = {
|
||||
@@ -43,6 +48,7 @@
|
||||
left: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -53,11 +59,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="__infinity-dev-box __infinity-dev-box-error" hidden>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="__infinity-dev-box __infinity-dev-box-error" hidden />
|
||||
<script>
|
||||
(function () {
|
||||
// FIXME: needed to make Metro work
|
||||
@@ -69,10 +71,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const box = document.querySelector('.__infinity-dev-box-error');
|
||||
box.removeAttribute('hidden');
|
||||
box.textContent = text;
|
||||
box.appendChild(closeButton);
|
||||
let box = document.getElementById('loading');
|
||||
if (box) {
|
||||
box.innerText = text;
|
||||
}
|
||||
else {
|
||||
box = document.querySelector('.__infinity-dev-box-error');
|
||||
box.removeAttribute('hidden');
|
||||
box.innerText = text;
|
||||
box.appendChild(closeButton);
|
||||
}
|
||||
}
|
||||
window.flipperShowError = openError;
|
||||
window.flipperHideError = () => {
|
||||
@@ -106,9 +114,28 @@
|
||||
|
||||
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();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
18
desktop/static/manifest.json
Normal file
18
desktop/static/manifest.json
Normal 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
133
desktop/static/offline.html
Normal 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>
|
||||
70
desktop/static/service-worker.js
Normal file
70
desktop/static/service-worker.js
Normal 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;
|
||||
}
|
||||
})());
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user