import { batch, Provider } from 'react-redux'; import { Router } from 'react-router'; import { bindActionCreators } from 'redux'; import { StyleSheetManager } from 'styled-components'; import { hasExpired } from '../shared/account-expiry'; import { ILinuxSplitTunnelingApplication, IWindowsApplication } from '../shared/application-types'; import { AccountToken, BridgeSettings, BridgeState, DeviceEvent, DeviceState, IAccountData, IAppVersionInfo, ICustomList, IDevice, IDeviceRemoval, IDnsOptions, ILocation, IRelayListWithEndpointData, ISettings, liftConstraint, 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 { IChangelog, ICurrentAppVersionInfo, IHistoryObject } from '../shared/ipc-types'; import log, { ConsoleOutput } from '../shared/logging'; import { LogLevel } from '../shared/logging-types'; import { Scheduler } from '../shared/scheduler'; import AppRouter from './components/AppRouter'; import { Changelog } from './components/Changelog'; 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 History, { ITransitionSpecification, transitions } from './lib/history'; import { loadTranslations } from './lib/load-translations'; import IpcOutput from './lib/logging'; import { RoutePath } from './lib/routes'; import accountActions from './redux/account/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' }, ]; export default class AppRenderer { private history: History; private reduxStore = configureStore(); private reduxActions = { account: bindActionCreators(accountActions, 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 lastDisconnectedLocation?: Partial; private relayList?: IRelayListWithEndpointData; private tunnelState!: TunnelState; private settings!: ISettings; private deviceState?: DeviceState; private loginState: LoginState = 'none'; private previousLoginState: LoginState = 'none'; private loginScheduler = new Scheduler(); private connectedToDaemon = false; private getLocationPromise?: Promise; 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?: AccountToken) => { this.setAccountHistory(newAccountHistory); }); IpcRendererEventChannel.tunnel.listen((newState: TunnelState) => { this.setTunnelState(newState); this.updateBlockedState(newState, this.settings.blockWhenDisconnected); }); IpcRendererEventChannel.settings.listen((newSettings: ISettings) => { this.setSettings(newSettings); this.updateBlockedState(this.tunnelState, newSettings.blockWhenDisconnected); }); IpcRendererEventChannel.relays.listen((relayListPair: IRelayListWithEndpointData) => { this.setRelayListPair(relayListPair); }); IpcRendererEventChannel.currentVersion.listen((currentVersion: ICurrentAppVersionInfo) => { this.setCurrentVersion(currentVersion); }); IpcRendererEventChannel.upgradeVersion.listen((upgradeVersion: IAppVersionInfo) => { this.setUpgradeVersion(upgradeVersion); }); IpcRendererEventChannel.guiSettings.listen((guiSettings: IGuiSettingsState) => { this.setGuiSettings(guiSettings); }); IpcRendererEventChannel.autoStart.listen((autoStart: boolean) => { this.storeAutoStart(autoStart); }); IpcRendererEventChannel.windowsSplitTunneling.listen((applications: IWindowsApplication[]) => { 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)); // 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, initialState.navigationHistory !== undefined, ); } // 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, initialState.settings.blockWhenDisconnected); this.setRelayListPair(initialState.relayList); this.setCurrentVersion(initialState.currentVersion); this.setUpgradeVersion(initialState.upgradeVersion); this.setGuiSettings(initialState.guiSettings); this.storeAutoStart(initialState.autoStart); this.setChangelog(initialState.changelog, initialState.forceShowChanges); 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.windowsSplitTunnelingApplications) { this.reduxActions.settings.setSplitTunnelingApplications( initialState.windowsSplitTunnelingApplications, ); } void 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 navigationBase = this.getNavigationBase(); 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.windowsSplitTunneling.setState(state); public addSplitTunnelingApplication = (application: string | IWindowsApplication) => IpcRendererEventChannel.windowsSplitTunneling.addApplication(application); public forgetManuallyAddedSplitTunnelingApplication = (application: IWindowsApplication) => IpcRendererEventChannel.windowsSplitTunneling.forgetManuallyAddedApplication(application); public setObfuscationSettings = (obfuscationSettings: ObfuscationSettings) => IpcRendererEventChannel.settings.setObfuscationSettings(obfuscationSettings); 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: string) => IpcRendererEventChannel.app.openUrl(url); public showOpenDialog = (options: Electron.OpenDialogOptions) => IpcRendererEventChannel.app.showOpenDialog(options); public createCustomList = (name: string) => IpcRendererEventChannel.customLists.createCustomList(name); public deleteCustomList = (id: string) => IpcRendererEventChannel.customLists.deleteCustomList(id); public updateCustomList = (customList: ICustomList) => IpcRendererEventChannel.customLists.updateCustomList(customList); public login = async (accountToken: AccountToken) => { const actions = this.reduxActions; actions.account.startLogin(accountToken); log.info('Logging in'); this.previousLoginState = this.loginState; this.loginState = 'logging in'; const response = await IpcRendererEventChannel.account.login(accountToken); if (response?.type === 'error') { if (response.error === 'too-many-devices') { try { await this.fetchDevices(accountToken); actions.account.loginTooManyDevices(); this.loginState = 'too many devices'; this.history.reset(RoutePath.tooManyDevices, { transition: transitions.push }); } catch (e) { 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 (transition = transitions.dismiss) => { try { this.history.reset(RoutePath.login, { transition }); await IpcRendererEventChannel.account.logout(); } catch (e) { const error = e as Error; log.info('Failed to logout: ', error.message); } }; public leaveRevokedDevice = async () => { await this.logout(transitions.pop); await this.disconnectTunnel(); }; public async createNewAccount() { log.info('Creating account'); const actions = this.reduxActions; actions.account.startCreateAccount(); this.loginState = 'creating account'; try { await IpcRendererEventChannel.account.create(); this.redirectToConnect(); } catch (e) { const error = e as Error; actions.account.createAccountFailed(error); } } public fetchDevices = async (accountToken: AccountToken): Promise> => { const devices = await IpcRendererEventChannel.account.listDevices(accountToken); this.reduxActions.account.updateDevices(devices); return devices; }; public openLinkWithAuth = async (link: string): 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(`${link}?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 setBlockWhenDisconnected = async (blockWhenDisconnected: boolean) => { const actions = this.reduxActions; await IpcRendererEventChannel.settings.setBlockWhenDisconnected(blockWhenDisconnected); actions.settings.updateBlockWhenDisconnected(blockWhenDisconnected); }; 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 getWindowsSplitTunnelingApplications(updateCache = false) { return IpcRendererEventChannel.windowsSplitTunneling.getApplications(updateCache); } public removeSplitTunnelingApplication(application: IWindowsApplication) { void IpcRendererEventChannel.windowsSplitTunneling.removeApplication(application); } public async showLaunchDaemonSettings() { await IpcRendererEventChannel.app.showLaunchDaemonSettings(); } 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 setNavigationHistory(history: IHistoryObject) { IpcRendererEventChannel.navigation.setHistory(history); if (window.env.e2e) { window.e2e.location = history.entries[history.index].pathname; } } private isLoggedIn(): boolean { return this.deviceState?.type === 'logged in'; } // 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 redirectToConnect() { // Redirect the user after some time to allow for the 'Logged in' screen to be visible this.loginScheduler.schedule(() => this.resetNavigation(), 1000); } 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: liftConstraint(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; if ('normal' in bridgeSettings) { actions.settings.updateBridgeSettings({ normal: { location: liftConstraint(bridgeSettings.normal.location), }, }); } else if ('custom' in bridgeSettings) { actions.settings.updateBridgeSettings({ custom: bridgeSettings.custom, }); } } private onDaemonConnected() { this.connectedToDaemon = true; this.reduxActions.userInterface.setConnectedToDaemon(true); this.reduxActions.userInterface.setDaemonAllowed(true); this.resetNavigation(); } private onDaemonDisconnected() { this.connectedToDaemon = false; this.reduxActions.userInterface.setConnectedToDaemon(false); this.resetNavigation(); } private resetNavigation() { if (this.history) { const pathname = this.history.location.pathname as RoutePath; const nextPath = this.getNavigationBase() as RoutePath; if (pathname !== nextPath) { const transition = this.getNavigationTransition(pathname, nextPath); this.history.reset(nextPath, { transition }); } } } private getNavigationTransition(prevPath: RoutePath, nextPath: RoutePath) { // First level contains the possible next locations and the second level contains the // possible current locations. const navigationTransitions: Partial< Record>> > = { [RoutePath.launch]: { [RoutePath.login]: transitions.pop, [RoutePath.main]: transitions.pop, '*': transitions.dismiss, }, [RoutePath.login]: { [RoutePath.launch]: transitions.push, [RoutePath.main]: transitions.pop, [RoutePath.deviceRevoked]: transitions.pop, '*': transitions.dismiss, }, [RoutePath.main]: { [RoutePath.launch]: transitions.push, [RoutePath.login]: transitions.push, [RoutePath.tooManyDevices]: transitions.push, '*': transitions.dismiss, }, [RoutePath.expired]: { [RoutePath.launch]: transitions.push, [RoutePath.login]: transitions.push, [RoutePath.tooManyDevices]: transitions.push, '*': transitions.dismiss, }, [RoutePath.timeAdded]: { [RoutePath.expired]: transitions.push, [RoutePath.redeemVoucher]: transitions.push, '*': transitions.dismiss, }, [RoutePath.deviceRevoked]: { '*': transitions.pop, }, }; return navigationTransitions[nextPath]?.[prevPath] ?? navigationTransitions[nextPath]?.['*']; } private getNavigationBase(): RoutePath { if (this.connectedToDaemon && this.deviceState !== undefined) { const loginState = this.reduxStore.getState().account.status; const deviceRevoked = loginState.type === 'none' && loginState.deviceRevoked; if (deviceRevoked) { return RoutePath.deviceRevoked; } else if (!this.isLoggedIn()) { return RoutePath.login; } else if ( loginState.type === 'ok' && (loginState.expiredState === 'expired' || loginState.method === 'new_account') ) { return RoutePath.expired; } else if (loginState.type === 'ok' && loginState.expiredState === 'time_added') { return RoutePath.timeAdded; } else { return RoutePath.main; } } else { return RoutePath.launch; } } private setAccountHistory(accountHistory?: AccountToken) { this.reduxActions.account.updateAccountHistory(accountHistory); } private setTunnelState(tunnelState: TunnelState) { const actions = this.reduxActions; log.verbose(`Tunnel state: ${tunnelState.state}`); this.tunnelState = tunnelState; batch(() => { switch (tunnelState.state) { case 'connecting': actions.connection.connecting(tunnelState.details); break; case 'connected': actions.connection.connected(tunnelState.details); break; case 'disconnecting': actions.connection.disconnecting(tunnelState.details); break; case 'disconnected': actions.connection.disconnected(); break; case 'error': actions.connection.blocked(tunnelState.details); break; } // Update the location when entering a new tunnel state since it's likely changed. void 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.updateBlockWhenDisconnected(newSettings.blockWhenDisconnected); reduxSettings.updateShowBetaReleases(newSettings.showBetaReleases); reduxSettings.updateOpenVpnMssfix(newSettings.tunnelOptions.openvpn.mssfix); reduxSettings.updateWireguardMtu(newSettings.tunnelOptions.wireguard.mtu); reduxSettings.updateWireguardQuantumResistant( newSettings.tunnelOptions.wireguard.quantumResistant, ); reduxSettings.updateBridgeState(newSettings.bridgeState); reduxSettings.updateDnsOptions(newSettings.tunnelOptions.dns); reduxSettings.updateSplitTunnelingState(newSettings.splitTunnel.enableExclusions); reduxSettings.updateObfuscationSettings(newSettings.obfuscationSettings); reduxSettings.updateCustomLists(newSettings.customLists); this.setReduxRelaySettings(newSettings.relaySettings); this.setBridgeSettings(newSettings.bridgeSettings); } private setIsPerformingPostUpgrade(isPerformingPostUpgrade: boolean) { this.reduxActions.userInterface.setIsPerformingPostUpgrade(isPerformingPostUpgrade); } private updateBlockedState(tunnelState: TunnelState, blockWhenDisconnected: boolean) { const actions = this.reduxActions.connection; switch (tunnelState.state) { case 'connecting': actions.updateBlockState(true); break; case 'connected': actions.updateBlockState(false); break; case 'disconnected': actions.updateBlockState(blockWhenDisconnected); break; case 'disconnecting': actions.updateBlockState(true); break; case 'error': actions.updateBlockState(!tunnelState.details.blockingError); break; } } private handleDeviceEvent(deviceEvent: DeviceEvent, preventRedirectToConnect?: boolean) { const reduxAccount = this.reduxActions.account; this.deviceState = deviceEvent.deviceState; switch (deviceEvent.type) { case 'logged in': { const accountToken = deviceEvent.deviceState.accountAndDevice.accountToken; const device = deviceEvent.deviceState.accountAndDevice.device; switch (this.loginState) { case 'none': reduxAccount.loggedIn(accountToken, device); this.resetNavigation(); break; case 'logging in': reduxAccount.loggedIn(accountToken, device); if (this.previousLoginState === 'too many devices') { this.resetNavigation(); } else if (!preventRedirectToConnect) { this.redirectToConnect(); } break; case 'creating account': reduxAccount.accountCreated(accountToken, device, new Date().toISOString()); break; } break; } case 'logged out': this.loginScheduler.cancel(); reduxAccount.loggedOut(); this.resetNavigation(); break; case 'revoked': { this.loginScheduler.cancel(); reduxAccount.deviceRevoked(); this.resetNavigation(); break; } } this.previousLoginState = this.loginState; 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) { if (window.env.e2e && expiry) { log.verbose('Expiry of account:', expiry); } const state = this.reduxStore.getState(); const previousExpiry = state.account.expiry; this.reduxActions.account.updateAccountExpiry(expiry); const expired = expiry !== undefined && hasExpired(expiry); if ( this.history && state.account.status.type === 'ok' && expiry !== undefined && expiry !== previousExpiry && ((state.account.status.expiredState === undefined && expired) || (state.account.status.expiredState === 'expired' && !expired)) && // If the login navigation is already scheduled no navigation is needed !this.loginScheduler.isRunning ) { const prevPath = this.history.location.pathname as RoutePath; const nextPath = expired ? RoutePath.expired : RoutePath.timeAdded; const transition = this.getNavigationTransition(prevPath, nextPath); this.history.replaceRoot(nextPath, { transition }); } } private storeAutoStart(autoStart: boolean) { this.reduxActions.settings.updateAutoStart(autoStart); } private setChangelog(changelog: IChangelog, forceShowChanges: boolean) { this.reduxActions.userInterface.setChangelog(changelog); this.reduxActions.userInterface.setForceShowChanges(forceShowChanges); } private async updateLocation() { switch (this.tunnelState.state) { case 'disconnected': { if (this.lastDisconnectedLocation) { this.setLocation(this.lastDisconnectedLocation); } const location = await this.fetchLocation(); if (location) { this.setLocation(location); this.lastDisconnectedLocation = location; } break; } case 'disconnecting': if (this.lastDisconnectedLocation) { this.setLocation(this.lastDisconnectedLocation); } 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': this.setLocation(this.tunnelState.details?.location ?? this.getLocationFromConstraints()); break; case 'connected': { if (this.tunnelState.details?.location) { this.setLocation(this.tunnelState.details.location); } const location = await this.fetchLocation(); if (location) { this.setLocation(location); } break; } } } private async fetchLocation(): Promise { try { // Fetch the new user location const getLocationPromise = IpcRendererEventChannel.location.get(); this.getLocationPromise = getLocationPromise; const location = await getLocationPromise; // If the location is currently unavailable, do nothing! This only ever happens when a // custom relay is set or we are in a blocked state. if (location && getLocationPromise === this.getLocationPromise) { return location; } } catch (e) { const error = e as Error; log.error(`Failed to update the location: ${error.message}`); } } 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; } }