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(
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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 {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) => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
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, {
|
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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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