diff --git a/desktop/app/src/deeplink.tsx b/desktop/app/src/deeplink.tsx index 5125a1981..16e4fac2f 100644 --- a/desktop/app/src/deeplink.tsx +++ b/desktop/app/src/deeplink.tsx @@ -20,6 +20,7 @@ import {selectPlugin} from './reducers/connections'; import {Layout, renderReactRoot} from 'flipper-plugin'; import React, {useState} from 'react'; import {Alert, Input, Modal} from 'antd'; +import {handleOpenPluginDeeplink} from './dispatcher/handleOpenPluginDeeplink'; const UNKNOWN = 'Unknown deeplink'; /** @@ -33,6 +34,9 @@ export async function handleDeeplink( if (uri.protocol !== 'flipper:') { throw new Error(UNKNOWN); } + if (uri.href.startsWith('flipper://open-plugin')) { + return handleOpenPluginDeeplink(store, query); + } if (uri.pathname.match(/^\/*import\/*$/)) { const url = uri.searchParams.get('url'); store.dispatch(toggleAction('downloadingImportData', true)); @@ -68,7 +72,14 @@ export async function handleDeeplink( } const match = uriComponents(query); if (match.length > 1) { + // deprecated, use the open-plugin format instead, which is more flexible + // and will guide the user through any necessary set up steps // flipper://// + console.warn( + `Deprecated deeplink format: '${query}', use 'flipper://open-plugin?plugin-id=${ + match[1] + }&client=${match[0]}&payload=${encodeURIComponent(match[2])}' instead.`, + ); store.dispatch( selectPlugin({ selectedApp: match[0], diff --git a/desktop/app/src/dispatcher/__tests__/handleOpenPluginDeeplink.node.tsx b/desktop/app/src/dispatcher/__tests__/handleOpenPluginDeeplink.node.tsx new file mode 100644 index 000000000..397e56683 --- /dev/null +++ b/desktop/app/src/dispatcher/__tests__/handleOpenPluginDeeplink.node.tsx @@ -0,0 +1,120 @@ +/** + * 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 {parseOpenPluginParams} from '../handleOpenPluginDeeplink'; +import {handleDeeplink} from '../../deeplink'; + +test('open-plugin deeplink parsing', () => { + const testpayload = 'http://www.google/?test=c o%20o+l'; + const testLink = + 'flipper://open-plugin?plugin-id=graphql&client=facebook&devices=android,ios&chrome=1&payload=' + + encodeURIComponent(testpayload); + const res = parseOpenPluginParams(testLink); + expect(res).toEqual({ + pluginId: 'graphql', + client: 'facebook', + devices: ['android', 'ios'], + payload: 'http://www.google/?test=c o o+l', + }); +}); + +test('open-plugin deeplink parsing - 2', () => { + const testLink = 'flipper://open-plugin?plugin-id=graphql'; + const res = parseOpenPluginParams(testLink); + expect(res).toEqual({ + pluginId: 'graphql', + client: undefined, + devices: [], + payload: undefined, + }); +}); + +test('open-plugin deeplink parsing - 3', () => { + expect(() => + parseOpenPluginParams('flipper://open-plugin?'), + ).toThrowErrorMatchingInlineSnapshot(`"Missing plugin-id param"`); +}); + +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://open-plugin?plugin-id=${definition.id}&client=${client.query.app}&payload=universe`, + ); + + jest.runAllTimers(); + expect(linksSeen).toEqual(['universe']); + expect(renderer.baseElement).toMatchInlineSnapshot(` + +
+
+
+
+
+

+ universe +

+
+
+
+
+
+ + `); +}); diff --git a/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx b/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx new file mode 100644 index 000000000..beae35948 --- /dev/null +++ b/desktop/app/src/dispatcher/handleOpenPluginDeeplink.tsx @@ -0,0 +1,75 @@ +/** + * 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 {selectPlugin} from '../reducers/connections'; +import {Store} from '../reducers/index'; + +type OpenPluginParams = { + pluginId: string; + client: string | undefined; + devices: string[]; + payload: string | undefined; +}; + +export function parseOpenPluginParams(query: string): OpenPluginParams { + // 'flipper://open-plugin?plugin-id=graphql&client=facebook&devices=android,ios&chrome=1&payload=' + const url = new URL(query); + const params = new Map(url.searchParams as any); + if (!params.has('plugin-id')) { + throw new Error('Missing plugin-id param'); + } + return { + pluginId: params.get('plugin-id')!, + client: params.get('client'), + devices: params.get('devices')?.split(',') ?? [], + payload: params.get('payload') + ? decodeURIComponent(params.get('payload')!) + : undefined, + }; +} + +export async function handleOpenPluginDeeplink(store: Store, query: string) { + const params = parseOpenPluginParams(query); + await verifyLighthouse(); + await verifyUserIsLoggedIn(); + await verifyFlipperIsUpToDate(); + await verifyPluginInstalled(); + await verifyClient(); + await verifyPluginInstalled(); + await openPlugin(store, params); +} +function verifyLighthouse() { + // TODO: +} + +function verifyUserIsLoggedIn() { + // TODO: +} + +function verifyFlipperIsUpToDate() { + // TODO: +} + +function verifyPluginInstalled() { + // TODO: +} + +function verifyClient() { + // TODO: +} + +function openPlugin(store: Store, params: OpenPluginParams) { + store.dispatch( + selectPlugin({ + selectedApp: params.client, + selectedPlugin: params.pluginId, + deepLinkPayload: params.payload, + }), + ); +}