diff --git a/config/Symphony.config b/config/Symphony.config index 912f5acca..2cdeb2c52 100644 --- a/config/Symphony.config +++ b/config/Symphony.config @@ -4,6 +4,7 @@ "autoUpdateChannel": "latest", "isAutoUpdateEnabled": true, "autoUpdateCheckInterval": "30", + "enableSeamlessLogin": false, "overrideUserAgent": false, "minimizeOnClose" : "ENABLED", "launchOnStartup" : "ENABLED", diff --git a/installer/win/WixSharpInstaller/Symphony.cs b/installer/win/WixSharpInstaller/Symphony.cs index 73da0b29c..511784a25 100644 --- a/installer/win/WixSharpInstaller/Symphony.cs +++ b/installer/win/WixSharpInstaller/Symphony.cs @@ -157,6 +157,7 @@ static public void Main(string[] args) new PublicProperty("POINTER_LOCK", "true"), new PublicProperty("USER_DATA_PATH", ""), new PublicProperty("OVERRIDE_USER_AGENT", "false"), + new PublicProperty("ENABLE_SEAMLESS_LOGIN", "false"), new PublicProperty("CHROME_FLAGS", ""), new Property("MSIINSTALLPERUSER", "1"), new Property("PROGRAMSFOLDER", System.Environment.ExpandEnvironmentVariables(@"%PROGRAMFILES%")) @@ -185,7 +186,7 @@ static public void Main(string[] args) new ElevatedManagedAction(CustomActions.UpdateConfig, Return.check, When.After, Step.InstallFiles, Condition.NOT_BeingRemoved ) { // The UpdateConfig action needs the built-in property INSTALLDIR as well as most of the custom properties - UsesProperties = "INSTALLDIR,POD_URL,CONTEXT_ORIGIN_URL,MINIMIZE_ON_CLOSE,ALWAYS_ON_TOP,AUTO_START,BRING_TO_FRONT,MEDIA,LOCATION,NOTIFICATIONS,MIDI_SYSEX,POINTER_LOCK,FULL_SCREEN,OPEN_EXTERNAL,CUSTOM_TITLE_BAR,DEV_TOOLS_ENABLED,AUTO_LAUNCH_PATH,USER_DATA_PATH,OVERRIDE_USER_AGENT,CHROME_FLAGS" + UsesProperties = "INSTALLDIR,POD_URL,CONTEXT_ORIGIN_URL,MINIMIZE_ON_CLOSE,ALWAYS_ON_TOP,AUTO_START,BRING_TO_FRONT,MEDIA,LOCATION,NOTIFICATIONS,MIDI_SYSEX,POINTER_LOCK,FULL_SCREEN,OPEN_EXTERNAL,CUSTOM_TITLE_BAR,DEV_TOOLS_ENABLED,AUTO_LAUNCH_PATH,USER_DATA_PATH,OVERRIDE_USER_AGENT,CHROME_FLAGS,ENABLE_SEAMLESS_LOGIN" }, // CleanRegistry @@ -359,7 +360,7 @@ public static ActionResult UpdateConfig(Session session) data = ReplaceBooleanProperty(data, "fullscreen", session.Property("FULL_SCREEN")); data = ReplaceBooleanProperty(data, "devToolsEnabled", session.Property("DEV_TOOLS_ENABLED")); data = ReplaceBooleanProperty(data, "overrideUserAgent", session.Property("OVERRIDE_USER_AGENT")); - + data = ReplaceBooleanProperty(data, "enableSeamlessLogin", session.Property("ENABLE_SEAMLESS_LOGIN")); // Write the contents back to the file System.IO.File.WriteAllText(filename, data); } diff --git a/installer/win/install_instructions_win.md b/installer/win/install_instructions_win.md index 252e2e00d..ea331fbd1 100644 --- a/installer/win/install_instructions_win.md +++ b/installer/win/install_instructions_win.md @@ -529,3 +529,28 @@ Expected values: msiexec /i Symphony.msi CHROME_FLAGS="--debug --debug2 --debug3" +------------------------------------------------------------------- +### ENABLE_SEAMLESS_LOGIN + +Expected values: + +* "true" + SDA will authenticate the user by relying on third-party browser +* "false" + SDA will authenticate the user in SDA + +#### Example, install with user-agent override + + msiexec /i Symphony.msi ENABLE_SEAMLESS_LOGIN="true" + +#### Example, install without user-agent override + + msiexec /i Symphony.msi ENABLE_SEAMLESS_LOGIN="false" + +or + + msiexec /i Symphony.msi + + + +------------------------------------------------------------------- diff --git a/package-lock.json b/package-lock.json index b55e37f34..d8fda9c21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5399,15 +5399,21 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001265", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz", - "integrity": "sha1-BhPJ5ski5CJ5Lm/O/fmjr+7k+MM=", + "version": "1.0.30001420", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/caniuse-lite/-/caniuse-lite-1.0.30001420.tgz", + "integrity": "sha1-9i818FHgttJVMs83Z3bUHkW0fvY=", "dev": true, - "license": "CC-BY-4.0", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ], + "license": "CC-BY-4.0" }, "node_modules/capture-exit": { "version": "2.0.0", @@ -24884,9 +24890,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001265", - "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz", - "integrity": "sha1-BhPJ5ski5CJ5Lm/O/fmjr+7k+MM=", + "version": "1.0.30001420", + "resolved": "https://repo.symphony.com/artifactory/api/npm/npm-virtual-dev/caniuse-lite/-/caniuse-lite-1.0.30001420.tgz", + "integrity": "sha1-9i818FHgttJVMs83Z3bUHkW0fvY=", "dev": true }, "capture-exit": { diff --git a/package.json b/package.json index 601a9945f..b4436c3f0 100644 --- a/package.json +++ b/package.json @@ -239,4 +239,4 @@ "pre-commit": "pretty-quick --staged && npm run lint" } } -} +} \ No newline at end of file diff --git a/spec/__mocks__/electron.ts b/spec/__mocks__/electron.ts index e06376d1b..9ded58fed 100644 --- a/spec/__mocks__/electron.ts +++ b/spec/__mocks__/electron.ts @@ -264,6 +264,7 @@ export const BrowserWindow = { export const session = { defaultSession: { clearCache: jest.fn(), + cookies: jest.fn(), }, }; diff --git a/spec/__snapshots__/welcome.spec.ts.snap b/spec/__snapshots__/welcome.spec.ts.snap index 60886a49c..6f58e74d0 100644 --- a/spec/__snapshots__/welcome.spec.ts.snap +++ b/spec/__snapshots__/welcome.spec.ts.snap @@ -4,47 +4,151 @@ exports[`welcome should render correctly 1`] = `
- Symphony Logo -
-
-

- Pod URL -

+ + + + + + + + + + + +
+ + Welcome to the largest global community in financial services with over + + + half a million users + + + and more than + + + 1,000 institutions. + +
+
- + + Log in with your pod URL + +
+
+ + Pod URL + +
+ + +
-
`; diff --git a/spec/protocolHandler.spec.ts b/spec/protocolHandler.spec.ts index c4abfa611..39bc011a6 100644 --- a/spec/protocolHandler.spec.ts +++ b/spec/protocolHandler.spec.ts @@ -12,6 +12,14 @@ jest.mock('../src/common/utils', () => { }; }); +jest.mock('../src/app/window-handler', () => { + return { + windowHandler: { + url: '', + }, + }; +}); + jest.mock('../src/common/env', () => { return { isWindowsOS: false, @@ -34,6 +42,14 @@ jest.mock('../src/common/logger', () => { }; }); +jest.mock('../src/app/config-handler', () => { + return { + config: { + getUserConfigFields: jest.fn(() => ''), + }, + }; +}); + describe('protocol handler', () => { let protocolHandlerInstance; diff --git a/spec/welcome.spec.ts b/spec/welcome.spec.ts index 846562510..d37c5089b 100644 --- a/spec/welcome.spec.ts +++ b/spec/welcome.spec.ts @@ -9,6 +9,8 @@ describe('welcome', () => { url: 'https://my.symphony.com', message: '', urlValid: true, + isPodConfigured: false, + isSeamlessLoginEnabled: true, }; const onLabelEvent = 'on'; const removeListenerLabelEvent = 'removeListener'; @@ -94,7 +96,7 @@ describe('welcome', () => { it('should set pod url', () => { const spy = jest.spyOn(Welcome.prototype, 'setState'); - const setPodUrlSpy = jest.spyOn(Welcome.prototype, 'setPodUrl'); + const setPodUrlSpy = jest.spyOn(Welcome.prototype, 'login'); const wrapper = shallow(React.createElement(Welcome)); ipcRenderer.send('welcome', welcomeMock); @@ -104,4 +106,18 @@ describe('welcome', () => { expect(setPodUrlSpy).toBeCalled(); expect(spy).toBeCalledWith(welcomeMock); }); + + it('should not show pod url input field', () => { + const welcomeMock = { + url: 'https://my.symphony.com', + message: '', + urlValid: true, + isPodConfigured: true, + isSeamlessLoginEnabled: true, + }; + const wrapper = shallow(React.createElement(Welcome)); + ipcRenderer.send('welcome', welcomeMock); + const podUrlBox = `input.Welcome-main-container-podurl-box`; + expect(wrapper.find(podUrlBox).getElements()).toEqual([]); + }); }); diff --git a/src/app/app-menu.ts b/src/app/app-menu.ts index 7fcf2d7fc..559015960 100644 --- a/src/app/app-menu.ts +++ b/src/app/app-menu.ts @@ -17,7 +17,7 @@ import { MenuActionTypes, } from './analytics-handler'; import { CloudConfigDataTypes, config, IConfig } from './config-handler'; -import { gpuRestartDialog, titleBarChangeDialog } from './dialog-handler'; +import { restartDialog, titleBarChangeDialog } from './dialog-handler'; import { exportCrashDumps, exportLogs } from './reports-handler'; import { registerConsoleMessages, @@ -84,6 +84,7 @@ const menuItemConfigFields = [ 'isCustomTitleBar', 'devToolsEnabled', 'isAutoUpdateEnabled', + 'enableSeamlessLogin', ]; let { @@ -95,6 +96,7 @@ let { isCustomTitleBar, devToolsEnabled, isAutoUpdateEnabled, + enableSeamlessLogin, } = config.getConfigFields(menuItemConfigFields) as IConfig; let initialAnalyticsSent = false; const CORP_URL = 'https://corporate.symphony.com'; @@ -240,6 +242,7 @@ export class AppMenu { isCustomTitleBar = configData.isCustomTitleBar; devToolsEnabled = configData.devToolsEnabled; isAutoUpdateEnabled = configData.isAutoUpdateEnabled; + enableSeamlessLogin = configData.enableSeamlessLogin; // fetch updated cloud config this.cloudConfig = config.getFilteredCloudConfigFields( this.menuItemConfigFields, @@ -299,6 +302,7 @@ export class AppMenu { windowHandler.createAboutAppWindow(windowName); }, }, + this.buildSeparator(), { click: (_item) => { autoUpdate.checkUpdates(AutoUpdateTrigger.MANUAL); @@ -513,6 +517,13 @@ export class AppMenu { enabled: !bringToFrontCC || bringToFrontCC === CloudConfigDataTypes.NOT_SET, }, + { + label: i18n.t('Third-party browser login')(), + checked: enableSeamlessLogin, + click: () => { + restartDialog({ enableSeamlessLogin: !enableSeamlessLogin }); + }, + }, this.buildSeparator(), { label: @@ -664,7 +675,7 @@ export class AppMenu { ? i18n.t('Enable GPU')() : i18n.t('Disable GPU')(), click: () => { - gpuRestartDialog(!this.disableGpu); + restartDialog({ disableGpu: !this.disableGpu }); }, }, { diff --git a/src/app/config-handler.ts b/src/app/config-handler.ts index 64ef14616..8b90fad6a 100644 --- a/src/app/config-handler.ts +++ b/src/app/config-handler.ts @@ -58,6 +58,7 @@ export interface IConfig { installVariant?: string; bootCount?: number; startedAfterAutoUpdate?: boolean; + enableSeamlessLogin: boolean; } export interface IGlobalConfig { diff --git a/src/app/dialog-handler.ts b/src/app/dialog-handler.ts index 638526c6b..693b94d1c 100644 --- a/src/app/dialog-handler.ts +++ b/src/app/dialog-handler.ts @@ -200,10 +200,10 @@ export const titleBarChangeDialog = async ( }; /** - * Displays a dialog to restart app upon changing gpu settings - * @param disableGpu + * Displays a dialog to restart app upon changing config settings + * @param config */ -export const gpuRestartDialog = async (disableGpu: boolean) => { +export const restartDialog = async (configFields: any) => { const focusedWindow = BrowserWindow.getFocusedWindow(); if (!focusedWindow || !windowExists(focusedWindow)) { return; @@ -218,7 +218,7 @@ export const gpuRestartDialog = async (disableGpu: boolean) => { cancelId: 1, }; const { response } = await dialog.showMessageBox(focusedWindow, options); - await config.updateUserConfig({ disableGpu }); + await config.updateUserConfig(configFields); if (response === 0) { app.relaunch(); app.exit(); diff --git a/src/app/main-api-handler.ts b/src/app/main-api-handler.ts index a46551df5..623372f5b 100644 --- a/src/app/main-api-handler.ts +++ b/src/app/main-api-handler.ts @@ -1,20 +1,23 @@ import { - app, BrowserWindow, clipboard, desktopCapturer, dialog, ipcMain, + shell, systemPreferences, } from 'electron'; +import fetch from 'electron-fetch'; import { apiCmds, apiName, IApiArgs, + IAuthResponse, INotificationData, } from '../common/api-interface'; import { i18n, LocaleType } from '../common/i18n'; import { logger } from '../common/logger'; +import { whitelistHandler } from '../common/whitelist-handler'; import { activityDetection } from './activity-detection'; import { analytics } from './analytics-handler'; import appStateHandler from './app-state-handler'; @@ -46,6 +49,7 @@ import { windowExists, } from './window-utils'; +import { getCommandLineArgs } from '../common/utils'; import { autoUpdate, AutoUpdateTrigger } from './auto-update-handler'; // Swift search API @@ -63,6 +67,9 @@ const broadcastMessage = (method, data) => { mainEvents.publish(apiCmds.onSwiftSearchMessage, [method, data]); }; +const getSeamlessLoginUrl = (pod: string) => + `${pod}/login/sso/initsso?RelayState=${pod}/client-bff/device-login/index.html?callbackScheme=symphony&action=login`; +const AUTH_STATUS_PATH = '/login/checkauth?type=user'; /** * Handle API related ipc messages from renderers. Only messages from windows * we have created are allowed. @@ -350,10 +357,50 @@ ipcMain.on( mainWebContents.focus(); } break; - case apiCmds.setPodUrl: - await config.updateUserConfig({ url: arg.newPodUrl }); - app.relaunch(); - app.exit(); + case apiCmds.seamlessLogin: + if (!arg.isPodConfigured) { + await config.updateUserConfig({ url: arg.newPodUrl }); + } + const urlFromCmd = getCommandLineArgs(process.argv, '--url=', false); + const { url: userConfigURL } = config.getUserConfigFields(['url']); + const { url: globalConfigURL } = config.getGlobalConfigFields(['url']); + const podUrl = urlFromCmd + ? urlFromCmd.substr(6) + : userConfigURL + ? userConfigURL + : globalConfigURL; + const { subdomain, domain, tld } = whitelistHandler.parseDomain(podUrl); + const formattedPodUrl = `https://${subdomain}.${domain}${tld}`; + const loginUrl = getSeamlessLoginUrl(formattedPodUrl); + logger.info( + 'main-api-handler:', + 'check if sso is enabled for the pod', + formattedPodUrl, + ); + const response = await fetch(`${formattedPodUrl}${AUTH_STATUS_PATH}`); + const authResponse = (await response.json()) as IAuthResponse; + logger.info('main-api-handler:', 'check auth response', authResponse); + if ( + arg.isSeamlessLoginEnabled && + authResponse.authenticationType === 'sso' + ) { + logger.info( + 'main-api-handler:', + 'seamless login is enabled - logging in', + loginUrl, + ); + await shell.openExternal(loginUrl); + } else { + logger.info( + 'main-api-handler:', + 'seamless login is not enabled - loading main window with', + formattedPodUrl, + ); + const mainWebContents = windowHandler.getMainWebContents(); + if (mainWebContents && !mainWebContents.isDestroyed()) { + mainWebContents.loadURL(formattedPodUrl); + } + } break; case apiCmds.setBroadcastMessage: if (swiftSearchInstance) { diff --git a/src/app/protocol-handler.ts b/src/app/protocol-handler.ts index 1dbf24569..1d908399d 100644 --- a/src/app/protocol-handler.ts +++ b/src/app/protocol-handler.ts @@ -1,9 +1,11 @@ -import { WebContents } from 'electron'; +import { session, WebContents } from 'electron'; import { apiName } from '../common/api-interface'; import { isMac } from '../common/env'; import { logger } from '../common/logger'; import { getCommandLineArgs } from '../common/utils'; +import { config } from './config-handler'; import { activate } from './window-actions'; +import { windowHandler } from './window-handler'; enum protocol { SymphonyProtocol = 'symphony://', @@ -45,7 +47,10 @@ class ProtocolHandler { * @param url {String} * @param isAppRunning {Boolean} - whether the application is running */ - public sendProtocol(url: string, isAppRunning: boolean = true): void { + public async sendProtocol( + url: string, + isAppRunning: boolean = true, + ): Promise { if (url && url.length > 2083) { logger.info( `protocol-handler: protocol handler url length is greater than 2083, not performing any action!`, @@ -55,6 +60,12 @@ class ProtocolHandler { logger.info( `protocol handler: processing protocol request for the url ${url}!`, ); + // Handle protocol for Seamless login + if (url?.includes('skey') && url?.includes('anticsrf')) { + await this.handleSeamlessLogin(url); + return; + } + if (!this.preloadWebContents || !isAppRunning) { logger.info( `protocol handler: app was started from the protocol request. Caching the URL ${url}!`, @@ -62,6 +73,7 @@ class ProtocolHandler { this.protocolUri = url; return; } + // This is needed for mac OS as it brings pop-outs to foreground // (if it has been previously focused) instead of main window if (isMac) { @@ -95,6 +107,38 @@ class ProtocolHandler { this.sendProtocol(protocolUriFromArgv, isAppAlreadyOpen); } } + + /** + * Sets session cookies and navigates to the pod url + */ + public async handleSeamlessLogin(protocolUri: string): Promise { + const { url } = config.getUserConfigFields(['url']); + if (protocolUri) { + const urlParams = new URLSearchParams(new URL(protocolUri).search); + const skeyValue = urlParams.get('skey'); + const anticsrfValue = urlParams.get('anticsrf'); + if (skeyValue) { + await session.defaultSession.cookies.set({ + url, + name: 'skey', + value: skeyValue, + }); + } + if (anticsrfValue) { + await session.defaultSession.cookies.set({ + url, + name: 'anti-csrf-cookie', + value: anticsrfValue, + }); + } + logger.info('protocol-handler: cookies has been set'); + const mainWebContents = windowHandler.getMainWebContents(); + if (mainWebContents && !mainWebContents?.isDestroyed() && url) { + logger.info('protocol-handler: redirecting main webContents', url); + mainWebContents?.loadURL(url); + } + } + } } const protocolHandler = new ProtocolHandler(); diff --git a/src/app/window-handler.ts b/src/app/window-handler.ts index 61b8eed5a..2fd9bde0a 100644 --- a/src/app/window-handler.ts +++ b/src/app/window-handler.ts @@ -81,6 +81,9 @@ export enum ClientSwitchType { CLIENT_2_0_DAILY = 'CLIENT_2_0_DAILY', } +export const DEFAULT_WELCOME_SCREEN_WIDTH: number = 542; +export const DEFAULT_WELCOME_SCREEN_HEIGHT: number = 333; + const MAIN_WEB_CONTENTS_EVENTS = ['enter-full-screen', 'leave-full-screen']; const SHORTCUT_KEY_THROTTLE = 1000; // 1sec @@ -104,8 +107,6 @@ export interface ICustomBrowserView extends Electron.BrowserView { // Default window width & height export const DEFAULT_WIDTH: number = 900; export const DEFAULT_HEIGHT: number = 900; -export const DEFAULT_WELCOME_SCREEN_WIDTH: number = 542; -export const DEFAULT_WELCOME_SCREEN_HEIGHT: number = 333; export const TITLE_BAR_HEIGHT: number = 32; export const IS_SAND_BOXED: boolean = true; export const IS_NODE_INTEGRATION_ENABLED: boolean = false; @@ -131,6 +132,7 @@ export class WindowHandler { } public mainView: ICustomBrowserView | null = null; public titleBarView: ICustomBrowserView | null = null; + public welcomeScreenWindow: BrowserWindow | null = null; public mainWebContents: WebContents | undefined; public appMenu: AppMenu | null = null; public isAutoReload: boolean = false; @@ -145,7 +147,6 @@ export class WindowHandler { public isLoggedIn: boolean = false; public isAutoUpdating: boolean = false; public screenShareIndicatorFrameUtil: string; - private defaultPodUrl: string = 'https://[POD].symphony.com'; private contextIsolation: boolean = true; private backgroundThrottling: boolean = false; private windowOpts: ICustomBrowserWindowConstructorOpts = @@ -158,7 +159,6 @@ export class WindowHandler { private loadFailError: string | undefined; private mainWindow: ICustomBrowserWindow | null = null; private aboutAppWindow: Electron.BrowserWindow | null = null; - private welcomeScreenWindow: Electron.BrowserWindow | null = null; private screenPickerWindow: Electron.BrowserWindow | null = null; private screenPickerPlaceholderWindow: Electron.BrowserWindow | null = null; private screenSharingIndicatorWindow: Electron.BrowserWindow | null = null; @@ -170,6 +170,9 @@ export class WindowHandler { private readonly opts: Electron.BrowserViewConstructorOptions | undefined; private hideOnCapture: boolean = false; private currentWindow?: string = undefined; + private isPodConfigured: boolean = false; + private shouldShowWelcomeScreen: boolean = true; + private didShowWelcomeScreen: boolean = false; constructor(opts?: Electron.BrowserViewConstructorOptions) { this.opts = opts; @@ -213,6 +216,7 @@ export class WindowHandler { 'customFlags', 'clientSwitch', 'enableRendererLogs', + 'enableSeamlessLogin', ]); logger.info( `window-handler: main windows initialized with following config data`, @@ -238,6 +242,13 @@ export class WindowHandler { this.isCustomTitleBar = isWindowsOS && this.config.isCustomTitleBar === CloudConfigDataTypes.ENABLED; + // Get url to load from cmd line or from global config file + const urlFromCmd = getCommandLineArgs(process.argv, '--url=', false); + this.isPodConfigured = !config.isFirstTimeLaunch(); + this.didShowWelcomeScreen = false; + this.shouldShowWelcomeScreen = + config.isFirstTimeLaunch() || this.config.enableSeamlessLogin; + this.windowOpts = { ...this.getWindowOpts( { @@ -285,20 +296,6 @@ export class WindowHandler { ); logger.info(`window-handler: setting url ${this.url} from config file!`); - // Get url to load from cmd line or from global config file - const urlFromCmd = getCommandLineArgs(process.argv, '--url=', false); - - // Displays welcome screen instead of starting the main application - if ( - config.isFirstTimeLaunch() && - this.globalConfig.url.indexOf('https://my.symphony.com') >= 0 && - urlFromCmd === null - ) { - this.url = this.defaultPodUrl; - this.showWelcomeScreen(); - return; - } - // set window opts with additional config this.mainWindow = new BrowserWindow({ ...this.windowOpts, @@ -382,6 +379,20 @@ export class WindowHandler { logger.info(`Loading main window with url ${this.url}`); const userAgent = this.getUserAgent(this.mainWindow.webContents); + // Displays welcome screen instead of starting the main application + if (this.shouldShowWelcomeScreen) { + this.url = format({ + pathname: require.resolve('../renderer/welcome.html'), + protocol: 'file', + query: { + componentName: 'welcome', + locale: i18n.getLocale(), + title: i18n.t('WelcomeText', 'Welcome')(), + }, + slashes: true, + }); + } + if ( this.config.isCustomTitleBar === CloudConfigDataTypes.ENABLED && isWindowsOS && @@ -513,9 +524,34 @@ export class WindowHandler { await this.mainWebContents?.loadURL(url, { userAgent }); return; } + logger.info('window-handler: did-finish-load, url: ' + this.url); if (this.mainWebContents && !this.mainWebContents.isDestroyed()) { + // Load welcome screen + if (this.shouldShowWelcomeScreen && !this.didShowWelcomeScreen) { + const userConfigUrl = + this.userConfig.url && + this.userConfig.url.indexOf('/login/sso/initsso') > -1 + ? this.userConfig.url.slice( + 0, + this.userConfig.url.indexOf('/login/sso/initsso'), + ) + : this.userConfig.url; + this.mainWebContents.send('page-load-welcome', { + locale: i18n.getLocale(), + resources: i18n.loadedResources, + }); + this.mainWebContents.send('welcome', { + url: userConfigUrl, + message: '', + urlValid: !!userConfigUrl, + isPodConfigured: this.isPodConfigured, + isSeamlessLoginEnabled: this.config.enableSeamlessLogin, + }); + this.didShowWelcomeScreen = true; + } + // Injects custom title bar and snack bar css into the webContents await injectStyles(this.mainWebContents, this.isCustomTitleBar); this.mainWebContents.send('page-load', { @@ -846,13 +882,6 @@ export class WindowHandler { return this.mainWindow; } - /** - * Gets the welcome screen window - */ - public getWelcomeScreenWindow(): BrowserWindow | null { - return this.welcomeScreenWindow; - } - /** * Gets the main browser webContents */ diff --git a/src/app/window-utils.ts b/src/app/window-utils.ts index bd742b943..45d56b658 100644 --- a/src/app/window-utils.ts +++ b/src/app/window-utils.ts @@ -384,8 +384,7 @@ export const updateLocale = async (locale: LocaleType): Promise => { * Displays a popup menu */ export const showPopupMenu = (opts: Electron.PopupOptions): void => { - const browserWindow = - windowHandler.getMainWindow() || windowHandler.getWelcomeScreenWindow(); + const browserWindow = windowHandler.getMainWindow(); if ( browserWindow && windowExists(browserWindow) && diff --git a/src/common/api-interface.ts b/src/common/api-interface.ts index 621189ff8..dca9d88b0 100644 --- a/src/common/api-interface.ts +++ b/src/common/api-interface.ts @@ -58,7 +58,6 @@ export enum apiCmds { isAeroGlassEnabled = 'is-aero-glass-enabled', showScreenSharePermissionDialog = 'show-screen-share-permission-dialog', getMediaAccessStatus = 'get-media-access-status', - setPodUrl = 'set-pod-url', setBroadcastMessage = 'set-broadcast-message', handleSwiftSearchMessageEvents = 'handle-shift-search-message-events', onSwiftSearchMessage = 'on-shift-search-message', @@ -73,6 +72,7 @@ export enum apiCmds { updateAndRestart = 'update-and-restart', downloadUpdate = 'download-update', checkForUpdates = 'check-for-updates', + seamlessLogin = 'seamless-login', } export enum apiName { @@ -120,6 +120,9 @@ export interface IApiArgs { requestId: number; mediaStatus: IMediaPermission; newPodUrl: string; + startUrl: string; + isPodConfigured: boolean; + isSeamlessLoginEnabled: boolean; swiftSearchData: any; types: string[]; thumbnailSize: Size; @@ -288,3 +291,13 @@ export interface ICloud9Pipe { write(data: Uint8Array): void; close(): void; } + +export type AuthType = 'password' | 'sso'; + +export interface IAuthResponse { + status: string; + podVersion: string; + authenticationType: AuthType; + ssoDisabledForMobile: boolean; + keymanagerUrl: string; +} diff --git a/src/locale/en-US.json b/src/locale/en-US.json index 6d9445fde..1392124f4 100644 --- a/src/locale/en-US.json +++ b/src/locale/en-US.json @@ -219,6 +219,13 @@ "Welcome": { "Continue": "Continue", "Enable Single Sign On": "Enable Single Sign On", + "Find your pod URL in your invitation email.": "Find your pod URL in your invitation email.", + "Welcome to the largest global community in financial services with over": "Welcome to the largest global community in financial services with over", + " half a million users": " half a million users", + " 1,000 institutions.": " 1,000 institutions.", + "Log in with your pod URL": "Log in with your pod URL", + "You’ll momentarily be redirected to your web browser.": "You’ll momentarily be redirected to your web browser.", + "log in": "log in", "Please enter a valid url": "Please enter a valid url", "Pod URL": "Pod URL", "SSO": "SSO", diff --git a/src/locale/en.json b/src/locale/en.json index d9626f0f1..7e19a3d4e 100644 --- a/src/locale/en.json +++ b/src/locale/en.json @@ -219,6 +219,13 @@ "Welcome": { "Continue": "Continue", "Enable Single Sign On": "Enable Single Sign On", + "Find your pod URL in your invitation email.": "Find your pod URL in your invitation email.", + "Welcome to the largest global community in financial services with over": "Welcome to the largest global community in financial services with over", + " half a million users": " half a million users", + " 1,000 institutions.": " 1,000 institutions.", + "Log in with your pod URL": "Log in with your pod URL", + "You’ll momentarily be redirected to your web browser.": "You’ll momentarily be redirected to your web browser.", + "log in": "log in", "Please enter a valid url": "Please enter a valid url", "Pod URL": "Pod URL", "SSO": "SSO", diff --git a/src/renderer/assets/welcome-symphony-logo.svg b/src/renderer/assets/welcome-symphony-logo.svg new file mode 100644 index 000000000..d5a4b2a2c --- /dev/null +++ b/src/renderer/assets/welcome-symphony-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/renderer/components/welcome.tsx b/src/renderer/components/welcome.tsx index 72e577805..22c2cf628 100644 --- a/src/renderer/components/welcome.tsx +++ b/src/renderer/components/welcome.tsx @@ -7,21 +7,31 @@ interface IState { url: string; message: string; urlValid: boolean; + isPodConfigured: boolean; + isSeamlessLoginEnabled: boolean; + isLoading: boolean; } const WELCOME_NAMESPACE = 'Welcome'; +const DEFAULT_MESSAGE = 'Find your pod URL in your invitation email.'; +const HEIGHT_WITH_POD_INPUT = '494px'; +const HEIGHT_WITHOUT_POD_INPUT = '376px'; +const DEFAULT_POD_URL = 'https://[POD].symphony.com'; export default class Welcome extends React.Component<{}, IState> { private readonly eventHandlers = { - onSetPodUrl: () => this.setPodUrl(), + onLogin: () => this.login(), }; constructor(props) { super(props); this.state = { - url: 'https://[POD].symphony.com', + url: DEFAULT_POD_URL, message: '', urlValid: false, + isPodConfigured: false, + isSeamlessLoginEnabled: true, + isLoading: false, }; this.updateState = this.updateState.bind(this); } @@ -30,41 +40,76 @@ export default class Welcome extends React.Component<{}, IState> { * Render the component */ public render(): JSX.Element { - const { url, message, urlValid } = this.state; + const { url, message, isPodConfigured } = this.state; return ( -
-
- {i18n.t('Symphony -
-
-

- {i18n.t('Pod URL', WELCOME_NAMESPACE)()} -

-
-
- -
+
+
+
+ {this.getWelcomeImage()}
- - + + {i18n.t( + 'Welcome to the largest global community in financial services with over', + WELCOME_NAMESPACE, + )()} + + + {i18n.t(' half a million users', WELCOME_NAMESPACE)()} + + {i18n.t(' and more than', WELCOME_NAMESPACE)()} + + {i18n.t(' 1,000 institutions.', WELCOME_NAMESPACE)()} + +
+ {!isPodConfigured && ( +
+
+ + {i18n.t('Log in with your pod URL', WELCOME_NAMESPACE)()} + +
+
+ {i18n.t('Pod URL', WELCOME_NAMESPACE)()} +
+ + +
+
+
+ )} + + {this.renderLoginButton()} + +
+ + {i18n.t( + 'You’ll momentarily be redirected to your web browser.', + WELCOME_NAMESPACE, + )()} + +
); @@ -85,13 +130,16 @@ export default class Welcome extends React.Component<{}, IState> { } /** - * Set pod url and pass it to the main process + * Handle seamless login */ - public setPodUrl(): void { - const { url } = this.state; + public login(): void { + this.setState({ isLoading: true }); + const { url, isPodConfigured, isSeamlessLoginEnabled } = this.state; ipcRenderer.send(apiName.symphonyApi, { - cmd: apiCmds.setPodUrl, + cmd: apiCmds.seamlessLogin, newPodUrl: url, + isPodConfigured, + isSeamlessLoginEnabled, }); } @@ -120,6 +168,45 @@ export default class Welcome extends React.Component<{}, IState> { }); } + /** + * Renders login button content + */ + private renderLoginButton() { + const { isLoading, isPodConfigured, urlValid } = this.state; + if (!isLoading) { + return ( + + ); + } + return ( +
+
+ + + +
+
+ ); + } + /** * Update state * @param _event @@ -128,4 +215,75 @@ export default class Welcome extends React.Component<{}, IState> { private updateState(_event, data): void { this.setState(data as IState); } + + /** + * Returns welcome screen symphony image + */ + private getWelcomeImage() { + return ( + + + + + + + + + + + + ); + } } diff --git a/src/renderer/preload-main.ts b/src/renderer/preload-main.ts index 5ff493d91..07f469b04 100644 --- a/src/renderer/preload-main.ts +++ b/src/renderer/preload-main.ts @@ -9,6 +9,7 @@ import DownloadManager from './components/download-manager'; import MessageBanner from './components/message-banner'; import NetworkError from './components/network-error'; import SnackBar from './components/snack-bar'; +import Welcome from './components/welcome'; import { SSFApi } from './ssf-api'; interface ISSFWindow extends Window { @@ -182,3 +183,16 @@ ipcRenderer.on('exit-html-fullscreen', async () => { await document.exitFullscreen(); } }); + +ipcRenderer.on('page-load-welcome', (_event, { locale, resources }) => { + i18n.setResource(locale, resources); + document.title = i18n.t('WelcomeText', 'Welcome')(); + const styles = document.createElement('link'); + styles.rel = 'stylesheet'; + styles.type = 'text/css'; + styles.href = `./styles/welcome.css`; + document.getElementsByTagName('head')[0].appendChild(styles); + const component = Welcome; + const element = React.createElement(component); + ReactDOM.render(element, document.getElementById('Root')); +}); diff --git a/src/renderer/react-window.html b/src/renderer/react-window.html index 426764a6d..3ff7ae316 100644 --- a/src/renderer/react-window.html +++ b/src/renderer/react-window.html @@ -15,12 +15,14 @@ --> - - - - - -
- - + + + + + + +
+ + + \ No newline at end of file diff --git a/src/renderer/styles/welcome.less b/src/renderer/styles/welcome.less index 0ded7ba2b..56f965528 100644 --- a/src/renderer/styles/welcome.less +++ b/src/renderer/styles/welcome.less @@ -9,12 +9,19 @@ } body { - background-color: white; + background-color: @graphite-90; margin: 0; height: 100%; width: 100%; } +.components-window-root { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +} + .Welcome:lang(ja-JP) { font-family: @font-family-ja; @@ -34,18 +41,64 @@ body { } .Welcome { + width: 360px; + height: 494px; display: flex; flex-direction: column; - padding: 10px; font-family: @font-family; + background-color: @graphite-80; + box-shadow: 0 24px 48px rgba(9, 10, 11, 0.64), + 0 4px 8px rgba(15, 27, 36, 0.16); + border-radius: 8px; + + &-content { + width: 300px; + padding: 48px 32px 24px; + } + + &-text-container { + font-size: 12px; + font-weight: 700; + line-height: 20px; + color: @graphite-05; + } &-image-container { text-align: center; - margin: 50px 0 10px; } - &-image-container img { - width: 200px; + &-about-symphony-text { + span { + color: @white; + font-size: 14px; + line-height: 20px; + } + } + + &-login-text { + margin-top: 32px; + + span { + color: @white; + font-size: 18px; + line-height: 20px; + font-weight: 600; + } + } + + &-text-bold { + font-weight: 700; + } + + &-input-container { + margin-top: 24px; + + span { + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: @graphite-20; + } } &-header-content { @@ -65,17 +118,26 @@ body { width: 100%; } - &-main-container-input-selection { - width: 100%; - } - &-main-container-podurl-box { float: left; width: 100%; - border-radius: 4px; - border: 1px solid #000; - padding: 5px; max-width: -webkit-fill-available; + height: 26px; + background-color: @graphite-90; + border: 2px solid @graphite-40; + padding: 6px 12px; + border-radius: 4px; + color: white; + font-size: 14px; + margin-top: 4px; + } + + &-input-message { + font-weight: 400; + font-size: 12px; + line-height: 16px; + margin-top: 4px; + color: @graphite-20; } &-main-container-sso-box { @@ -140,6 +202,8 @@ body { } &-continue-button { + width: 300px; + height: 40px; box-shadow: none; border: none; border-radius: 20px; @@ -149,40 +213,114 @@ body { display: inline-block; text-decoration: none; line-height: 12px; - background-color: #3da2fd; + background-color: @electricity-ui-50; color: #ffffff; cursor: pointer; text-transform: uppercase; - float: right; - margin: 24px 0px 4px 0; + margin: 24px 0 4px 0; &:focus { box-shadow: 0 0 10px rgba(61, 162, 253, 1); outline: none; } + + &:disabled { + background-color: @graphite-20; + } } - &-continue-button-disabled { - box-shadow: none; - border: none; - border-radius: 20px; - font-size: 0.8rem; - text-align: center; - padding: 10px 32px; - display: inline-block; - text-decoration: none; - line-height: 12px; - color: #ffffff; - text-transform: uppercase; - float: right; - margin: 24px 0px 4px 0; - cursor: not-allowed; - background-color: #cccccc; - pointer-events: none; + &-redirect-info-text-container { + margin-top: 32px; - &:focus { - box-shadow: 0 0 10px rgba(61, 162, 253, 1); - outline: none; + span { + font-weight: 400; + font-size: 12px; + line-height: 16px; + text-align: center; + color: @graphite-30; } } } + +@keyframes fade-in-logo { + 0% { + opacity: 0; + transform: translateY(-20px); + } + 100% { + opacity: 1; + } +} +@keyframes fade-in-spinner { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} +@keyframes progress-circular-rotate { + 100% { + transform: rotate(360deg); + } +} +@keyframes progress-circular-dash { + 0% { + stroke-dasharray: 1px, 200px; + stroke-dashoffset: 0px; + } + + 50% { + stroke-dasharray: 100px, 200px; + stroke-dashoffset: -15px; + } + + 100% { + stroke-dasharray: 100px, 200px; + stroke-dashoffset: -125px; + } +} +.splash-screen--container { + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + background-color: #141618; + position: absolute; + top: 0; + left: 0; + inset: 0; + transition: opacity 1s ease-out, height 0s linear 1s; + z-index: 9; +} +.splash-screen--close { + opacity: 0; + height: 0; +} +.splash-screen--logo-wrapper { + width: 33%; + max-width: 261px; + animation: fade-in-logo 1s ease-in; +} +.splash-screen--spinner-container { + opacity: 0; + animation: fade-in-spinner 1s ease-in 0s forwards; + margin-top: 2rem; + text-align: center; +} +.splash-screen--spinner { + display: inline-block; + line-height: 1; + color: #0277d6; + width: 40px; + height: 40px; + animation: progress-circular-rotate 1.4s linear infinite; +} +.splash-screen--spinner-circle { + animation: progress-circular-dash 1.4s ease-in-out infinite; + stroke-dasharray: 80px, 200px; + stroke-dashoffset: 0px; + stroke: currentColor; +}