diff --git a/desktop/app/src/MenuBar.tsx b/desktop/app/src/MenuBar.tsx
index dfbbad388..fc350f606 100644
--- a/desktop/app/src/MenuBar.tsx
+++ b/desktop/app/src/MenuBar.tsx
@@ -34,6 +34,7 @@ import {
import {StyleGuide} from './sandy-chrome/StyleGuide';
import {showEmulatorLauncher} from './sandy-chrome/appinspect/LaunchEmulator';
import {webFrame} from 'electron';
+import {openDeeplinkDialog} from './deeplink';
export type DefaultKeyboardAction = keyof typeof _buildInMenuEntries;
export type TopLevelMenu = 'Edit' | 'View' | 'Window' | 'Help';
@@ -364,6 +365,12 @@ function getTemplate(
}
},
},
+ {
+ label: 'Trigger deeplink...',
+ click() {
+ openDeeplinkDialog(store);
+ },
+ },
{
type: 'separator',
},
diff --git a/desktop/app/src/__tests__/deeplink.node.tsx b/desktop/app/src/__tests__/deeplink.node.tsx
new file mode 100644
index 000000000..3c4999d90
--- /dev/null
+++ b/desktop/app/src/__tests__/deeplink.node.tsx
@@ -0,0 +1,95 @@
+/**
+ * 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
+ */
+
+jest.useFakeTimers();
+
+import React from 'react';
+import {renderMockFlipperWithPlugin} from '../test-utils/createMockFlipperWithPlugin';
+import {
+ _SandyPluginDefinition,
+ PluginClient,
+ TestUtils,
+ usePlugin,
+ createState,
+ useValue,
+} from 'flipper-plugin';
+import {handleDeeplink} from '../deeplink';
+
+test('Triggering a deeplink will work', async () => {
+ const linksSeen: any[] = [];
+
+ const plugin = (client: PluginClient) => {
+ const linkState = createState('');
+ client.onDeepLink((link) => {
+ linksSeen.push(link);
+ linkState.set(String(link));
+ });
+ return {
+ linkState,
+ };
+ };
+
+ const definition = new _SandyPluginDefinition(
+ TestUtils.createMockPluginDetails(),
+ {
+ plugin,
+ Component() {
+ const instance = usePlugin(plugin);
+ const linkState = useValue(instance.linkState);
+ return
{linkState || 'world'}
;
+ },
+ },
+ );
+ const {renderer, client, store} = await renderMockFlipperWithPlugin(
+ definition,
+ );
+
+ expect(linksSeen).toEqual([]);
+
+ await handleDeeplink(
+ store,
+ `flipper://${client.query.app}/${definition.id}/universe`,
+ );
+
+ jest.runAllTimers();
+ expect(linksSeen).toEqual(['universe']);
+ expect(renderer.baseElement).toMatchInlineSnapshot(`
+
+
+
+ `);
+});
+
+test('Will throw error on invalid deeplinks', async () => {
+ // flipper:///support-form/?form=litho
+ expect(() =>
+ handleDeeplink(undefined as any, `flipper://test`),
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`"Unknown deeplink"`);
+});
diff --git a/desktop/app/src/deeplink.tsx b/desktop/app/src/deeplink.tsx
new file mode 100644
index 000000000..5125a1981
--- /dev/null
+++ b/desktop/app/src/deeplink.tsx
@@ -0,0 +1,146 @@
+/**
+ * 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 {
+ ACTIVE_SHEET_SIGN_IN,
+ setActiveSheet,
+ setPastedToken,
+ toggleAction,
+} from './reducers/application';
+import {Group, SUPPORTED_GROUPS} from './reducers/supportForm';
+import {Store} from './reducers/index';
+import {importDataToStore} from './utils/exportData';
+import {selectPlugin} from './reducers/connections';
+import {Layout, renderReactRoot} from 'flipper-plugin';
+import React, {useState} from 'react';
+import {Alert, Input, Modal} from 'antd';
+
+const UNKNOWN = 'Unknown deeplink';
+/**
+ * Handle a flipper:// deeplink. Will throw if the URL pattern couldn't be recognised
+ */
+export async function handleDeeplink(
+ store: Store,
+ query: string,
+): Promise {
+ const uri = new URL(query);
+ if (uri.protocol !== 'flipper:') {
+ throw new Error(UNKNOWN);
+ }
+ if (uri.pathname.match(/^\/*import\/*$/)) {
+ const url = uri.searchParams.get('url');
+ store.dispatch(toggleAction('downloadingImportData', true));
+ if (url) {
+ return fetch(url)
+ .then((res) => res.text())
+ .then((data) => importDataToStore(url, data, store))
+ .then(() => {
+ store.dispatch(toggleAction('downloadingImportData', false));
+ })
+ .catch((e: Error) => {
+ console.error('Failed to download Flipper trace' + e);
+ store.dispatch(toggleAction('downloadingImportData', false));
+ throw e;
+ });
+ }
+ throw new Error(UNKNOWN);
+ } else if (uri.pathname.match(/^\/*support-form\/*$/)) {
+ const formParam = uri.searchParams.get('form');
+ const grp = deeplinkFormParamToGroups(formParam);
+ if (grp) {
+ grp.handleSupportFormDeeplinks(store);
+ return;
+ }
+ throw new Error(UNKNOWN);
+ } else if (uri.pathname.match(/^\/*login\/*$/)) {
+ const token = uri.searchParams.get('token');
+ store.dispatch(setPastedToken(token ?? undefined));
+ if (store.getState().application.activeSheet !== ACTIVE_SHEET_SIGN_IN) {
+ store.dispatch(setActiveSheet(ACTIVE_SHEET_SIGN_IN));
+ }
+ return;
+ }
+ const match = uriComponents(query);
+ if (match.length > 1) {
+ // flipper:////
+ store.dispatch(
+ selectPlugin({
+ selectedApp: match[0],
+ selectedPlugin: match[1],
+ deepLinkPayload: match[2],
+ }),
+ );
+ return;
+ } else {
+ throw new Error(UNKNOWN);
+ }
+}
+
+function deeplinkFormParamToGroups(
+ formParam: string | null,
+): Group | undefined {
+ if (!formParam) {
+ return undefined;
+ }
+ return SUPPORTED_GROUPS.find((grp) => {
+ return grp.deeplinkSuffix.toLowerCase() === formParam.toLowerCase();
+ });
+}
+
+export const uriComponents = (url: string): Array => {
+ if (!url) {
+ return [];
+ }
+ const match: Array | undefined | null = url.match(
+ /^flipper:\/\/([^\/]*)\/([^\/\?]*)\/?(.*)$/,
+ );
+ if (match) {
+ return match.map(decodeURIComponent).slice(1).filter(Boolean);
+ }
+ return [];
+};
+
+export function openDeeplinkDialog(store: Store) {
+ renderReactRoot((hide) => );
+}
+
+function TestDeeplinkDialog({
+ store,
+ onHide,
+}: {
+ store: Store;
+ onHide: () => void;
+}) {
+ const [query, setQuery] = useState('flipper://');
+ const [error, setError] = useState('');
+ return (
+ {
+ handleDeeplink(store, query)
+ .then(onHide)
+ .catch((e) => {
+ setError(`Failed to handle deeplink '${query}': ${e}`);
+ });
+ }}
+ onCancel={onHide}>
+
+ <>Enter a deeplink to test it:>
+ {
+ setQuery(v.target.value);
+ }}
+ />
+ {error && }
+
+
+ );
+}
diff --git a/desktop/app/src/dispatcher/__tests__/deeplinkURLParsing.node.tsx b/desktop/app/src/dispatcher/__tests__/deeplinkURLParsing.node.tsx
index a19150146..1d6113c2c 100644
--- a/desktop/app/src/dispatcher/__tests__/deeplinkURLParsing.node.tsx
+++ b/desktop/app/src/dispatcher/__tests__/deeplinkURLParsing.node.tsx
@@ -7,7 +7,7 @@
* @format
*/
-import {uriComponents} from '../application';
+import {uriComponents} from '../../deeplink';
test('test parsing of deeplink URL', () => {
const url = 'flipper://app/plugin/meta/data';
diff --git a/desktop/app/src/dispatcher/application.tsx b/desktop/app/src/dispatcher/application.tsx
index f90783620..c6e57c147 100644
--- a/desktop/app/src/dispatcher/application.tsx
+++ b/desktop/app/src/dispatcher/application.tsx
@@ -8,36 +8,16 @@
*/
import {remote, ipcRenderer, IpcRendererEvent} from 'electron';
-import {
- ACTIVE_SHEET_SIGN_IN,
- setActiveSheet,
- setPastedToken,
- toggleAction,
-} from '../reducers/application';
-import {Group, SUPPORTED_GROUPS} from '../reducers/supportForm';
import {Store} from '../reducers/index';
import {Logger} from '../fb-interfaces/Logger';
import {parseFlipperPorts} from '../utils/environmentVariables';
import {
- importDataToStore,
importFileToStore,
IMPORT_FLIPPER_TRACE_EVENT,
} from '../utils/exportData';
import {tryCatchReportPlatformFailures} from '../utils/metrics';
-import {selectPlugin} from '../reducers/connections';
-
-export const uriComponents = (url: string): Array => {
- if (!url) {
- return [];
- }
- const match: Array | undefined | null = url.match(
- /^flipper:\/\/([^\/]*)\/([^\/\?]*)\/?(.*)$/,
- );
- if (match) {
- return match.map(decodeURIComponent).slice(1).filter(Boolean);
- }
- return [];
-};
+import {handleDeeplink} from '../deeplink';
+import {message} from 'antd';
export default (store: Store, _logger: Logger) => {
const currentWindow = remote.getCurrentWindow();
@@ -77,66 +57,13 @@ export default (store: Store, _logger: Logger) => {
ipcRenderer.on(
'flipper-protocol-handler',
(_event: IpcRendererEvent, query: string) => {
- const uri = new URL(query);
- if (uri.protocol !== 'flipper:') {
- return;
- }
- if (uri.pathname.match(/^\/*import\/*$/)) {
- const url = uri.searchParams.get('url');
- store.dispatch(toggleAction('downloadingImportData', true));
- return (
- typeof url === 'string' &&
- fetch(url)
- .then((res) => res.text())
- .then((data) => importDataToStore(url, data, store))
- .then(() => {
- store.dispatch(toggleAction('downloadingImportData', false));
- })
- .catch((e: Error) => {
- console.error(e);
- store.dispatch(toggleAction('downloadingImportData', false));
- })
- );
- } else if (uri.pathname.match(/^\/*support-form\/*$/)) {
- const formParam = uri.searchParams.get('form');
- const grp = deeplinkFormParamToGroups(formParam);
- if (grp) {
- grp.handleSupportFormDeeplinks(store);
- }
- return;
- } else if (uri.pathname.match(/^\/*login\/*$/)) {
- const token = uri.searchParams.get('token');
- store.dispatch(setPastedToken(token ?? undefined));
- if (store.getState().application.activeSheet !== ACTIVE_SHEET_SIGN_IN) {
- store.dispatch(setActiveSheet(ACTIVE_SHEET_SIGN_IN));
- }
- return;
- }
- const match = uriComponents(query);
- if (match.length > 1) {
- // flipper:////
- return store.dispatch(
- selectPlugin({
- selectedApp: match[0],
- selectedPlugin: match[1],
- deepLinkPayload: match[2],
- }),
- );
- }
+ handleDeeplink(store, query).catch((e) => {
+ console.warn('Failed to handle deeplink', query, e);
+ message.error(`Failed to handle deeplink '${query}': ${e}`);
+ });
},
);
- function deeplinkFormParamToGroups(
- formParam: string | null,
- ): Group | undefined {
- if (!formParam) {
- return undefined;
- }
- return SUPPORTED_GROUPS.find((grp) => {
- return grp.deeplinkSuffix.toLowerCase() === formParam.toLowerCase();
- });
- }
-
ipcRenderer.on(
'open-flipper-file',
(_event: IpcRendererEvent, url: string) => {
diff --git a/desktop/types/index.d.ts b/desktop/types/index.d.ts
index 3745ea231..7e73a0b7b 100644
--- a/desktop/types/index.d.ts
+++ b/desktop/types/index.d.ts
@@ -16,6 +16,7 @@
///
///
///
+///
///
///
///
@@ -26,4 +27,3 @@
///
///
///
-///