From 1dc1dbbd29092b6739fbc8d48d5590c84f7fb280 Mon Sep 17 00:00:00 2001 From: Salah Benmoussati Date: Mon, 18 Nov 2024 10:27:47 +0100 Subject: [PATCH] SDA-4770 Expose openfin --- config/Symphony.config | 8 +- installer/mac/postinstall.sh | 8 ++ package-lock.json | 57 +++++++++++- package.json | 1 + spec/config.spec.ts | 1 + spec/mainApiHandler.spec.ts | 158 +++++++++++++++++++++++++++++++++ spec/openfinHandler.spec.ts | 111 +++++++++++++++++++++++ spec/postinstall.spec.ts | 3 +- src/app/config-handler.ts | 8 ++ src/app/main-api-handler.ts | 24 +++++ src/app/openfin-handler.ts | 140 +++++++++++++++++++++++++++++ src/app/plist-handler.ts | 27 ++++++ src/app/window-utils.ts | 4 +- src/common/api-interface.ts | 23 +++++ src/common/config-interface.ts | 6 ++ src/common/openfin-logger.ts | 5 ++ src/renderer/preload-main.ts | 13 +++ src/renderer/ssf-api.ts | 110 +++++++++++++++++++++++ 18 files changed, 701 insertions(+), 6 deletions(-) create mode 100644 spec/openfinHandler.spec.ts create mode 100644 src/app/openfin-handler.ts create mode 100644 src/common/openfin-logger.ts diff --git a/config/Symphony.config b/config/Symphony.config index 7a31c258c..df45dae1c 100644 --- a/config/Symphony.config +++ b/config/Symphony.config @@ -47,5 +47,11 @@ "userDataPath": "", "chromeFlags": "", "betaAutoUpdateChannelEnabled": true, - "latestAutoUpdateChannelEnabled": true + "latestAutoUpdateChannelEnabled": true, + "openfin": { + "uuid": "", + "licenseKey": "", + "runtimeVersion": "", + "autoConnect": false + } } diff --git a/installer/mac/postinstall.sh b/installer/mac/postinstall.sh index a541348dc..77e904c65 100755 --- a/installer/mac/postinstall.sh +++ b/installer/mac/postinstall.sh @@ -123,6 +123,10 @@ if [ "$EUID" -ne 0 ]; then defaults write "$plistFilePath" betaAutoUpdateChannelEnabled -bool true defaults write "$plistFilePath" latestAutoUpdateChannelEnabled -bool true defaults write "$plistFilePath" installVariant -string "$uuid" + defaults write "$plistFilePath" uuid -string "" + defaults write "$plistFilePath" licenseKey -string "" + defaults write "$plistFilePath" runtimeVersion -string "" + defaults write "$plistFilePath" autoConnect -bool false else sudo -u "$userName" defaults write "$plistFilePath" url -string "$pod_url" sudo -u "$userName" defaults write "$plistFilePath" autoUpdateUrl -string "" @@ -168,6 +172,10 @@ else sudo -u "$userName" defaults write "$plistFilePath" betaAutoUpdateChannelEnabled -bool true sudo -u "$userName" defaults write "$plistFilePath" latestAutoUpdateChannelEnabled -bool true sudo -u "$userName" defaults write "$plistFilePath" installVariant -string "$uuid" + sudo -u "$userName" defaults write "$plistFilePath" uuid -string "" + sudo -u "$userName" defaults write "$plistFilePath" licenseKey -string "" + sudo -u "$userName" defaults write "$plistFilePath" runtimeVersion -string "" + sudo -u "$userName" defaults write "$plistFilePath" autoConnect -bool false fi ## Remove the temp settings & permissions file created ## diff --git a/package-lock.json b/package-lock.json index 8c84a96bd..5786d882e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@symphony/symphony-c9-shell": "3.38.0-94.195", + "@openfin/node-adapter": "^40.100.7", "@types/lazy-brush": "^1.0.0", "adm-zip": "^0.5.10", "bplist-parser": "^0.3.2", @@ -2637,6 +2637,59 @@ "node": ">= 10.0.0" } }, + "node_modules/@openfin/core": { + "version": "40.100.7", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@openfin/core/-/core-40.100.7.tgz", + "integrity": "sha512-uADuXRyC52aAZaGRbLRhHRVs1aPyCxi+f8pPjC0CxmEYBrvm1uDH8cAFWm+/8y005tHtegW1JsbEEQ1+y/ngEQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@types/node": "^20.14.2", + "lodash": "^4.17.21", + "ws": "^7.3.0" + } + }, + "node_modules/@openfin/core/node_modules/@types/node": { + "version": "20.17.6", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/node/-/node-20.17.6.tgz", + "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@openfin/core/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/@openfin/node-adapter": { + "version": "40.100.7", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@openfin/node-adapter/-/node-adapter-40.100.7.tgz", + "integrity": "sha512-0QiY6BJTIuD04S02j/32ixhFEllVxYyAnruWTm/FkAPvhgGAcwiuCnoQt9RBK0fxTkzREHijSZ7GvQMrccgpXQ==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@openfin/core": "40.100.7", + "@types/node": "^20.14.2", + "lodash": "^4.17.21", + "ws": "^7.3.0" + } + }, + "node_modules/@openfin/node-adapter/node_modules/@types/node": { + "version": "20.17.6", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@types/node/-/node-20.17.6.tgz", + "integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@openfin/node-adapter/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -12039,7 +12092,6 @@ }, "node_modules/lodash": { "version": "4.17.21", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -17821,7 +17873,6 @@ }, "node_modules/ws": { "version": "7.4.4", - "dev": true, "license": "MIT", "engines": { "node": ">=8.3.0" diff --git a/package.json b/package.json index 8b3f7ea7c..db4ff33f3 100644 --- a/package.json +++ b/package.json @@ -221,6 +221,7 @@ "typescript": "^4.9.5" }, "dependencies": { + "@openfin/node-adapter": "^40.100.7", "@types/lazy-brush": "^1.0.0", "adm-zip": "^0.5.10", "bplist-parser": "^0.3.2", diff --git a/spec/config.spec.ts b/spec/config.spec.ts index dfed88faf..2dbae3337 100644 --- a/spec/config.spec.ts +++ b/spec/config.spec.ts @@ -98,6 +98,7 @@ describe('config', () => { 'latestAutoUpdateChannelEnabled', 'betaAutoUpdateChannelEnabled', 'browserLoginRetryTimeout', + 'openfin', ]; const globalConfig: object = { url: 'test' }; const userConfig: object = { configVersion: '4.0.1' }; diff --git a/spec/mainApiHandler.spec.ts b/spec/mainApiHandler.spec.ts index e8f28b597..ca3bd5427 100644 --- a/spec/mainApiHandler.spec.ts +++ b/spec/mainApiHandler.spec.ts @@ -2,6 +2,7 @@ import { activityDetection } from '../src/app/activity-detection'; import * as c9PipeHandler from '../src/app/c9-pipe-handler'; import { downloadHandler } from '../src/app/download-handler'; import '../src/app/main-api-handler'; +import { openfinHandler } from '../src/app/openfin-handler'; import { protocolHandler } from '../src/app/protocol-handler'; import { screenSnippet } from '../src/app/screen-snippet-handler'; import * as windowActions from '../src/app/window-actions'; @@ -10,9 +11,56 @@ import * as utils from '../src/app/window-utils'; import { apiCmds, apiName } from '../src/common/api-interface'; import { logger } from '../src/common/logger'; import { BrowserWindow, ipcMain } from './__mocks__/electron'; +import { connect } from '@openfin/node-adapter'; jest.mock('electron-log'); +jest.mock('../src/app/openfin-handler', () => { + return { + openfinHandler: { + connect: jest.fn(), + fireIntent: jest.fn(), + joinContextGroup: jest.fn(), + getContextGroups: jest.fn(), + getConnectionStatus: jest.fn(), + getInfo: jest.fn(), + getAllClientsInContextGroup: jest.fn(), + registerIntentHandler: jest.fn(), + unregisterIntentHandler: jest.fn(), + }, + }; +}); + +jest.mock('@openfin/node-adapter', () => ({ + connect: jest.fn(), +})); + +(connect as jest.Mock).mockResolvedValue({ + Interop: { + connectSync: jest.fn().mockReturnValue({ + onDisconnection: jest.fn(), + fireIntent: jest.fn(), + registerIntentHandler: jest.fn(), + }), + }, +}); + +jest.mock('../src/app/config-handler', () => { + return { + config: { + getConfigFields: jest.fn(() => { + return { + openfin: { + uuid: 'some-uuid', + licenseKey: 'some-license-key', + runtimeVersion: 'some-runtime-version', + }, + }; + }), + }, + }; +}); + jest.mock('../src/app/protocol-handler', () => { return { protocolHandler: { @@ -553,4 +601,114 @@ describe('main api handler', () => { expect(spy).toBeCalledWith(...expectedValue); }); }); + + describe('openfin api events', () => { + beforeEach(() => { + (connect as jest.Mock).mockResolvedValue({ + Interop: { + connectSync: jest.fn().mockReturnValue({ + onDisconnection: jest.fn(), + }), + }, + }); + (windowHandler.getMainWebContents as jest.Mock).mockReturnValue({ + send: jest.fn(), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should call `connect` correctly', () => { + const spy = jest.spyOn(openfinHandler, 'connect'); + const value = { + cmd: apiCmds.openfinConnect, + }; + + ipcMain.send(apiName.symphonyApi, value); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should call `fireIntent`', () => { + const spy = jest.spyOn(openfinHandler, 'fireIntent'); + const value = { + cmd: apiCmds.openfinFireIntent, + intent: { + name: 'ViewContact', + context: { + type: 'fdc3.contact', + name: 'Andy Young', + id: { + email: 'andy.young@example.com', + }, + }, + }, + }; + + ipcMain.send(apiName.symphonyApi, value); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should call `registerIntentHandler`', () => { + const spy = jest.spyOn(openfinHandler, 'registerIntentHandler'); + const value = { + cmd: apiCmds.openfinRegisterIntentHandler, + intentName: 'ViewContact', + }; + + ipcMain.send(apiName.symphonyApi, value); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should call `unregisterIntentHandler`', () => { + const spy = jest.spyOn(openfinHandler, 'unregisterIntentHandler'); + const value = { + cmd: apiCmds.openfinUnregisterIntentHandler, + intentName: 'ViewContact', + }; + + ipcMain.send(apiName.symphonyApi, value); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should call `joinContextGroup`', () => { + const spy = jest.spyOn(openfinHandler, 'joinContextGroup'); + const value = { + cmd: apiCmds.openfinJoinContextGroup, + contextGroupId: 'group-id', + }; + + ipcMain.send(apiName.symphonyApi, value); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should call `getContextGroups`', () => { + const spy = jest.spyOn(openfinHandler, 'getContextGroups'); + const value = { + cmd: apiCmds.openfinGetContextGroups, + }; + + ipcMain.send(apiName.symphonyApi, value); + + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should call `getAllClientsInContextGroup`', () => { + const spy = jest.spyOn(openfinHandler, 'getAllClientsInContextGroup'); + const value = { + cmd: apiCmds.openfinGetAllClientsInContextGroup, + contextGroupId: 'group-id', + }; + + ipcMain.send(apiName.symphonyApi, value); + + expect(spy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/spec/openfinHandler.spec.ts b/spec/openfinHandler.spec.ts new file mode 100644 index 000000000..3e2a6a1e3 --- /dev/null +++ b/spec/openfinHandler.spec.ts @@ -0,0 +1,111 @@ +import { ConnectConfig } from 'openfin/_v2/transport/wire'; +import { openfinHandler } from '../src/app/openfin-handler'; +import { connect } from '@openfin/node-adapter'; + +jest.mock('@openfin/node-adapter', () => ({ + connect: jest.fn(), +})); + +(connect as jest.Mock).mockResolvedValue({ + Interop: { + connectSync: jest.fn().mockReturnValue({ + onDisconnection: jest.fn(), + fireIntent: jest.fn(), + registerIntentHandler: jest.fn(), + joinContextGroup: jest.fn(), + getContextGroups: jest.fn(), + getAllClientsInContextGroup: jest.fn(), + }), + }, +}); + +describe('Openfin', () => { + let connectMock; + beforeAll(async () => { + connectMock = await connect({} as ConnectConfig); + }); + + it('should not be connected', () => { + const info = openfinHandler.getInfo(); + const isConnected = openfinHandler.getConnectionStatus(); + + expect(info.isConnected).toBeFalsy(); + expect(isConnected).toBeFalsy(); + }); + + it('should connect', async () => { + const connectSyncSpy = jest.spyOn(connectMock.Interop, 'connectSync'); + + await openfinHandler.connect(); + const info = openfinHandler.getInfo(); + const isConnected = openfinHandler.getConnectionStatus(); + + expect(connect).toHaveBeenCalled(); + expect(connectSyncSpy).toHaveBeenCalledTimes(1); + expect(info.isConnected).toBeTruthy(); + expect(isConnected).toBeTruthy(); + }); + + it('should fire an intent', async () => { + const connectSyncMock = await connectMock.Interop.connectSync(); + const fireIntentSpy = jest.spyOn(connectSyncMock, 'fireIntent'); + + await openfinHandler.connect(); + const customIntent = { + type: 'fdc3.contact', + name: 'Andy Young', + id: { + email: 'andy.young@example.com', + }, + }; + await openfinHandler.fireIntent(customIntent); + + expect(fireIntentSpy).toHaveBeenCalledTimes(1); + }); + + it('should register an intent handler', async () => { + const connectSyncMock = await connectMock.Interop.connectSync(); + const intentHandlerRegistrationSpy = jest.spyOn( + connectSyncMock, + 'registerIntentHandler', + ); + + await openfinHandler.connect(); + await openfinHandler.registerIntentHandler('my-intent'); + + expect(intentHandlerRegistrationSpy).toHaveBeenCalledTimes(1); + }); + + it('should join a context group', async () => { + const connectSyncMock = await connectMock.Interop.connectSync(); + const joinContextGroupSpy = jest.spyOn(connectSyncMock, 'joinContextGroup'); + + await openfinHandler.connect(); + await openfinHandler.joinContextGroup('contextGroupId'); + + expect(joinContextGroupSpy).toHaveBeenCalledTimes(1); + }); + + it('should return all context groups', async () => { + const connectSyncMock = await connectMock.Interop.connectSync(); + const getContextGroupsSpy = jest.spyOn(connectSyncMock, 'getContextGroups'); + + await openfinHandler.connect(); + await openfinHandler.getContextGroups(); + + expect(getContextGroupsSpy).toHaveBeenCalledTimes(1); + }); + + it('should return all clients in a given context group', async () => { + const connectSyncMock = await connectMock.Interop.connectSync(); + const getAllClientsInContextGroupSpy = jest.spyOn( + connectSyncMock, + 'getAllClientsInContextGroup', + ); + + await openfinHandler.connect(); + await openfinHandler.getAllClientsInContextGroup('contextGroup1'); + + expect(getAllClientsInContextGroupSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/spec/postinstall.spec.ts b/spec/postinstall.spec.ts index 5344244c9..d6bf7905c 100644 --- a/spec/postinstall.spec.ts +++ b/spec/postinstall.spec.ts @@ -34,6 +34,7 @@ describe('Shell Script Field Validation', () => { 'notificationSettings', 'customFlags', 'permissions', + 'openfin', ]); // Read fields from post install script file @@ -54,12 +55,12 @@ describe('Shell Script Field Validation', () => { 'notificationSettings', 'customFlags', 'permissions', + 'openfin', ]); // Read fields from post install script file const scriptFields = extractSystemDefaults(scriptFilePath); scriptFields.splice(scriptFields.indexOf('ApplicationName'), 1); - expect(isArraySubset(scriptFields, filteredFields)).toBe(true); }); }); diff --git a/src/app/config-handler.ts b/src/app/config-handler.ts index b0aeef5d7..864c3f2df 100644 --- a/src/app/config-handler.ts +++ b/src/app/config-handler.ts @@ -83,6 +83,7 @@ export interface IConfig { isPodUrlEditable?: boolean; sdaInstallerMsiUrlEnabledVisible?: boolean; sdaInstallerMsiUrlBetaEnabledVisible?: boolean; + openfin?: IOpenfin; } export interface IGlobalConfig { @@ -163,6 +164,13 @@ export interface ICustomRectangle extends Partial { isFullScreen?: boolean; } +export interface IOpenfin { + uuid: string; + licenseKey: string; + runtimeVersion: string; + autoConnect: boolean; +} + class Config { public userConfig: IConfig | {}; public globalConfig: IConfig | {}; diff --git a/src/app/main-api-handler.ts b/src/app/main-api-handler.ts index 4902b2e70..18d5df804 100644 --- a/src/app/main-api-handler.ts +++ b/src/app/main-api-handler.ts @@ -59,6 +59,7 @@ import { getCommandLineArgs } from '../common/utils'; import callNotificationHelper from '../renderer/call-notification-helper'; import { autoUpdate, AutoUpdateTrigger } from './auto-update-handler'; import { SDAUserSessionActionTypes } from './bi/interface'; +import { openfinHandler } from './openfin-handler'; import { presenceStatus } from './presence-status-handler'; import { appStats } from './stats'; import { presenceStatusStore, sdaMenuStore } from './stores/index'; @@ -559,6 +560,21 @@ ipcMain.on( helpMenu.setValue(helpCenter); break; + case apiCmds.openfinConnect: + openfinHandler.connect(); + break; + case apiCmds.openfinFireIntent: + openfinHandler.fireIntent(arg.intent); + break; + case apiCmds.openfinJoinContextGroup: + openfinHandler.joinContextGroup(arg.contextGroupId, arg.target); + break; + case apiCmds.openfinRegisterIntentHandler: + openfinHandler.registerIntentHandler(arg.intentName); + break; + case apiCmds.openfinUnregisterIntentHandler: + openfinHandler.unregisterIntentHandler(arg.intentName); + break; default: break; } @@ -627,6 +643,14 @@ ipcMain.handle( return getContentWindowHandle(windowHandle); } break; + case apiCmds.openfinGetConnectionStatus: + return openfinHandler.getConnectionStatus(); + case apiCmds.openfinGetInfo: + return openfinHandler.getInfo(); + case apiCmds.openfinGetContextGroups: + return openfinHandler.getContextGroups(); + case apiCmds.openfinGetAllClientsInContextGroup: + return openfinHandler.getAllClientsInContextGroup(arg.contextGroupId); default: break; } diff --git a/src/app/openfin-handler.ts b/src/app/openfin-handler.ts new file mode 100644 index 000000000..52af092e8 --- /dev/null +++ b/src/app/openfin-handler.ts @@ -0,0 +1,140 @@ +import { connect } from '@openfin/node-adapter'; +import { logger } from '../common/openfin-logger'; +import { config, IConfig } from './config-handler'; +import { windowHandler } from './window-handler'; + +export class OpenfinHandler { + private interopClient; + private intentHandlerSubscriptions = new Map(); + private isConnected: boolean = false; + + /** + * Connection to interop brocker + */ + public async connect() { + logger.info('openfin-handler: connecting'); + const { openfin }: IConfig = config.getConfigFields(['openfin']); + if (openfin) { + const fin = await connect({ + uuid: openfin.uuid, + licenseKey: openfin.licenseKey, + runtime: { + version: openfin.runtimeVersion, + }, + }); + logger.info('openfin-handler: connected'); + logger.info('openfin-handler: connecting to interop broker'); + this.interopClient = fin.Interop.connectSync( + 'workspace-platform-starter', + ); + this.isConnected = true; + this.interopClient.onDisconnection((event) => { + const { brokerName } = event; + logger.warn( + `openfin-handler: Disconnected from Interop Broker ${brokerName} `, + ); + this.clearSubscriptions(); + }); + return; + } + logger.error('openfin-handler: missing openfin params to connect.'); + } + + /** + * Sends an intent to the Interop Broker + */ + public fireIntent(intent) { + this.interopClient.fireIntent(intent); + } + + /** + * Adds an intent handler for incoming intents + */ + public async registerIntentHandler(intentName: string) { + const unsubscriptionCallback = + await this.interopClient.registerIntentHandler( + this.intentHandler, + intentName, + ); + this.intentHandlerSubscriptions.set(intentName, unsubscriptionCallback); + } + + /** + * Removes an intent handler for a given intent + */ + public unregisterIntentHandler(intentName) { + const unsubscriptionCallback = + this.intentHandlerSubscriptions.get(intentName); + unsubscriptionCallback.unsubscribe(); + this.intentHandlerSubscriptions.delete(intentName); + } + + /** + * Join all Interop Clients at the given identity to context group contextGroupId. If no target is specified, it adds the sender to the context group. + */ + public async joinContextGroup(contextGroupId: string, target?: any) { + await this.interopClient.joinContextGroup(contextGroupId, target); + } + + /** + * Returns the Interop-Broker-defined context groups available for an entity to join. + */ + public async getContextGroups() { + return this.interopClient.getContextGroups(); + } + + /** + * Gets all clients for a context group. + */ + public getAllClientsInContextGroup(contextGroupId: string) { + return this.interopClient.getAllClientsInContextGroup(contextGroupId); + } + + /** + * Clears all openfin subscriptions + */ + public clearSubscriptions() { + this.isConnected = false; + this.interopClient = undefined; + this.intentHandlerSubscriptions.forEach( + (unsubscriptionCallback, intent) => { + try { + unsubscriptionCallback.unsubscribe(); + } catch (e) { + logger.error( + `openfin-handler: Error unsubscribing from intent ${intent}:`, + e, + ); + } + }, + ); + this.intentHandlerSubscriptions.clear(); + } + + /** + * Returns openfin connection status + */ + public getConnectionStatus(): boolean { + return this.isConnected; + } + + /** + * Returns connection status and provider name + */ + public getInfo() { + return { + provider: 'Openfin', + isConnected: this.getConnectionStatus(), + }; + } + + private intentHandler = (intent: any) => { + logger.info('openfin-handler: intent received - ', intent); + const mainWebContents = windowHandler.getMainWebContents(); + mainWebContents?.send('intent-received', intent.name); + }; +} + +const openfinHandler = new OpenfinHandler(); + +export { openfinHandler }; diff --git a/src/app/plist-handler.ts b/src/app/plist-handler.ts index 0c5210431..f3c2ac045 100644 --- a/src/app/plist-handler.ts +++ b/src/app/plist-handler.ts @@ -65,6 +65,13 @@ const PERMISSIONS = { openExternal: 'boolean', }; +const OPENFIN = { + uuid: 'string', + licenseKey: 'string', + runtimeVersion: 'string', + autoConnect: 'boolean', +}; + export const getAllUserDefaults = (): IConfig => { const settings: any = {}; @@ -113,6 +120,14 @@ export const getAllUserDefaults = (): IConfig => { PERMISSIONS[key], ); }); + + Object.keys(OPENFIN).map((key) => { + if (!settings.openfin) { + settings.openfin = {}; + } + settings.openfin[key] = systemPreferences.getUserDefault(key, OPENFIN[key]); + }); + logger.info('plist-handler: getting all user defaults', settings); return settings; }; @@ -161,6 +176,18 @@ export const setPlistFromPreviousSettings = ( } systemPreferences.setUserDefault(key, PERMISSIONS[key], value); }); + + Object.keys(OPENFIN).map((key) => { + let value = settings?.openfin?.[key]; + if (value === undefined) { + if (appGlobalConfig?.openfin?.[key] === undefined) { + return; + } + value = appGlobalConfig.openfin[key]; + } + systemPreferences.setUserDefault(key, OPENFIN[key], value); + }); + systemPreferences.setUserDefault('installVariant', 'string', getGuid()); }; diff --git a/src/app/window-utils.ts b/src/app/window-utils.ts index 1b45963b4..aed54ea6e 100644 --- a/src/app/window-utils.ts +++ b/src/app/window-utils.ts @@ -55,6 +55,7 @@ import { notification } from '../renderer/notification'; import { autoLaunchInstance } from './auto-launch-controller'; import { autoUpdate, AutoUpdateTrigger } from './auto-update-handler'; import { mainEvents } from './main-event-handler'; +import { openfinHandler } from './openfin-handler'; import { presenceStatus } from './presence-status-handler'; import { presenceStatusStore } from './stores'; interface IStyles { @@ -472,7 +473,8 @@ export const sanitize = (windowName: string): void => { if (mainWindow && windowName === mainWindow.winName) { // reset the badge count whenever an user refreshes the electron client showBadgeCount(0); - + // Clear all openfin subscriptions + openfinHandler.clearSubscriptions(); // Terminates the screen snippet process and screen share indicator frame on reload if (!isMac || !isLinux) { logger.info( diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index 571da223f..e02081865 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -1,3 +1,4 @@ +import { UUID } from 'crypto'; import { NativeImage, Size, Tray } from 'electron'; import { AutoUpdateTrigger } from '../app/auto-update-handler'; @@ -82,6 +83,16 @@ export enum apiCmds { registerPhoneNumberServices = 'register-phone-numbers-services', unregisterPhoneNumberServices = 'unregister-phone-numbers-services', getHelpInfo = 'get-help-info', + // Openfin API commands + openfinConnect = 'openfin-connect', + openfinFireIntent = 'openfin-fire-intent', + openfinRegisterIntentHandler = 'openfin-register-intent-handler', + openfinUnregisterIntentHandler = 'openfin-unregister-intent-handler', + openfinGetConnectionStatus = 'openfin-get-connection-status', + openfinGetInfo = 'openfin-get-info', + openfinJoinContextGroup = 'openfin-join-context-group', + openfinGetContextGroups = 'openfin-get-context-groups', + openfinGetAllClientsInContextGroup = 'openfin-get-all-clients-in-context-group', } export enum apiName { @@ -149,6 +160,18 @@ export interface IApiArgs { status: IPresenceStatus; protocols: PhoneNumberProtocol[]; menu?: any; + handler: any; + uuid: UUID; + intent: any; + intentHandler: any; + intentName: any; + infoForIntentOptions: any; + context: any; + sessionContextGroupId: any; + contextForIntent: any; + contextType: any; + contextGroupId: string; + target: any; } export type Themes = 'light' | 'dark'; diff --git a/src/common/config-interface.ts b/src/common/config-interface.ts index f78db3d8a..d1686c7a7 100644 --- a/src/common/config-interface.ts +++ b/src/common/config-interface.ts @@ -8,4 +8,10 @@ export const ConfigFieldsDefaultValues: Partial = { latestAutoUpdateChannelEnabled: true, betaAutoUpdateChannelEnabled: true, browserLoginRetryTimeout: '5', + openfin: { + uuid: '', + licenseKey: '', + runtimeVersion: '', + autoConnect: false, + }, }; diff --git a/src/common/openfin-logger.ts b/src/common/openfin-logger.ts new file mode 100644 index 000000000..1eacbf81e --- /dev/null +++ b/src/common/openfin-logger.ts @@ -0,0 +1,5 @@ +import { Logger } from './loggerBase'; + +const logger = new Logger('openfin'); + +export { logger }; diff --git a/src/renderer/preload-main.ts b/src/renderer/preload-main.ts index 13355da93..d43596941 100644 --- a/src/renderer/preload-main.ts +++ b/src/renderer/preload-main.ts @@ -105,6 +105,19 @@ if (ssfWindow.ssf) { registerPhoneNumberServices: ssfWindow.ssf.registerPhoneNumberServices, unregisterPhoneNumberServices: ssfWindow.ssf.unregisterPhoneNumberServices, }); + + contextBridge.exposeInMainWorld('openfin', { + init: ssfWindow.ssf.openfinInit, + getInfo: ssfWindow.ssf.openfinGetInfo, + getConnectionStatus: ssfWindow.ssf.openfinGetConnectionStatus, + fireIntent: ssfWindow.ssf.openfinFireIntent, + registerIntentHandler: ssfWindow.ssf.openfinRegisterIntentHandler, + unregisterIntentHandler: ssfWindow.ssf.openfinUnregisterIntentHandler, + getContextGroups: ssfWindow.ssf.openfinGetContextGroups, + joinContextGroup: ssfWindow.ssf.openfinJoinContextGroup, + getAllClientsInContextGroup: + ssfWindow.ssf.openfinGetAllClientsInContextGroup, + }); } /** diff --git a/src/renderer/ssf-api.ts b/src/renderer/ssf-api.ts index 33a40641c..7c568d6bd 100644 --- a/src/renderer/ssf-api.ts +++ b/src/renderer/ssf-api.ts @@ -66,12 +66,14 @@ export interface ILocalObject { c9MessageCallback?: (status: IShellStatus) => void; updateMyPresenceCallback?: (presence: EPresenceStatusCategory) => void; phoneNumberCallback?: (arg: string) => void; + intentsCallbacks: {}; writeImageToClipboard?: (blob: string) => void; getHelpInfo?: () => Promise; } const local: ILocalObject = { ipcRenderer, + intentsCallbacks: {}, }; const notificationActionCallbacks = new Map< @@ -952,6 +954,107 @@ export class SSFApi { }); } + /** + * Openfin Interop client initialization + */ + public openfinInit(): void { + local.ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.openfinConnect, + }); + } + + /** + * Returns provider and connection status + */ + public async openfinGetInfo() { + const info = await local.ipcRenderer.invoke(apiName.symphonyApi, { + cmd: apiCmds.openfinGetInfo, + }); + return info; + } + + /** + * Fires an intent + */ + public openfinFireIntent(intent: any): void { + local.ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.openfinFireIntent, + intent, + }); + } + + /** + * + * Returns Openfin connection status + */ + public async openfinGetConnectionStatus() { + const connectionStatus = await local.ipcRenderer.invoke( + apiName.symphonyApi, + { + cmd: apiCmds.openfinGetConnectionStatus, + }, + ); + return connectionStatus; + } + + /** + * Registers a handler for a given intent + */ + public openfinRegisterIntentHandler( + intentHandler: any, + intentName: any, + ): void { + local.intentsCallbacks[intentName] = intentHandler; + local.ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.openfinRegisterIntentHandler, + intentName, + }); + } + + /** + * Unregisters a handler based on a given intent name + * @param intentName + */ + public openfinUnregisterIntentHandler(intentName: string): void { + local.ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.openfinUnregisterIntentHandler, + intentName, + }); + } + + /** + * Returns openfin context groups + */ + public async openfinGetContextGroups() { + const contextGroups = await local.ipcRenderer.invoke(apiName.symphonyApi, { + cmd: apiCmds.openfinGetContextGroups, + }); + return contextGroups; + } + + /** + * Allows to join an Openfin context group + * @param contextGroupId + * @param target + */ + public openfinJoinContextGroup(contextGroupId: string, target?: any) { + local.ipcRenderer.send(apiName.symphonyApi, { + cmd: apiCmds.openfinJoinContextGroup, + contextGroupId, + target, + }); + } + + /** + * Returns registered clients in a given context group + */ + public openfinGetAllClientsInContextGroup(contextGroupId: string) { + return local.ipcRenderer.invoke(apiName.symphonyApi, { + cmd: apiCmds.openfinGetAllClientsInContextGroup, + contextGroupId, + }); + } + /** * Allows JS to register SDA for phone numbers clicks * @param {Function} phoneNumberCallback callback function invoked when receiving a phone number for calls/sms @@ -1290,6 +1393,12 @@ local.ipcRenderer.on( }, ); +local.ipcRenderer.on('intent-received', (_event: Event, intentName: string) => { + if (typeof intentName === 'string' && local.intentsCallbacks[intentName]) { + local.intentsCallbacks[intentName](); + } +}); + // Invoked whenever the app is reloaded/navigated const sanitize = (): void => { if (window.name === apiName.mainWindowName) { @@ -1298,6 +1407,7 @@ const sanitize = (): void => { windowName: window.name, }); } + local.intentsCallbacks = {}; }; // listens for the online/offline events and updates the main process