import { MotionConfig } from 'motion/react'; import { StrictMode } from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router'; import { bindActionCreators } from 'redux'; import { StyleSheetManager } from 'styled-components'; import { closeToExpiry, hasExpired } from '../shared/account-expiry'; import { ILinuxSplitTunnelingApplication, ISplitTunnelingApplication, } from '../shared/application-types'; import { Url } from '../shared/constants'; import { AccessMethodSetting, AccountNumber, BridgeSettings, BridgeState, CustomProxy, DeviceEvent, IAccountData, IAppVersionInfo, ICustomList, IDevice, IDeviceRemoval, IDnsOptions, ILocation, IRelayListWithEndpointData, ISettings, liftConstraint, NewAccessMethodSetting, NewCustomList, ObfuscationSettings, RelaySettings, TunnelState, } from '../shared/daemon-rpc-types'; import { messages, relayLocations } from '../shared/gettext'; import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state'; import { DaemonStatus, IChangelog, ICurrentAppVersionInfo, IHistoryObject, } from '../shared/ipc-types'; import log, { ConsoleOutput } from '../shared/logging'; import { LogLevel } from '../shared/logging-types'; import { RoutePath } from '../shared/routes'; import { Scheduler } from '../shared/scheduler'; import AppRouter from './components/AppRouter'; import ErrorBoundary from './components/ErrorBoundary'; import KeyboardNavigation from './components/KeyboardNavigation'; import Lang from './components/Lang'; import MacOsScrollbarDetection from './components/MacOsScrollbarDetection'; import { ModalContainer } from './components/Modal'; import { AppContext } from './context'; import { Theme } from './lib/components'; import { getNavigationBase } from './lib/functions/navigation-base'; import History from './lib/history'; import { loadTranslations } from './lib/load-translations'; import IpcOutput from './lib/logging'; import accountActions from './redux/account/actions'; import { appUpgradeActions } from './redux/app-upgrade/actions'; import connectionActions from './redux/connection/actions'; import settingsActions from './redux/settings/actions'; import configureStore from './redux/store'; import userInterfaceActions from './redux/userinterface/actions'; import versionActions from './redux/version/actions'; const IpcRendererEventChannel = window.ipc; interface IPreferredLocaleDescriptor { name: string; code: string; } type LoginState = 'none' | 'logging in' | 'creating account' | 'too many devices'; const SUPPORTED_LOCALE_LIST = [ { name: 'Dansk', code: 'da' }, { name: 'Deutsch', code: 'de' }, { name: 'English', code: 'en' }, { name: 'Español', code: 'es' }, { name: 'Suomi', code: 'fi' }, { name: 'Français', code: 'fr' }, { name: 'Italiano', code: 'it' }, { name: '日本語', code: 'ja' }, { name: '한국어', code: 'ko' }, { name: 'မြန်မာဘာသာ', code: 'my' }, { name: 'Nederlands', code: 'nl' }, { name: 'Norsk', code: 'nb' }, { name: 'Polski', code: 'pl' }, { name: 'Português', code: 'pt' }, { name: 'Русский', code: 'ru' }, { name: 'Svenska', code: 'sv' }, { name: 'ภาษาไทย', code: 'th' }, { name: 'Türkçe', code: 'tr' }, { name: '简体中文', code: 'zh-CN' }, { name: '繁體中文', code: 'zh-TW' }, ]; if (window.env.development) { SUPPORTED_LOCALE_LIST.push({ name: 'Rövarspråket', code: 'sv-rö' }); } export default class AppRenderer { private history: History; private reduxStore = configureStore(); private reduxActions = { account: bindActionCreators(accountActions, this.reduxStore.dispatch), appUpgrade: bindActionCreators(appUpgradeActions, this.reduxStore.dispatch), connection: bindActionCreators(connectionActions, this.reduxStore.dispatch), settings: bindActionCreators(settingsActions, this.reduxStore.dispatch), version: bindActionCreators(versionActions, this.reduxStore.dispatch), userInterface: bindActionCreators(userInterfaceActions, this.reduxStore.dispatch), }; private location?: Partial; private relayList?: IRelayListWithEndpointData; private tunnelState!: TunnelState; private settings!: ISettings; private loginState: LoginState = 'none'; private connectedToDaemon = false; private loginScheduler = new Scheduler(); private expiryScheduler = new Scheduler(); constructor() { log.addOutput(new ConsoleOutput(LogLevel.debug)); log.addOutput(new IpcOutput(LogLevel.debug)); IpcRendererEventChannel.window.listenShape((windowShapeParams) => { if (typeof windowShapeParams.arrowPosition === 'number') { this.reduxActions.userInterface.updateWindowArrowPosition(windowShapeParams.arrowPosition); } }); IpcRendererEventChannel.daemon.listenConnected(() => { void this.onDaemonConnected(); }); IpcRendererEventChannel.daemon.listenDisconnected(() => { this.onDaemonDisconnected(); }); IpcRendererEventChannel.daemon.listenIsPerformingPostUpgrade((isPerformingPostUpgrade) => { this.setIsPerformingPostUpgrade(isPerformingPostUpgrade); }); IpcRendererEventChannel.daemon.listenDaemonAllowed((daemonAllowed) => { this.reduxActions.userInterface.setDaemonAllowed(daemonAllowed); }); IpcRendererEventChannel.account.listen((newAccountData?: IAccountData) => { this.setAccountExpiry(newAccountData?.expiry); }); IpcRendererEventChannel.account.listenDevice((deviceEvent) => { this.handleDeviceEvent(deviceEvent); }); IpcRendererEventChannel.account.listenDevices((devices) => { this.reduxActions.account.updateDevices(devices); }); IpcRendererEventChannel.accountHistory.listen((newAccountHistory?: AccountNumber) => { this.setAccountHistory(newAccountHistory); }); IpcRendererEventChannel.tunnel.listen((newState: TunnelState) => { this.setTunnelState(newState); this.updateBlockedState(newState); }); IpcRendererEventChannel.settings.listen((newSettings: ISettings) => { this.setSettings(newSettings); this.updateBlockedState(this.tunnelState); }); IpcRendererEventChannel.settings.listenApiAccessMethodSettingChange((setting) => { this.setCurrentApiAccessMethod(setting); }); IpcRendererEventChannel.relays.listen((relayListPair: IRelayListWithEndpointData) => { this.setRelayListPair(relayListPair); }); IpcRendererEventChannel.daemon.listenTryStartEvent((status: DaemonStatus) => { this.reduxActions.userInterface.setDaemonStatus(status); }); IpcRendererEventChannel.app.listenUpgradeEvent((appUpgradeEvent) => { this.reduxActions.appUpgrade.setAppUpgradeEvent(appUpgradeEvent); if (appUpgradeEvent.type === 'APP_UPGRADE_STATUS_DOWNLOAD_PROGRESS') { this.reduxActions.appUpgrade.setLastProgress(appUpgradeEvent.progress); } // Ensure progress is updated to 100%, since the daemon doesn't send the last event if ( appUpgradeEvent.type === 'APP_UPGRADE_STATUS_VERIFYING_INSTALLER' || appUpgradeEvent.type === 'APP_UPGRADE_STATUS_VERIFIED_INSTALLER' ) { this.reduxActions.appUpgrade.setLastProgress(100); } // Check if the installer should be started automatically this.appUpgradeMaybeStartInstaller(); }); IpcRendererEventChannel.app.listenUpgradeError((appUpgradeError) => { this.reduxActions.appUpgrade.setAppUpgradeError(appUpgradeError); }); IpcRendererEventChannel.currentVersion.listen((currentVersion: ICurrentAppVersionInfo) => { this.setCurrentVersion(currentVersion); }); IpcRendererEventChannel.upgradeVersion.listen((upgradeVersion: IAppVersionInfo) => { const reduxStore = this.reduxStore.getState(); const currentSuggestedUpgradeVersion = reduxStore.version.suggestedUpgrade?.version; const newSuggestedUpgradeVersion = upgradeVersion.suggestedUpgrade?.version; if ( currentSuggestedUpgradeVersion && currentSuggestedUpgradeVersion !== newSuggestedUpgradeVersion ) { this.reduxActions.appUpgrade.resetAppUpgrade(); } this.setUpgradeVersion(upgradeVersion); // Check if the installer should be started automatically this.appUpgradeMaybeStartInstaller(); }); IpcRendererEventChannel.guiSettings.listen((guiSettings: IGuiSettingsState) => { this.setGuiSettings(guiSettings); }); IpcRendererEventChannel.autoStart.listen((autoStart: boolean) => { this.storeAutoStart(autoStart); }); IpcRendererEventChannel.splitTunneling.listen((applications: ISplitTunnelingApplication[]) => { this.reduxActions.settings.setSplitTunnelingApplications(applications); }); IpcRendererEventChannel.window.listenFocus((focus: boolean) => { this.reduxActions.userInterface.setWindowFocused(focus); }); IpcRendererEventChannel.window.listenMacOsScrollbarVisibility((visibility) => { this.reduxActions.userInterface.setMacOsScrollbarVisibility(visibility); }); IpcRendererEventChannel.navigation.listenReset(() => this.history.pop(true)); IpcRendererEventChannel.app.listenOpenRoute((route: RoutePath) => { this.history.push({ routePath: route, }); }); // Request the initial state from the main process const initialState = IpcRendererEventChannel.state.get(); this.setLocale(initialState.translations.locale); loadTranslations( messages, initialState.translations.locale, initialState.translations.messages, ); loadTranslations( relayLocations, initialState.translations.locale, initialState.translations.relayLocations, ); this.setSettings(initialState.settings); this.setIsPerformingPostUpgrade(initialState.isPerformingPostUpgrade); if (initialState.daemonAllowed !== undefined) { this.reduxActions.userInterface.setDaemonAllowed(initialState.daemonAllowed); } if (initialState.deviceState) { const deviceState = initialState.deviceState; this.handleDeviceEvent({ type: deviceState.type, deviceState } as DeviceEvent); } // Login state and account needs to be set before expiry. this.setAccountExpiry(initialState.accountData?.expiry); this.setAccountHistory(initialState.accountHistory); this.setTunnelState(initialState.tunnelState); this.updateBlockedState(initialState.tunnelState); this.setRelayListPair(initialState.relayList); this.setCurrentVersion(initialState.currentVersion); this.setUpgradeVersion(initialState.upgradeVersion); this.setGuiSettings(initialState.guiSettings); this.storeAutoStart(initialState.autoStart); this.setChangelog(initialState.changelog); this.setCurrentApiAccessMethod(initialState.currentApiAccessMethod); this.reduxActions.userInterface.setIsMacOs13OrNewer(initialState.isMacOs13OrNewer); if (initialState.macOsScrollbarVisibility !== undefined) { this.reduxActions.userInterface.setMacOsScrollbarVisibility( initialState.macOsScrollbarVisibility, ); } if (initialState.isConnected) { void this.onDaemonConnected(); } this.checkContentHeight(false); window.addEventListener('resize', () => { this.checkContentHeight(true); }); if (initialState.splitTunnelingApplications) { this.reduxActions.settings.setSplitTunnelingApplications( initialState.splitTunnelingApplications, ); } this.updateLocation(); if (initialState.navigationHistory) { // Set last action to POP to trigger automatic scrolling to saved coordinates. initialState.navigationHistory.lastAction = 'POP'; this.history = History.fromSavedHistory(initialState.navigationHistory); } else { const loginState = this.reduxStore.getState().account.status; const navigationBase = getNavigationBase(this.connectedToDaemon, loginState); this.history = new History(navigationBase); } if (window.env.e2e) { // Make the current location available to the tests if running e2e tests window.e2e = { location: this.history.location.pathname }; } } public renderView() { return ( {window.env.platform === 'darwin' && } ); } public submitVoucher = (code: string) => IpcRendererEventChannel.account.submitVoucher(code); public updateAccountData = () => IpcRendererEventChannel.account.updateData(); public removeDevice = (device: IDeviceRemoval) => IpcRendererEventChannel.account.removeDevice(device); public connectTunnel = () => IpcRendererEventChannel.tunnel.connect(); public disconnectTunnel = () => IpcRendererEventChannel.tunnel.disconnect(); public reconnectTunnel = () => IpcRendererEventChannel.tunnel.reconnect(); public setRelaySettings = (relaySettings: RelaySettings) => IpcRendererEventChannel.settings.setRelaySettings(relaySettings); public updateBridgeSettings = (bridgeSettings: BridgeSettings) => IpcRendererEventChannel.settings.updateBridgeSettings(bridgeSettings); public setDnsOptions = (dnsOptions: IDnsOptions) => IpcRendererEventChannel.settings.setDnsOptions(dnsOptions); public clearAccountHistory = () => IpcRendererEventChannel.accountHistory.clear(); public setAutoConnect = (value: boolean) => IpcRendererEventChannel.guiSettings.setAutoConnect(value); public setEnableSystemNotifications = (value: boolean) => IpcRendererEventChannel.guiSettings.setEnableSystemNotifications(value); public setStartMinimized = (value: boolean) => IpcRendererEventChannel.guiSettings.setStartMinimized(value); public setMonochromaticIcon = (value: boolean) => IpcRendererEventChannel.guiSettings.setMonochromaticIcon(value); public setUnpinnedWindow = (value: boolean) => IpcRendererEventChannel.guiSettings.setUnpinnedWindow(value); public getLinuxSplitTunnelingApplications = () => IpcRendererEventChannel.linuxSplitTunneling.getApplications(); public launchExcludedApplication = (application: ILinuxSplitTunnelingApplication | string) => IpcRendererEventChannel.linuxSplitTunneling.launchApplication(application); public setSplitTunnelingState = (state: boolean) => IpcRendererEventChannel.splitTunneling.setState(state); public addSplitTunnelingApplication = (application: string | ISplitTunnelingApplication) => IpcRendererEventChannel.splitTunneling.addApplication(application); public forgetManuallyAddedSplitTunnelingApplication = (application: ISplitTunnelingApplication) => IpcRendererEventChannel.splitTunneling.forgetManuallyAddedApplication(application); public needFullDiskPermissions = () => IpcRendererEventChannel.macOsSplitTunneling.needFullDiskPermissions(); public setObfuscationSettings = (obfuscationSettings: ObfuscationSettings) => IpcRendererEventChannel.settings.setObfuscationSettings(obfuscationSettings); public setEnableDaita = (value: boolean) => IpcRendererEventChannel.settings.setEnableDaita(value); public setDaitaDirectOnly = (value: boolean) => IpcRendererEventChannel.settings.setDaitaDirectOnly(value); public collectProblemReport = (toRedact: string | undefined) => IpcRendererEventChannel.problemReport.collectLogs(toRedact); public viewLog = (path: string) => IpcRendererEventChannel.problemReport.viewLog(path); public quit = () => IpcRendererEventChannel.app.quit(); public openUrl = (url: Url) => IpcRendererEventChannel.app.openUrl(url); public getPathBaseName = (path: string) => IpcRendererEventChannel.app.getPathBaseName(path); public showOpenDialog = (options: Electron.OpenDialogOptions) => IpcRendererEventChannel.app.showOpenDialog(options); public createCustomList = (newCustomList: NewCustomList) => IpcRendererEventChannel.customLists.createCustomList(newCustomList); public deleteCustomList = (id: string) => IpcRendererEventChannel.customLists.deleteCustomList(id); public updateCustomList = (customList: ICustomList) => IpcRendererEventChannel.customLists.updateCustomList(customList); public addApiAccessMethod = (method: NewAccessMethodSetting) => IpcRendererEventChannel.settings.addApiAccessMethod(method); public updateApiAccessMethod = (method: AccessMethodSetting) => IpcRendererEventChannel.settings.updateApiAccessMethod(method); public removeApiAccessMethod = (id: string) => IpcRendererEventChannel.settings.removeApiAccessMethod(id); public setApiAccessMethod = (id: string) => IpcRendererEventChannel.settings.setApiAccessMethod(id); public testApiAccessMethodById = (id: string) => IpcRendererEventChannel.settings.testApiAccessMethodById(id); public testCustomApiAccessMethod = (method: CustomProxy) => IpcRendererEventChannel.settings.testCustomApiAccessMethod(method); public importSettingsFile = (path: string) => IpcRendererEventChannel.settings.importFile(path); public importSettingsText = (text: string) => IpcRendererEventChannel.settings.importText(text); public clearAllRelayOverrides = () => IpcRendererEventChannel.settings.clearAllRelayOverrides(); public getMapData = () => IpcRendererEventChannel.map.getData(); public setAnimateMap = (displayMap: boolean): void => IpcRendererEventChannel.guiSettings.setAnimateMap(displayMap); public daemonPrepareRestart = (shutdown: boolean): void => { IpcRendererEventChannel.daemon.prepareRestart(shutdown); }; public getLinuxSplitTunnelingSupported = () => { return IpcRendererEventChannel.linuxSplitTunneling.isSplitTunnelingSupported(); }; public getAppUpgradeCacheDir = () => IpcRendererEventChannel.app.getUpgradeCacheDir(); public tryStartDaemon = () => { if (window.env.platform === 'win32') IpcRendererEventChannel.daemon.tryStart(); }; public appUpgrade = () => { const reduxState = this.reduxStore.getState(); const appUpgradeError = reduxState.appUpgrade.error; if (appUpgradeError) { this.reduxActions.appUpgrade.resetAppUpgradeError(); } this.reduxActions.appUpgrade.setAppUpgradeEvent({ type: 'APP_UPGRADE_STATUS_DOWNLOAD_INITIATED', }); IpcRendererEventChannel.app.upgrade(); }; public appUpgradeAbort = () => IpcRendererEventChannel.app.upgradeAbort(); public appUpgradeInstallerStart = () => { const reduxState = this.reduxStore.getState(); const verifiedInstallerPath = reduxState.version.suggestedUpgrade?.verifiedInstallerPath; const hasVerifiedInstallerPath = typeof verifiedInstallerPath === 'string' && verifiedInstallerPath.length > 0; // Ensure we have a the path to the verified installer and that we are not already trying // to start the installer. if (hasVerifiedInstallerPath) { this.reduxActions.appUpgrade.setAppUpgradeEvent({ type: 'APP_UPGRADE_STATUS_MANUAL_STARTING_INSTALLER', }); this.reduxActions.appUpgrade.resetAppUpgradeError(); IpcRendererEventChannel.app.upgradeInstallerStart(verifiedInstallerPath); } }; public login = async (accountNumber: AccountNumber) => { const actions = this.reduxActions; actions.account.startLogin(accountNumber); log.info('Logging in'); this.loginState = 'logging in'; const response = await IpcRendererEventChannel.account.login(accountNumber); if (response?.type === 'error') { if (response.error === 'too-many-devices') { try { await this.fetchDevices(accountNumber); actions.account.loginTooManyDevices(); this.loginState = 'too many devices'; } catch { log.error('Failed to fetch device list'); actions.account.loginFailed('list-devices'); } } else { actions.account.loginFailed(response.error); } } }; public cancelLogin = (): void => { const reduxAccount = this.reduxActions.account; reduxAccount.loggedOut(); this.loginState = 'none'; }; public logout = async () => { try { await IpcRendererEventChannel.account.logout(); } catch (e) { const error = e as Error; log.info('Failed to logout: ', error.message); } }; public leaveRevokedDevice = async () => { await this.logout(); await this.disconnectTunnel(); }; public createNewAccount = async () => { log.info('Creating account'); const actions = this.reduxActions; actions.account.startCreateAccount(); this.loginState = 'creating account'; try { await IpcRendererEventChannel.account.create(); } catch (e) { const error = e as Error; actions.account.createAccountFailed(error); } }; public fetchDevices = async (accountNumber: AccountNumber): Promise> => { const devices = await IpcRendererEventChannel.account.listDevices(accountNumber); this.reduxActions.account.updateDevices(devices); return devices; }; public openUrlWithAuth = async (url: Url): Promise => { let token = ''; try { token = await IpcRendererEventChannel.account.getWwwAuthToken(); } catch (e) { const error = e as Error; log.error(`Failed to get the WWW auth token: ${error.message}`); } void this.openUrl(`${url}?token=${token}`); }; public setAllowLan = async (allowLan: boolean) => { const actions = this.reduxActions; await IpcRendererEventChannel.settings.setAllowLan(allowLan); actions.settings.updateAllowLan(allowLan); }; public setShowBetaReleases = async (showBetaReleases: boolean) => { const actions = this.reduxActions; await IpcRendererEventChannel.settings.setShowBetaReleases(showBetaReleases); actions.settings.updateShowBetaReleases(showBetaReleases); }; public setEnableIpv6 = async (enableIpv6: boolean) => { const actions = this.reduxActions; await IpcRendererEventChannel.settings.setEnableIpv6(enableIpv6); actions.settings.updateEnableIpv6(enableIpv6); }; public setBridgeState = async (bridgeState: BridgeState) => { const actions = this.reduxActions; await IpcRendererEventChannel.settings.setBridgeState(bridgeState); actions.settings.updateBridgeState(bridgeState); }; public setLockdownMode = async (lockdownMode: boolean) => { const actions = this.reduxActions; await IpcRendererEventChannel.settings.setLockdownMode(lockdownMode); actions.settings.updateLockdownMode(lockdownMode); }; public setOpenVpnMssfix = async (mssfix?: number) => { const actions = this.reduxActions; actions.settings.updateOpenVpnMssfix(mssfix); await IpcRendererEventChannel.settings.setOpenVpnMssfix(mssfix); }; public setWireguardMtu = async (mtu?: number) => { const actions = this.reduxActions; actions.settings.updateWireguardMtu(mtu); await IpcRendererEventChannel.settings.setWireguardMtu(mtu); }; public setWireguardQuantumResistant = async (quantumResistant?: boolean) => { const actions = this.reduxActions; actions.settings.updateWireguardQuantumResistant(quantumResistant); await IpcRendererEventChannel.settings.setWireguardQuantumResistant(quantumResistant); }; public setAutoStart = (autoStart: boolean): Promise => { this.storeAutoStart(autoStart); return IpcRendererEventChannel.autoStart.set(autoStart); }; public getSplitTunnelingApplications(updateCache = false) { return IpcRendererEventChannel.splitTunneling.getApplications(updateCache); } public removeSplitTunnelingApplication(application: ISplitTunnelingApplication) { void IpcRendererEventChannel.splitTunneling.removeApplication(application); } public async showLaunchDaemonSettings() { await IpcRendererEventChannel.app.showLaunchDaemonSettings(); } public showFullDiskAccessSettings = async () => { await IpcRendererEventChannel.app.showFullDiskAccessSettings(); }; public async sendProblemReport( email: string, message: string, savedReportId: string, ): Promise { await IpcRendererEventChannel.problemReport.sendReport({ email, message, savedReportId }); } public getPreferredLocaleList(): IPreferredLocaleDescriptor[] { return [ { // TRANSLATORS: The option that represents the active operating system language in the // TRANSLATORS: user interface language selection list. name: messages.gettext('System default'), code: SYSTEM_PREFERRED_LOCALE_KEY, }, ...SUPPORTED_LOCALE_LIST.sort((a, b) => a.name.localeCompare(b.name)), ]; } public setPreferredLocale = async (preferredLocale: string): Promise => { const translations = await IpcRendererEventChannel.guiSettings.setPreferredLocale(preferredLocale); // set current locale this.setLocale(translations.locale); // load translations for new locale loadTranslations(messages, translations.locale, translations.messages); loadTranslations(relayLocations, translations.locale, translations.relayLocations); }; public getPreferredLocaleDisplayName = (localeCode: string): string => { const preferredLocale = this.getPreferredLocaleList().find((item) => item.code === localeCode); return preferredLocale ? preferredLocale.name : ''; }; public setDisplayedChangelog = (): void => { IpcRendererEventChannel.currentVersion.displayedChangelog(); }; public setDismissedUpgrade = (): void => { IpcRendererEventChannel.upgradeVersion.dismissedUpgrade( this.reduxStore.getState().version.suggestedUpgrade?.version ?? '', ); }; public setNavigationHistory(history: IHistoryObject) { IpcRendererEventChannel.navigation.setHistory(history); } // If the installer has just been downloaded and verified we want to automatically // start the installer if the window is focused. private appUpgradeMaybeStartInstaller() { const reduxState = this.reduxStore.getState(); const appUpgradeEvent = reduxState.appUpgrade.event; const verifiedInstallerPath = reduxState.version.suggestedUpgrade?.verifiedInstallerPath; const windowFocused = reduxState.userInterface.windowFocused; const hasVerifiedInstallerPath = typeof verifiedInstallerPath === 'string' && verifiedInstallerPath.length > 0; if ( hasVerifiedInstallerPath && appUpgradeEvent?.type === 'APP_UPGRADE_STATUS_VERIFIED_INSTALLER' ) { // Only trigger the installer if the window is focused if (windowFocused) { this.reduxActions.appUpgrade.setAppUpgradeEvent({ type: 'APP_UPGRADE_STATUS_AUTOMATIC_STARTING_INSTALLER', }); IpcRendererEventChannel.app.upgradeInstallerStart(verifiedInstallerPath); } else { // Otherwise, flag this as requiring manual start this.reduxActions.appUpgrade.setAppUpgradeEvent({ type: 'APP_UPGRADE_STATUS_MANUAL_START_INSTALLER', }); } } } // Make sure that the content height is correct and log if it isn't. This is mostly for debugging // purposes since there's a bug in Electron that causes the app height to be another value than // the one we have set. // https://github.com/electron/electron/issues/28777 private checkContentHeight(resize: boolean): void { const expectedContentHeight = 568; const contentHeight = window.innerHeight; if (contentHeight !== expectedContentHeight) { log.verbose( resize ? 'Resize:' : 'Initial:', `Wrong content height: ${contentHeight}, expected ${expectedContentHeight}`, ); } } private setLocale(locale: string) { this.reduxActions.userInterface.updateLocale(locale); } private setReduxRelaySettings(relaySettings: RelaySettings) { const actions = this.reduxActions; if ('normal' in relaySettings) { const { location, openvpnConstraints, wireguardConstraints, tunnelProtocol, providers, ownership, } = relaySettings.normal; actions.settings.updateRelay({ normal: { location: liftConstraint(location), providers, ownership, openvpn: { port: liftConstraint(openvpnConstraints.port), protocol: liftConstraint(openvpnConstraints.protocol), }, wireguard: { port: liftConstraint(wireguardConstraints.port), ipVersion: liftConstraint(wireguardConstraints.ipVersion), useMultihop: wireguardConstraints.useMultihop, entryLocation: liftConstraint(wireguardConstraints.entryLocation), }, tunnelProtocol, }, }); } else if ('customTunnelEndpoint' in relaySettings) { const customTunnelEndpoint = relaySettings.customTunnelEndpoint; const config = customTunnelEndpoint.config; if ('openvpn' in config) { actions.settings.updateRelay({ customTunnelEndpoint: { host: customTunnelEndpoint.host, port: config.openvpn.endpoint.port, protocol: config.openvpn.endpoint.protocol, }, }); } else if ('wireguard' in config) { // TODO: handle wireguard } } } private setBridgeSettings(bridgeSettings: BridgeSettings) { const actions = this.reduxActions; actions.settings.updateBridgeSettings({ type: bridgeSettings.type, normal: { location: liftConstraint(bridgeSettings.normal.location), providers: bridgeSettings.normal.providers, ownership: bridgeSettings.normal.ownership, }, custom: bridgeSettings.custom, }); } private onDaemonConnected() { this.connectedToDaemon = true; this.reduxActions.userInterface.setConnectedToDaemon(true); this.reduxActions.userInterface.setDaemonAllowed(true); this.reduxActions.userInterface.setDaemonStatus('running'); } private onDaemonDisconnected() { this.connectedToDaemon = false; this.reduxActions.userInterface.setConnectedToDaemon(false); this.reduxActions.userInterface.setDaemonStatus('stopped'); } private setAccountHistory(accountHistory?: AccountNumber) { this.reduxActions.account.updateAccountHistory(accountHistory); } private setTunnelState(tunnelState: TunnelState) { const actions = this.reduxActions; log.verbose(`Tunnel state: ${tunnelState.state}`); this.tunnelState = tunnelState; switch (tunnelState.state) { case 'connecting': actions.connection.connecting(tunnelState.details, tunnelState.featureIndicators); break; case 'connected': actions.connection.connected(tunnelState.details, tunnelState.featureIndicators); break; case 'disconnecting': actions.connection.disconnecting(tunnelState.details); break; case 'disconnected': actions.connection.disconnected(tunnelState.lockedDown); break; case 'error': actions.connection.blocked(tunnelState.details); break; } // Update the location when entering a new tunnel state since it's likely changed. this.updateLocation(); } private setSettings(newSettings: ISettings) { this.settings = newSettings; const reduxSettings = this.reduxActions.settings; reduxSettings.updateAllowLan(newSettings.allowLan); reduxSettings.updateEnableIpv6(newSettings.tunnelOptions.generic.enableIpv6); reduxSettings.updateLockdownMode(newSettings.lockdownMode); reduxSettings.updateShowBetaReleases(newSettings.showBetaReleases); reduxSettings.updateOpenVpnMssfix(newSettings.tunnelOptions.openvpn.mssfix); reduxSettings.updateWireguardMtu(newSettings.tunnelOptions.wireguard.mtu); reduxSettings.updateWireguardQuantumResistant( newSettings.tunnelOptions.wireguard.quantumResistant, ); reduxSettings.updateWireguardDaita(newSettings.tunnelOptions.wireguard.daita); reduxSettings.updateBridgeState(newSettings.bridgeState); reduxSettings.updateDnsOptions(newSettings.tunnelOptions.dns); reduxSettings.updateSplitTunnelingState(newSettings.splitTunnel.enableExclusions); reduxSettings.updateObfuscationSettings(newSettings.obfuscationSettings); reduxSettings.updateCustomLists(newSettings.customLists); reduxSettings.updateApiAccessMethods(newSettings.apiAccessMethods); reduxSettings.updateRelayOverrides(newSettings.relayOverrides); this.setReduxRelaySettings(newSettings.relaySettings); this.setBridgeSettings(newSettings.bridgeSettings); } private setIsPerformingPostUpgrade(isPerformingPostUpgrade: boolean) { this.reduxActions.userInterface.setIsPerformingPostUpgrade(isPerformingPostUpgrade); } private updateBlockedState(tunnelState: TunnelState) { const actions = this.reduxActions.connection; switch (tunnelState.state) { case 'connecting': actions.updateBlockState(true); break; case 'connected': actions.updateBlockState(false); break; case 'disconnected': actions.updateBlockState(tunnelState.lockedDown); break; case 'disconnecting': actions.updateBlockState(true); break; case 'error': actions.updateBlockState(!tunnelState.details.blockingError); break; } } private handleDeviceEvent(deviceEvent: DeviceEvent) { const reduxAccount = this.reduxActions.account; switch (deviceEvent.type) { case 'logged in': { const accountNumber = deviceEvent.deviceState.accountAndDevice.accountNumber; const device = deviceEvent.deviceState.accountAndDevice.device; switch (this.loginState) { case 'none': reduxAccount.loggedIn(accountNumber, device); break; case 'logging in': reduxAccount.loggedIn(accountNumber, device); break; case 'creating account': reduxAccount.accountCreated(accountNumber, device, new Date().toISOString()); break; } break; } case 'logged out': this.loginScheduler.cancel(); reduxAccount.loggedOut(); break; case 'revoked': { this.loginScheduler.cancel(); reduxAccount.deviceRevoked(); break; } } this.loginState = 'none'; } private setLocation(location: Partial) { this.location = location; this.propagateLocationToRedux(); } private propagateLocationToRedux() { if (this.location) { this.reduxActions.connection.newLocation(this.location); } } private setRelayListPair(relayListPair?: IRelayListWithEndpointData) { this.relayList = relayListPair; this.propagateRelayListPairToRedux(); } private propagateRelayListPairToRedux() { if (this.relayList) { this.reduxActions.settings.updateRelayLocations(this.relayList.relayList.countries); this.reduxActions.settings.updateWireguardEndpointData(this.relayList.wireguardEndpointData); } } private setCurrentVersion(versionInfo: ICurrentAppVersionInfo) { this.reduxActions.version.updateVersion( versionInfo.gui, versionInfo.isConsistent, versionInfo.isBeta, ); } private setUpgradeVersion(upgradeVersion: IAppVersionInfo) { this.reduxActions.version.updateLatest(upgradeVersion); } private setGuiSettings(guiSettings: IGuiSettingsState) { this.reduxActions.settings.updateGuiSettings(guiSettings); } private setAccountExpiry(expiry?: string) { log.info(`setAccountExpiry(${expiry}) called at ${new Date().toISOString()}`); this.expiryScheduler.cancel(); if (expiry !== undefined) { const expired = hasExpired(expiry); // Set state to expired when expiry date passes. if (!expired && closeToExpiry(expiry)) { const delay = new Date(expiry).getTime() - Date.now() + 1; this.expiryScheduler.schedule(() => this.handleExpiry(expiry), delay); } this.handleExpiry(expiry); } else { this.handleExpiry(expiry); } } private handleExpiry(expiry?: string) { this.reduxActions.account.updateAccountExpiry(expiry); } private storeAutoStart(autoStart: boolean) { this.reduxActions.settings.updateAutoStart(autoStart); } private setChangelog(changelog: IChangelog) { this.reduxActions.userInterface.setChangelog(changelog); } private updateLocation() { switch (this.tunnelState.state) { case 'disconnected': if (this.tunnelState.location) { this.setLocation(this.tunnelState.location); } break; case 'disconnecting': if (this.tunnelState.location) { this.setLocation(this.tunnelState.location); } else { // If there's no previous location while disconnecting we remove the location. We keep the // coordinates to prevent the map from jumping around. const { longitude, latitude } = this.reduxStore.getState().connection; this.setLocation({ longitude, latitude }); } break; case 'connecting': case 'connected': { this.setLocation(this.tunnelState.details?.location ?? this.getLocationFromConstraints()); break; } } } private setCurrentApiAccessMethod(method?: AccessMethodSetting) { if (method) { this.reduxActions.settings.updateCurrentApiAccessMethod(method); } } private getLocationFromConstraints(): Partial { const state = this.reduxStore.getState(); const coordinates = { longitude: state.connection.longitude, latitude: state.connection.latitude, }; const relaySettings = this.settings.relaySettings; if ('normal' in relaySettings) { const location = relaySettings.normal.location; if (location !== 'any' && 'only' in location) { const constraint = location.only; const relayLocations = state.settings.relayLocations; if ('hostname' in constraint) { const country = relayLocations.find(({ code }) => constraint.country === code); const city = country?.cities.find(({ code }) => constraint.city === code); let entryHostname: string | undefined; const multihopConstraint = relaySettings.normal.wireguardConstraints.useMultihop; const entryLocationConstraint = relaySettings.normal.wireguardConstraints.entryLocation; if ( multihopConstraint && entryLocationConstraint !== 'any' && 'hostname' in entryLocationConstraint.only && entryLocationConstraint.only.hostname.length === 3 ) { entryHostname = entryLocationConstraint.only.hostname; } return { country: country?.name, city: city?.name, hostname: constraint.hostname, entryHostname, ...coordinates, }; } else if ('city' in constraint) { const country = relayLocations.find(({ code }) => constraint.country === code); const city = country?.cities.find(({ code }) => constraint.city === code); return { country: country?.name, city: city?.name, ...coordinates }; } else if ('country' in constraint) { const country = relayLocations.find(({ code }) => constraint.country === code); return { country: country?.name, ...coordinates }; } } } return coordinates; } }