diff --git a/desktop/flipper-common/src/settings.tsx b/desktop/flipper-common/src/settings.tsx index 863238281..89046449d 100644 --- a/desktop/flipper-common/src/settings.tsx +++ b/desktop/flipper-common/src/settings.tsx @@ -39,6 +39,12 @@ export type Settings = { darkMode: 'dark' | 'light' | 'system'; showWelcomeAtStartup: boolean; suppressPluginErrors: boolean; + /** + * Plugin marketplace - allow internal plugin distribution + */ + enablePluginMarketplace: boolean; + marketplaceURL: string; + enablePluginMarketplaceAutoUpdate: boolean; }; export enum ReleaseChannel { diff --git a/desktop/flipper-server-core/src/plugins/PluginManager.tsx b/desktop/flipper-server-core/src/plugins/PluginManager.tsx index 2f38a0631..75fabd3b7 100644 --- a/desktop/flipper-server-core/src/plugins/PluginManager.tsx +++ b/desktop/flipper-server-core/src/plugins/PluginManager.tsx @@ -138,7 +138,17 @@ export class PluginManager { } }, }); - if (response.headers['content-type'] !== 'application/octet-stream') { + function parseHeaderValue(header: string) { + const values = header.split(';'); + // remove white space + return values.map((value) => value.trim()); + } + + if ( + !parseHeaderValue(response.headers['content-type']).includes( + 'application/octet-stream', + ) + ) { throw new Error( `It looks like you are not on VPN/Lighthouse. Unexpected content type received: ${response.headers['content-type']}.`, ); diff --git a/desktop/flipper-server-core/src/utils/settings.tsx b/desktop/flipper-server-core/src/utils/settings.tsx index 28b02a920..52cfc18f9 100644 --- a/desktop/flipper-server-core/src/utils/settings.tsx +++ b/desktop/flipper-server-core/src/utils/settings.tsx @@ -73,6 +73,9 @@ function getDefaultSettings(): Settings { darkMode: 'light', showWelcomeAtStartup: true, suppressPluginErrors: false, + enablePluginMarketplace: false, + marketplaceURL: '', + enablePluginMarketplaceAutoUpdate: true, }; } diff --git a/desktop/flipper-ui-core/src/chrome/SettingsSheet.tsx b/desktop/flipper-ui-core/src/chrome/SettingsSheet.tsx index f824ad148..0bfb30b18 100644 --- a/desktop/flipper-ui-core/src/chrome/SettingsSheet.tsx +++ b/desktop/flipper-ui-core/src/chrome/SettingsSheet.tsx @@ -18,7 +18,11 @@ import {connect} from 'react-redux'; import {State as Store} from '../reducers'; import {flush} from '../utils/persistor'; import ToggledSection from './settings/ToggledSection'; -import {FilePathConfigField, ConfigText} from './settings/configFields'; +import { + FilePathConfigField, + ConfigText, + URLConfigField, +} from './settings/configFields'; import KeyboardShortcutInput from './settings/KeyboardShortcutInput'; import {isEqual, isMatch, isEmpty} from 'lodash'; import LauncherSettingsPanel from '../fb-stubs/LauncherSettingsPanel'; @@ -30,7 +34,12 @@ import { sleep, } from 'flipper-common'; import {Modal, message, Button} from 'antd'; -import {Layout, withTrackingScope, _NuxManagerContext} from 'flipper-plugin'; +import { + Layout, + withTrackingScope, + _NuxManagerContext, + NUX, +} from 'flipper-plugin'; import {getRenderHostInstance} from '../RenderHost'; import {loadTheme} from '../utils/loadTheme'; @@ -118,6 +127,9 @@ class SettingsSheet extends Component { reactNative, darkMode, suppressPluginErrors, + enablePluginMarketplace, + enablePluginMarketplaceAutoUpdate, + marketplaceURL, } = this.state.updatedSettings; const settingsPristine = @@ -324,6 +336,51 @@ class SettingsSheet extends Component { }} /> + + { + this.setState({ + updatedSettings: { + ...this.state.updatedSettings, + enablePluginMarketplace: v, + }, + }); + }}> + { + this.setState({ + updatedSettings: { + ...this.state.updatedSettings, + marketplaceURL: v, + }, + }); + }} + /> + { + this.setState({ + updatedSettings: { + ...this.state.updatedSettings, + enablePluginMarketplaceAutoUpdate: v, + }, + }); + }} + /> + + Reset all new user tooltips diff --git a/desktop/flipper-ui-core/src/chrome/settings/configFields.tsx b/desktop/flipper-ui-core/src/chrome/settings/configFields.tsx index 3bbc2fc3e..004ca9a2d 100644 --- a/desktop/flipper-ui-core/src/chrome/settings/configFields.tsx +++ b/desktop/flipper-ui-core/src/chrome/settings/configFields.tsx @@ -154,3 +154,60 @@ export function ConfigText(props: {content: string; frozen?: boolean}) { ); } + +export function URLConfigField(props: { + label: string; + resetValue?: string; + defaultValue: string; + onChange: (path: string) => void; + frozen?: boolean; +}) { + const [value, setValue] = useState(props.defaultValue); + const [isValid, setIsValid] = useState(true); + + useEffect(() => { + try { + const url = new URL(value); + const isValidUrl = + ['http:', 'https:'].includes(url.protocol) && + url.href.startsWith(value); + setIsValid(isValidUrl); + } catch (_) { + setIsValid(false); + } + }, [value]); + + return ( + + {props.label} + { + setValue(e.target.value); + props.onChange(e.target.value); + }} + /> + + {props.resetValue && ( + { + setValue(props.resetValue!); + props.onChange(props.resetValue!); + }}> + + + )} + {isValid ? null : ( + + )} + {props.frozen && } + + ); +} diff --git a/desktop/flipper-ui-core/src/dispatcher/handleOpenPluginDeeplink.tsx b/desktop/flipper-ui-core/src/dispatcher/handleOpenPluginDeeplink.tsx index 219d0c586..6a5b9bc6c 100644 --- a/desktop/flipper-ui-core/src/dispatcher/handleOpenPluginDeeplink.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/handleOpenPluginDeeplink.tsx @@ -320,7 +320,7 @@ async function verifyPluginStatus( if (!isTest() && !store.getState().plugins.marketplacePlugins.length) { // plugins not yet fetched // updates plugins from marketplace (if logged in), and stores them - await loadPluginsFromMarketplace(); + await loadPluginsFromMarketplace(store); } // while true loop; after pressing install or add GK, we want to check again if plugin is available while (true) { diff --git a/desktop/flipper-ui-core/src/dispatcher/pluginMarketplace.tsx b/desktop/flipper-ui-core/src/dispatcher/pluginMarketplace.tsx index 84ce505cb..b2cfd1607 100644 --- a/desktop/flipper-ui-core/src/dispatcher/pluginMarketplace.tsx +++ b/desktop/flipper-ui-core/src/dispatcher/pluginMarketplace.tsx @@ -27,6 +27,7 @@ import {isConnectivityOrAuthError} from 'flipper-common'; import {isLoggedIn} from '../fb-stubs/user'; import {getRenderHostInstance} from '..'; +// TODO: provide this value from settings export const pollingIntervalMs = getRenderHostInstance().serverConfig.env .FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL ? parseInt( @@ -36,25 +37,31 @@ export const pollingIntervalMs = getRenderHostInstance().serverConfig.env ) // for manual testing we could set smaller interval : 300000; // 5 min by default -function isAutoUpdateDisabled() { +function isAutoUpdateDisabled(store: Store) { return ( - !getFlipperLib().isFB || + // for open-source version auto-updates must be explicitly enabled in Settings + (!getFlipperLib().isFB && + !store.getState().settingsState.enablePluginMarketplaceAutoUpdate) || + // for internal build we disable auto-updates in case user is not logged + (getFlipperLib().isFB && !isLoggedIn().get()) || getRenderHostInstance().GK('flipper_disable_plugin_auto_update') || getRenderHostInstance().serverConfig.env.FLIPPER_NO_PLUGIN_AUTO_UPDATE !== undefined ); } -function isPluginMarketplaceDisabled() { +function isPluginMarketplaceDisabled(store: Store) { return ( - !getFlipperLib().isFB || + // for open-source version marketplace must be explicitly enabled in Settings + (!getFlipperLib().isFB && + !store.getState().settingsState.enablePluginMarketplace) || getRenderHostInstance().GK('flipper_disable_plugin_marketplace') || getRenderHostInstance().serverConfig.env.FLIPPER_NO_PLUGIN_MARKETPLACE ); } export default (store: Store) => { - if (isPluginMarketplaceDisabled()) { + if (isPluginMarketplaceDisabled(store)) { console.warn( 'Loading plugins from Plugin Marketplace disabled by GK or env var', ); @@ -98,10 +105,10 @@ export default (store: Store) => { }; }; -export async function loadPluginsFromMarketplace(): Promise< - MarketplacePluginDetails[] -> { - const availablePlugins = await loadAvailablePlugins(); +export async function loadPluginsFromMarketplace( + store: Store, +): Promise { + const availablePlugins = await loadAvailablePlugins(store); return selectCompatibleMarketplaceVersions(availablePlugins); } @@ -111,7 +118,7 @@ async function refreshMarketplacePlugins(store: Store): Promise { return; } try { - const plugins = await loadPluginsFromMarketplace(); + const plugins = await loadPluginsFromMarketplace(store); store.dispatch(registerMarketplacePlugins(plugins)); autoUpdatePlugins(store, plugins); } catch (err) { @@ -177,7 +184,7 @@ export function autoUpdatePlugins( } } } - if (isAutoUpdateDisabled() || !isLoggedIn().get()) { + if (isAutoUpdateDisabled(store)) { return; } for (const plugin of marketplacePlugins) { diff --git a/desktop/flipper-ui-core/src/fb-stubs/pluginMarketplaceAPI.tsx b/desktop/flipper-ui-core/src/fb-stubs/pluginMarketplaceAPI.tsx index 3ac40320f..9d28dc021 100644 --- a/desktop/flipper-ui-core/src/fb-stubs/pluginMarketplaceAPI.tsx +++ b/desktop/flipper-ui-core/src/fb-stubs/pluginMarketplaceAPI.tsx @@ -8,9 +8,22 @@ */ import {MarketplacePluginDetails} from '../reducers/plugins'; +import {Store} from '../reducers/index'; -export async function loadAvailablePlugins(): Promise< - MarketplacePluginDetails[] -> { - return []; +export async function loadAvailablePlugins( + store: Store, +): Promise { + const {enablePluginMarketplace, marketplaceURL} = + store.getState().settingsState; + try { + if (!enablePluginMarketplace && !marketplaceURL) { + throw new Error('Marketplace is not enabled'); + } + const response = await fetch(marketplaceURL); + const plugins = await response.json(); + return plugins; + } catch (e) { + console.error('Failed while retrieving marketplace plugins', e); + return []; + } } diff --git a/desktop/scripts/jest-setup-after.tsx b/desktop/scripts/jest-setup-after.tsx index d5fff08d2..af92fa4e0 100644 --- a/desktop/scripts/jest-setup-after.tsx +++ b/desktop/scripts/jest-setup-after.tsx @@ -150,6 +150,9 @@ function createStubRenderHost(): RenderHost { }, showWelcomeAtStartup: false, suppressPluginErrors: false, + enablePluginMarketplace: false, + marketplaceURL: '', + enablePluginMarketplaceAutoUpdate: true, }, validWebSocketOrigins: [], };