Enable Marketplace for Flipper (#3491)

Summary:
This is PR on top of: https://github.com/facebook/flipper/pull/3473

It adds an option to Settings to allow distribution of marketplace plugins.

Also includes a simple fetch function to retrieve data from external API/server.

## Changelog

Allow marketplace plugins

Pull Request resolved: https://github.com/facebook/flipper/pull/3491

Test Plan:
1. Enable marketplace
2. Provide custom marketplace server (it will serve the list of internal plugins with downloadURL)
3. Test if can see Available plugins and can download/remove the plugin
4. If new update for the plugin, it should also allow auto update

Reviewed By: antonk52

Differential Revision: D34586339

Pulled By: nikoant

fbshipit-source-id: c887982aa0f0f9abd3b5360f22e8692a2445d345
This commit is contained in:
Anton Nikolaev
2022-03-07 02:49:49 -08:00
committed by Facebook GitHub Bot
parent 5b6000b424
commit 37ff34390a
9 changed files with 175 additions and 19 deletions

View File

@@ -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 {

View File

@@ -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']}.`,
);

View File

@@ -73,6 +73,9 @@ function getDefaultSettings(): Settings {
darkMode: 'light',
showWelcomeAtStartup: true,
suppressPluginErrors: false,
enablePluginMarketplace: false,
marketplaceURL: '',
enablePluginMarketplaceAutoUpdate: true,
};
}

View File

@@ -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<Props, State> {
reactNative,
darkMode,
suppressPluginErrors,
enablePluginMarketplace,
enablePluginMarketplaceAutoUpdate,
marketplaceURL,
} = this.state.updatedSettings;
const settingsPristine =
@@ -324,6 +336,51 @@ class SettingsSheet extends Component<Props, State> {
}}
/>
</ToggledSection>
<NUX
// TODO: provide link to Flipper doc with more details
title="Plugin marketplace serve as a way to distribute private/internal plugins"
placement="right">
<ToggledSection
label="Enable plugin marketplace"
toggled={enablePluginMarketplace}
frozen={false}
onChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
enablePluginMarketplace: v,
},
});
}}>
<URLConfigField
label="Martkeplace URL"
defaultValue={
marketplaceURL || 'http://plugin-marketplace.local/get-plugins'
}
onChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
marketplaceURL: v,
},
});
}}
/>
<ToggledSection
label="Enable auto update"
toggled={enablePluginMarketplaceAutoUpdate}
frozen={false}
onChange={(v) => {
this.setState({
updatedSettings: {
...this.state.updatedSettings,
enablePluginMarketplaceAutoUpdate: v,
},
});
}}
/>
</ToggledSection>
</NUX>
<Layout.Right center>
<span>Reset all new user tooltips</span>
<ResetTooltips />

View File

@@ -154,3 +154,60 @@ export function ConfigText(props: {content: string; frozen?: boolean}) {
</ConfigFieldContainer>
);
}
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 (
<ConfigFieldContainer>
<InfoText>{props.label}</InfoText>
<FileInputBox
placeholder={props.label}
value={value}
isValid={isValid}
onChange={(e) => {
setValue(e.target.value);
props.onChange(e.target.value);
}}
/>
{props.resetValue && (
<FlexColumn
title={`Reset to default value ${props.resetValue}`}
onClick={() => {
setValue(props.resetValue!);
props.onChange(props.resetValue!);
}}>
<CenteredGlyph
color={theme.primaryColor}
name="undo"
variant="outline"
/>
</FlexColumn>
)}
{isValid ? null : (
<CenteredGlyph name="caution-triangle" color={colors.yellow} />
)}
{props.frozen && <GrayedOutOverlay />}
</ConfigFieldContainer>
);
}

View File

@@ -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) {

View File

@@ -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<MarketplacePluginDetails[]> {
const availablePlugins = await loadAvailablePlugins(store);
return selectCompatibleMarketplaceVersions(availablePlugins);
}
@@ -111,7 +118,7 @@ async function refreshMarketplacePlugins(store: Store): Promise<void> {
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) {

View File

@@ -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<MarketplacePluginDetails[]> {
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 [];
}
}

View File

@@ -150,6 +150,9 @@ function createStubRenderHost(): RenderHost {
},
showWelcomeAtStartup: false,
suppressPluginErrors: false,
enablePluginMarketplace: false,
marketplaceURL: '',
enablePluginMarketplaceAutoUpdate: true,
},
validWebSocketOrigins: [],
};