Summary: This diffs adds support for the activeSheets mechanism in Sandy. It should be removed in the future (see T78696648) since open a dialog and keeping dialog state locally results in much more straight forward code, but supporting this for now makes sure that old flows are still supported. With this change the plugin selection during a Flipper export for example wouldn't become visible Reviewed By: cekkaewnumchai Differential Revision: D24620074 fbshipit-source-id: f0558f5738e86a84a5cd0b9d574a3cfd0a3bf424
313 lines
8.5 KiB
TypeScript
313 lines
8.5 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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 {KeyboardActions} from './MenuBar';
|
|
import {Logger} from './fb-interfaces/Logger';
|
|
import Client from './Client';
|
|
import {Store} from './reducers/index';
|
|
import {MetricType} from './utils/exportMetrics';
|
|
import {ReactNode, Component} from 'react';
|
|
import BaseDevice from './devices/BaseDevice';
|
|
import {serialize, deserialize} from './utils/serialization';
|
|
import {Idler} from './utils/Idler';
|
|
import {StaticView} from './reducers/connections';
|
|
import {State as ReduxState} from './reducers';
|
|
import {DEFAULT_MAX_QUEUE_SIZE} from './reducers/pluginMessageQueue';
|
|
import {PluginDetails} from 'flipper-plugin-lib';
|
|
import {Settings} from './reducers/settings';
|
|
import {SandyPluginDefinition} from 'flipper-plugin';
|
|
|
|
type Parameters = {[key: string]: any};
|
|
|
|
export type PluginDefinition = ClientPluginDefinition | DevicePluginDefinition;
|
|
|
|
export type DevicePluginDefinition =
|
|
| typeof FlipperDevicePlugin
|
|
| SandyPluginDefinition;
|
|
|
|
export type ClientPluginDefinition =
|
|
| typeof FlipperPlugin
|
|
| SandyPluginDefinition;
|
|
|
|
export type ClientPluginMap = Map<string, ClientPluginDefinition>;
|
|
export type DevicePluginMap = Map<string, DevicePluginDefinition>;
|
|
|
|
export function isSandyPlugin(
|
|
plugin?: PluginDefinition | null,
|
|
): plugin is SandyPluginDefinition {
|
|
return plugin instanceof SandyPluginDefinition;
|
|
}
|
|
|
|
// This function is intended to be called from outside of the plugin.
|
|
// If you want to `call` from the plugin use, this.client.call
|
|
export function callClient(
|
|
client: Client,
|
|
id: string,
|
|
): (method: string, params: Parameters) => Promise<any> {
|
|
return (method, params) => client.call(id, method, false, params);
|
|
}
|
|
|
|
// This function is intended to be called from outside of the plugin.
|
|
// If you want to `supportsMethod` from the plugin use, this.client.supportsMethod
|
|
export function supportsMethod(
|
|
client: Client,
|
|
id: string,
|
|
): (method: string) => Promise<boolean> {
|
|
return (method) => client.supportsMethod(id, method);
|
|
}
|
|
|
|
export interface PluginClient {
|
|
// eslint-disable-next-line
|
|
send(method: string, params?: Parameters): void;
|
|
// eslint-disable-next-line
|
|
call(method: string, params?: Parameters): Promise<any>;
|
|
// eslint-disable-next-line
|
|
subscribe(method: string, callback: (params: any) => void): void;
|
|
// eslint-disable-next-line
|
|
supportsMethod(method: string): Promise<boolean>;
|
|
}
|
|
|
|
type PluginTarget = BaseDevice | Client;
|
|
|
|
export type Notification = {
|
|
id: string;
|
|
title: string;
|
|
message: string | ReactNode;
|
|
severity: 'warning' | 'error';
|
|
timestamp?: number;
|
|
category?: string;
|
|
action?: string;
|
|
};
|
|
|
|
export type Props<T> = {
|
|
logger: Logger;
|
|
persistedState: T;
|
|
setPersistedState: (state: Partial<T>) => void;
|
|
target: PluginTarget;
|
|
deepLinkPayload: unknown;
|
|
selectPlugin: (pluginID: string, deepLinkPayload: unknown) => boolean;
|
|
isArchivedDevice: boolean;
|
|
selectedApp: string | null;
|
|
setStaticView: (payload: StaticView) => void;
|
|
settingsState: Settings;
|
|
};
|
|
|
|
export type BaseAction = {
|
|
type: string;
|
|
};
|
|
|
|
export type PersistedStateReducer = (
|
|
persistedState: StaticPersistedState,
|
|
method: string,
|
|
data: any,
|
|
) => StaticPersistedState;
|
|
|
|
type StaticPersistedState = any;
|
|
|
|
export abstract class FlipperBasePlugin<
|
|
State,
|
|
Actions extends BaseAction,
|
|
PersistedState
|
|
> extends Component<Props<PersistedState>, State> {
|
|
abstract ['constructor']: any;
|
|
static title: string | null = null;
|
|
static category: string | null = null;
|
|
static id: string = '';
|
|
static packageName: string = '';
|
|
static version: string = '';
|
|
static icon: string | null = null;
|
|
static gatekeeper: string | null = null;
|
|
static entry: string | null = null;
|
|
static isDefault: boolean;
|
|
static details: PluginDetails;
|
|
static keyboardActions: KeyboardActions | null;
|
|
static screenshot: string | null;
|
|
static defaultPersistedState: any;
|
|
static persistedStateReducer: PersistedStateReducer | null;
|
|
static maxQueueSize: number = DEFAULT_MAX_QUEUE_SIZE;
|
|
static metricsReducer:
|
|
| ((persistedState: StaticPersistedState) => Promise<MetricType>)
|
|
| undefined;
|
|
static exportPersistedState:
|
|
| ((
|
|
callClient: (method: string, params?: any) => Promise<any>,
|
|
persistedState: StaticPersistedState | undefined,
|
|
store: ReduxState | undefined,
|
|
idler?: Idler,
|
|
statusUpdate?: (msg: string) => void,
|
|
supportsMethod?: (method: string) => Promise<boolean>,
|
|
) => Promise<StaticPersistedState | undefined>)
|
|
| undefined;
|
|
static getActiveNotifications:
|
|
| ((persistedState: StaticPersistedState) => Array<Notification>)
|
|
| undefined;
|
|
static onRegisterDevice:
|
|
| ((
|
|
store: Store,
|
|
baseDevice: BaseDevice,
|
|
setPersistedState: (
|
|
pluginKey: string,
|
|
newPluginState: StaticPersistedState | null,
|
|
) => void,
|
|
) => void)
|
|
| null;
|
|
|
|
reducers: {
|
|
[actionName: string]: (state: State, actionData: any) => Partial<State>;
|
|
} = {};
|
|
onKeyboardAction: ((action: string) => void) | undefined;
|
|
|
|
toJSON() {
|
|
return `<${this.constructor.name}#${this.constructor.id}>`;
|
|
}
|
|
|
|
// methods to be overriden by plugins
|
|
init(): void {}
|
|
|
|
static serializePersistedState: (
|
|
persistedState: StaticPersistedState,
|
|
statusUpdate?: (msg: string) => void,
|
|
idler?: Idler,
|
|
pluginName?: string,
|
|
) => Promise<string> = (
|
|
persistedState: StaticPersistedState,
|
|
statusUpdate?: (msg: string) => void,
|
|
idler?: Idler,
|
|
pluginName?: string,
|
|
) => {
|
|
return serialize(
|
|
persistedState,
|
|
idler,
|
|
statusUpdate,
|
|
pluginName != null ? `Serializing ${pluginName}` : undefined,
|
|
);
|
|
};
|
|
|
|
static deserializePersistedState: (
|
|
serializedString: string,
|
|
) => StaticPersistedState = (serializedString: string) => {
|
|
return deserialize(serializedString);
|
|
};
|
|
|
|
teardown(): void {}
|
|
|
|
computeNotifications(
|
|
_props: Props<PersistedState>,
|
|
_state: State,
|
|
): Array<Notification> {
|
|
return [];
|
|
}
|
|
|
|
// methods to be overridden by subclasses
|
|
_init(): void {}
|
|
|
|
_teardown(): void {}
|
|
|
|
dispatchAction(actionData: Actions) {
|
|
const action = this.reducers[actionData.type];
|
|
if (!action) {
|
|
throw new ReferenceError(`Unknown action ${actionData.type}`);
|
|
}
|
|
|
|
if (typeof action === 'function') {
|
|
this.setState(action.call(this, this.state, actionData) as State);
|
|
} else {
|
|
throw new TypeError(`Reducer ${actionData.type} isn't a function`);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class FlipperDevicePlugin<
|
|
S,
|
|
A extends BaseAction,
|
|
P
|
|
> extends FlipperBasePlugin<S, A, P> {
|
|
['constructor']: typeof FlipperPlugin;
|
|
device: BaseDevice;
|
|
|
|
constructor(props: Props<P>) {
|
|
super(props);
|
|
this.device = props.target as BaseDevice;
|
|
}
|
|
|
|
_init() {
|
|
this.init();
|
|
}
|
|
|
|
_teardown() {
|
|
this.teardown();
|
|
}
|
|
|
|
static supportsDevice(_device: BaseDevice): boolean {
|
|
throw new Error(
|
|
'supportsDevice is unimplemented in FlipperDevicePlugin class',
|
|
);
|
|
}
|
|
}
|
|
|
|
export class FlipperPlugin<
|
|
S,
|
|
A extends BaseAction,
|
|
P
|
|
> extends FlipperBasePlugin<S, A, P> {
|
|
['constructor']: typeof FlipperPlugin;
|
|
constructor(props: Props<P>) {
|
|
super(props);
|
|
// @ts-ignore constructor should be assigned already
|
|
const {id} = this.constructor;
|
|
this.subscriptions = [];
|
|
this.realClient = props.target as Client;
|
|
this.client = {
|
|
call: (method, params) => this.realClient.call(id, method, true, params),
|
|
send: (method, params) => this.realClient.send(id, method, params),
|
|
subscribe: (method, callback) => {
|
|
this.subscriptions.push({
|
|
method,
|
|
callback,
|
|
});
|
|
this.realClient.subscribe(id, method, callback);
|
|
},
|
|
supportsMethod: (method) => this.realClient.supportsMethod(id, method),
|
|
};
|
|
}
|
|
|
|
subscriptions: Array<{
|
|
method: string;
|
|
callback: Function;
|
|
}>;
|
|
|
|
client: PluginClient;
|
|
realClient: Client;
|
|
|
|
getDevice(): Promise<BaseDevice> {
|
|
return this.realClient.device;
|
|
}
|
|
|
|
_teardown() {
|
|
// automatically unsubscribe subscriptions
|
|
const pluginId = this.constructor.id;
|
|
for (const {method, callback} of this.subscriptions) {
|
|
this.realClient.unsubscribe(pluginId, method, callback);
|
|
}
|
|
// run plugin teardown
|
|
this.teardown();
|
|
if (!this.realClient.isBackgroundPlugin(pluginId)) {
|
|
this.realClient.deinitPlugin(pluginId);
|
|
}
|
|
}
|
|
|
|
_init() {
|
|
const pluginId = this.constructor.id;
|
|
if (!this.realClient.isBackgroundPlugin(pluginId)) {
|
|
this.realClient.initPlugin(pluginId);
|
|
}
|
|
this.init();
|
|
}
|
|
}
|