Check if plugin status before opening

Summary:
This diff takes care of current plugin status when handling deeplinks. It checks:
1. if the plugin failed to load
2. if the plugin is behind GK
3. if the plugin is installable from bundle
4. if the plugin is installable from marketplace

Reviewed By: passy

Differential Revision: D29875483

fbshipit-source-id: 8dac1aab63822f43a0d002b10efa5b4a756fce41
This commit is contained in:
Michel Weststrate
2021-08-17 04:43:02 -07:00
committed by Facebook GitHub Bot
parent 2cb21f2a06
commit bf65da0e72
4 changed files with 197 additions and 8 deletions

View File

@@ -92,10 +92,7 @@ test('Triggering a deeplink will work', async () => {
jest.runAllTimers(); jest.runAllTimers();
expect(linksSeen).toEqual(['universe']); expect(linksSeen).toEqual(['universe']);
expect(renderer.baseElement).toMatchInlineSnapshot(` expect(renderer.baseElement).toMatchInlineSnapshot(`
<body <body>
class=""
style=""
>
<div> <div>
<div <div
class="css-1x2cmzz-SandySplitContainer e1hsqii10" class="css-1x2cmzz-SandySplitContainer e1hsqii10"

View File

@@ -7,6 +7,10 @@
* @format * @format
*/ */
export async function loadPluginsFromMarketplace() {
// Marketplace is not implemented in public version of Flipper
}
export default () => { export default () => {
// Marketplace is not implemented in public version of Flipper // Marketplace is not implemented in public version of Flipper
}; };

View File

@@ -10,7 +10,7 @@
import React from 'react'; import React from 'react';
import {Dialog, getFlipperLib} from 'flipper-plugin'; import {Dialog, getFlipperLib} from 'flipper-plugin';
import {getUser} from '../fb-stubs/user'; import {getUser} from '../fb-stubs/user';
import {Store} from '../reducers/index'; import {State, Store} from '../reducers/index';
import {checkForUpdate} from '../fb-stubs/checkForUpdate'; import {checkForUpdate} from '../fb-stubs/checkForUpdate';
import {getAppVersion} from '../utils/info'; import {getAppVersion} from '../utils/info';
import {ACTIVE_SHEET_SIGN_IN, setActiveSheet} from '../reducers/application'; import {ACTIVE_SHEET_SIGN_IN, setActiveSheet} from '../reducers/application';
@@ -18,6 +18,12 @@ import {UserNotSignedInError} from '../utils/errors';
import {selectPlugin} from '../reducers/connections'; import {selectPlugin} from '../reducers/connections';
import {getUpdateAvailableMessage} from '../chrome/UpdateIndicator'; import {getUpdateAvailableMessage} from '../chrome/UpdateIndicator';
import {Typography} from 'antd'; import {Typography} from 'antd';
import {getPluginStatus} from '../utils/pluginUtils';
import {loadPluginsFromMarketplace} from './fb-stubs/pluginMarketplace';
import {loadPlugin} from '../reducers/pluginManager';
import {startPluginDownload} from '../reducers/pluginDownloads';
import isProduction, {isTest} from '../utils/isProduction';
import restart from '../utils/restartFlipper';
type OpenPluginParams = { type OpenPluginParams = {
pluginId: string; pluginId: string;
@@ -51,7 +57,9 @@ export async function handleOpenPluginDeeplink(store: Store, query: string) {
return; return;
} }
await verifyFlipperIsUpToDate(title); await verifyFlipperIsUpToDate(title);
// await verifyPluginInstalled(); if (!(await verifyPluginStatus(store, params.pluginId, title))) {
return;
}
// await verifyDevices(); // await verifyDevices();
// await verifyClient(); // await verifyClient();
// await verifyPluginEnabled(); // await verifyPluginEnabled();
@@ -128,9 +136,17 @@ async function showPleaseLoginDialog(
} }
async function waitForLogin(store: Store) { async function waitForLogin(store: Store) {
return waitFor(store, (state) => !!state.user?.id);
}
// make this more reusable?
function waitFor(
store: Store,
predicate: (state: State) => boolean,
): Promise<void> {
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
const unsub = store.subscribe(() => { const unsub = store.subscribe(() => {
if (store.getState().user?.id) { if (predicate(store.getState())) {
unsub(); unsub();
resolve(); resolve();
} }
@@ -139,6 +155,9 @@ async function waitForLogin(store: Store) {
} }
async function verifyFlipperIsUpToDate(title: string) { async function verifyFlipperIsUpToDate(title: string) {
if (!isProduction() || isTest()) {
return;
}
const currentVersion = getAppVersion(); const currentVersion = getAppVersion();
const handle = Dialog.loading({ const handle = Dialog.loading({
title, title,
@@ -173,6 +192,139 @@ async function verifyFlipperIsUpToDate(title: string) {
} }
} }
async function verifyPluginStatus(
store: Store,
pluginId: string,
title: string,
): Promise<boolean> {
// make sure we have marketplace plugin data present
if (!isTest() && !store.getState().plugins.marketplacePlugins.length) {
// plugins not yet fetched
// updates plugins from marketplace (if logged in), and stores them
await loadPluginsFromMarketplace();
}
// while true loop; after pressing install or add GK, we want to check again if plugin is available
while (true) {
const [status, reason] = getPluginStatus(store, pluginId);
switch (status) {
case 'ready':
return true;
case 'unknown':
await Dialog.alert({
type: 'warning',
title,
message: `No plugin with id '${pluginId}' is known to Flipper. Please correct the deeplink, or install the plugin from NPM using the plugin manager.`,
});
return false;
case 'failed':
await Dialog.alert({
type: 'error',
title,
message: `We found plugin '${pluginId}', but failed to load it: ${reason}. Please check the logs for more details`,
});
return false;
case 'gatekeeped':
if (
!(await Dialog.confirm({
title,
message: (
<p>
{`To use plugin '${pluginId}', it is necessary to be a member of the GK '${reason}'. Click `}
<Typography.Link
href={`https://www.internalfb.com/intern/gatekeeper/projects/${reason}`}>
here
</Typography.Link>{' '}
to enroll, restart Flipper, and click the link again.
</p>
),
okText: 'Restart',
onConfirm: async () => {
restart();
// intentionally forever pending, we're restarting...
return new Promise(() => {});
},
}))
) {
return false;
}
break;
case 'bundle_installable': {
// For convenience, don't ask user to install bundled plugins, handle it directly
await installBundledPlugin(store, pluginId, title);
break;
}
case 'marketplace_installable': {
if (!(await installMarketPlacePlugin(store, pluginId, title))) {
return false;
}
break;
}
default:
throw new Error('Unhandled state: ' + status);
}
}
}
async function installBundledPlugin(
store: Store,
pluginId: string,
title: string,
) {
const plugin = store.getState().plugins.bundledPlugins.get(pluginId);
if (!plugin || !plugin.isBundled) {
throw new Error(`Failed to find bundled plugin '${pluginId}'`);
}
const loadingDialog = Dialog.loading({
title,
message: `Loading plugin '${pluginId}'...`,
});
store.dispatch(loadPlugin({plugin, enable: true, notifyIfFailed: true}));
try {
await waitFor(
store,
() => getPluginStatus(store, pluginId)[0] !== 'bundle_installable',
);
} finally {
loadingDialog.close();
}
}
async function installMarketPlacePlugin(
store: Store,
pluginId: string,
title: string,
): Promise<boolean> {
if (
!(await Dialog.confirm({
title,
message: `The requested plugin '${pluginId}' is currently not installed, but can be downloaded from the Flipper plugin Marketplace. If you trust the source of the current link, press 'Install' to continue`,
okText: 'Install',
}))
) {
return false;
}
const plugin = store
.getState()
.plugins.marketplacePlugins.find((p) => p.id === pluginId);
if (!plugin) {
throw new Error(`Failed to find marketplace plugin '${pluginId}'`);
}
const loadingDialog = Dialog.loading({
title,
message: `Installing plugin '${pluginId}'...`,
});
try {
store.dispatch(startPluginDownload({plugin, startedByUser: true}));
await waitFor(
store,
() => getPluginStatus(store, pluginId)[0] !== 'marketplace_installable',
);
} finally {
loadingDialog.close();
}
return true;
}
function openPlugin(store: Store, params: OpenPluginParams) { function openPlugin(store: Store, params: OpenPluginParams) {
store.dispatch( store.dispatch(
selectPlugin({ selectPlugin({

View File

@@ -8,7 +8,7 @@
*/ */
import type {PluginDefinition} from '../plugin'; import type {PluginDefinition} from '../plugin';
import type {State} from '../reducers'; import type {State, Store} from '../reducers';
import type {State as PluginsState} from '../reducers/plugins'; import type {State as PluginsState} from '../reducers/plugins';
import type BaseDevice from '../server/devices/BaseDevice'; import type BaseDevice from '../server/devices/BaseDevice';
import type Client from '../Client'; import type Client from '../Client';
@@ -401,3 +401,39 @@ export function computeActivePluginList({
} }
return pluginList; return pluginList;
} }
export function getPluginStatus(
store: Store,
id: string,
): [
state:
| 'ready'
| 'unknown'
| 'failed'
| 'gatekeeped'
| 'bundle_installable'
| 'marketplace_installable',
reason?: string,
] {
const state: PluginsState = store.getState().plugins;
if (state.devicePlugins.has(id) || state.clientPlugins.has(id)) {
return ['ready'];
}
const gateKeepedDetails = state.gatekeepedPlugins.find((d) => d.id === id);
if (gateKeepedDetails) {
return ['gatekeeped', gateKeepedDetails.gatekeeper];
}
const failedPluginEntry = state.failedPlugins.find(
([details]) => details.id === id,
);
if (failedPluginEntry) {
return ['failed', failedPluginEntry[1]];
}
if (state.bundledPlugins.has(id)) {
return ['bundle_installable'];
}
if (state.marketplacePlugins.find((d) => d.id === id)) {
return ['marketplace_installable'];
}
return ['unknown'];
}