From bf65da0e72b02e89166bb575db3a2141607e665b Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Tue, 17 Aug 2021 04:43:02 -0700 Subject: [PATCH] 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 --- .../handleOpenPluginDeeplink.node.tsx | 5 +- .../dispatcher/fb-stubs/pluginMarketplace.tsx | 4 + .../dispatcher/handleOpenPluginDeeplink.tsx | 158 +++++++++++++++++- desktop/app/src/utils/pluginUtils.tsx | 38 ++++- 4 files changed, 197 insertions(+), 8 deletions(-) diff --git a/desktop/app/src/dispatcher/__tests__/handleOpenPluginDeeplink.node.tsx b/desktop/app/src/dispatcher/__tests__/handleOpenPluginDeeplink.node.tsx index 41f340c2b..397e56683 100644 --- a/desktop/app/src/dispatcher/__tests__/handleOpenPluginDeeplink.node.tsx +++ b/desktop/app/src/dispatcher/__tests__/handleOpenPluginDeeplink.node.tsx @@ -92,10 +92,7 @@ test('Triggering a deeplink will work', async () => { jest.runAllTimers(); expect(linksSeen).toEqual(['universe']); expect(renderer.baseElement).toMatchInlineSnapshot(` - +
{ // Marketplace is not implemented in public version of Flipper }; diff --git a/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx b/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx index 75aaa3f3a..e0fb80e20 100644 --- a/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx +++ b/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx @@ -10,7 +10,7 @@ import React from 'react'; import {Dialog, getFlipperLib} from 'flipper-plugin'; import {getUser} from '../fb-stubs/user'; -import {Store} from '../reducers/index'; +import {State, Store} from '../reducers/index'; import {checkForUpdate} from '../fb-stubs/checkForUpdate'; import {getAppVersion} from '../utils/info'; import {ACTIVE_SHEET_SIGN_IN, setActiveSheet} from '../reducers/application'; @@ -18,6 +18,12 @@ import {UserNotSignedInError} from '../utils/errors'; import {selectPlugin} from '../reducers/connections'; import {getUpdateAvailableMessage} from '../chrome/UpdateIndicator'; 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 = { pluginId: string; @@ -51,7 +57,9 @@ export async function handleOpenPluginDeeplink(store: Store, query: string) { return; } await verifyFlipperIsUpToDate(title); - // await verifyPluginInstalled(); + if (!(await verifyPluginStatus(store, params.pluginId, title))) { + return; + } // await verifyDevices(); // await verifyClient(); // await verifyPluginEnabled(); @@ -128,9 +136,17 @@ async function showPleaseLoginDialog( } 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 { return new Promise((resolve) => { const unsub = store.subscribe(() => { - if (store.getState().user?.id) { + if (predicate(store.getState())) { unsub(); resolve(); } @@ -139,6 +155,9 @@ async function waitForLogin(store: Store) { } async function verifyFlipperIsUpToDate(title: string) { + if (!isProduction() || isTest()) { + return; + } const currentVersion = getAppVersion(); const handle = Dialog.loading({ title, @@ -173,6 +192,139 @@ async function verifyFlipperIsUpToDate(title: string) { } } +async function verifyPluginStatus( + store: Store, + pluginId: string, + title: string, +): Promise { + // 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: ( +

+ {`To use plugin '${pluginId}', it is necessary to be a member of the GK '${reason}'. Click `} + + here + {' '} + to enroll, restart Flipper, and click the link again. +

+ ), + 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 { + 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) { store.dispatch( selectPlugin({ diff --git a/desktop/app/src/utils/pluginUtils.tsx b/desktop/app/src/utils/pluginUtils.tsx index 7cdf881cf..658ebd4be 100644 --- a/desktop/app/src/utils/pluginUtils.tsx +++ b/desktop/app/src/utils/pluginUtils.tsx @@ -8,7 +8,7 @@ */ 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 BaseDevice from '../server/devices/BaseDevice'; import type Client from '../Client'; @@ -401,3 +401,39 @@ export function computeActivePluginList({ } 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']; +}