summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src/renderer')
-rw-r--r--gui/src/renderer/app.tsx238
-rw-r--r--gui/src/renderer/components/Account.tsx99
-rw-r--r--gui/src/renderer/components/AccountStyles.tsx11
-rw-r--r--gui/src/renderer/components/AdvancedSettings.tsx67
-rw-r--r--gui/src/renderer/components/AdvancedSettingsStyles.tsx11
-rw-r--r--gui/src/renderer/components/AppRouter.tsx6
-rw-r--r--gui/src/renderer/components/DeviceRevokedView.tsx95
-rw-r--r--gui/src/renderer/components/ExpiredAccountAddTime.tsx6
-rw-r--r--gui/src/renderer/components/KeyboardNavigation.tsx16
-rw-r--r--gui/src/renderer/components/Login.tsx9
-rw-r--r--gui/src/renderer/components/MainView.tsx21
-rw-r--r--gui/src/renderer/components/NotificationArea.tsx8
-rw-r--r--gui/src/renderer/components/TooManyDevices.tsx317
-rw-r--r--gui/src/renderer/components/WireguardKeys.tsx345
-rw-r--r--gui/src/renderer/components/WireguardKeysStyles.tsx59
-rw-r--r--gui/src/renderer/components/WireguardSettings.tsx10
-rw-r--r--gui/src/renderer/containers/AccountPage.tsx10
-rw-r--r--gui/src/renderer/containers/AdvancedSettingsPage.tsx1
-rw-r--r--gui/src/renderer/containers/WireguardKeysPage.tsx27
-rw-r--r--gui/src/renderer/containers/WireguardSettingsPage.tsx3
-rw-r--r--gui/src/renderer/lib/routes.ts15
-rw-r--r--gui/src/renderer/redux/account/actions.ts98
-rw-r--r--gui/src/renderer/redux/account/reducers.ts56
-rw-r--r--gui/src/renderer/redux/settings/actions.ts99
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts163
25 files changed, 881 insertions, 909 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 28f73ac9e0..778bf4cdbd 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -11,7 +11,6 @@ import { AppContext } from './context';
import accountActions from './redux/account/actions';
import connectionActions from './redux/connection/actions';
import settingsActions from './redux/settings/actions';
-import { IWgKey } from './redux/settings/reducers';
import configureStore from './redux/store';
import userInterfaceActions from './redux/userinterface/actions';
import versionActions from './redux/version/actions';
@@ -32,16 +31,18 @@ import {
BridgeState,
IAccountData,
IAppVersionInfo,
+ IDevice,
+ IDeviceConfig,
+ IDeviceRemoval,
IDnsOptions,
ILocation,
ISettings,
- IWireguardPublicKey,
- KeygenEvent,
liftConstraint,
RelaySettings,
RelaySettingsUpdate,
TunnelState,
VoucherResponse,
+ IDeviceEvent,
} from '../shared/daemon-rpc-types';
import { LogLevel } from '../shared/logging-types';
import IpcOutput from './lib/logging';
@@ -56,6 +57,8 @@ interface IPreferredLocaleDescriptor {
code: string;
}
+type LoginState = 'none' | 'logging in' | 'creating account' | 'too many devices';
+
const SUPPORTED_LOCALE_LIST = [
{ name: 'Dansk', code: 'da' },
{ name: 'Deutsch', code: 'de' },
@@ -95,8 +98,10 @@ export default class AppRenderer {
private relayListPair!: IRelayListPair;
private tunnelState!: TunnelState;
private settings!: ISettings;
+ private deviceConfig?: IDeviceConfig;
private guiSettings!: IGuiSettingsState;
- private doingLogin = false;
+ private loginState: LoginState = 'none';
+ private previousLoginState: LoginState = 'none';
private loginScheduler = new Scheduler();
private connectedToDaemon = false;
private getLocationPromise?: Promise<ILocation>;
@@ -123,6 +128,15 @@ export default class AppRenderer {
this.setAccountExpiry(newAccountData?.expiry);
});
+ IpcRendererEventChannel.account.listenDevice((deviceEvent) => {
+ const oldDeviceConfig = this.deviceConfig;
+ this.handleAccountChange(deviceEvent, oldDeviceConfig?.accountToken);
+ });
+
+ IpcRendererEventChannel.account.listenDevices((devices) => {
+ this.reduxActions.account.updateDevices(devices);
+ });
+
IpcRendererEventChannel.accountHistory.listen((newAccountHistory?: AccountToken) => {
this.setAccountHistory(newAccountHistory);
});
@@ -133,10 +147,7 @@ export default class AppRenderer {
});
IpcRendererEventChannel.settings.listen((newSettings: ISettings) => {
- const oldSettings = this.settings;
-
this.setSettings(newSettings);
- this.handleAccountChange(oldSettings.accountToken, newSettings.accountToken);
this.updateBlockedState(this.tunnelState, newSettings.blockWhenDisconnected);
});
@@ -160,14 +171,6 @@ export default class AppRenderer {
this.storeAutoStart(autoStart);
});
- IpcRendererEventChannel.wireguardKeys.listenPublicKey((publicKey?: IWireguardPublicKey) => {
- this.setWireguardPublicKey(publicKey);
- });
-
- IpcRendererEventChannel.wireguardKeys.listenKeygenEvent((event: KeygenEvent) => {
- this.reduxActions.settings.setWireguardKeygenEvent(event);
- });
-
IpcRendererEventChannel.windowsSplitTunneling.listen((applications: IApplication[]) => {
this.reduxActions.settings.setSplitTunnelingApplications(applications);
});
@@ -201,7 +204,7 @@ export default class AppRenderer {
this.setAccountExpiry(initialState.accountData?.expiry);
this.setSettings(initialState.settings);
- this.handleAccountChange(undefined, initialState.settings.accountToken);
+ this.handleAccountChange({ deviceConfig: initialState.deviceConfig }, undefined);
this.setAccountHistory(initialState.accountHistory);
this.setTunnelState(initialState.tunnelState);
this.updateBlockedState(initialState.tunnelState, initialState.settings.blockWhenDisconnected);
@@ -211,7 +214,6 @@ export default class AppRenderer {
this.setUpgradeVersion(initialState.upgradeVersion);
this.setGuiSettings(initialState.guiSettings);
this.storeAutoStart(initialState.autoStart);
- this.setWireguardPublicKey(initialState.wireguardPublicKey);
this.setChangelog(initialState.changelog);
if (initialState.macOsScrollbarVisibility !== undefined) {
@@ -237,10 +239,7 @@ export default class AppRenderer {
void this.updateLocation();
- const navigationBase = this.getNavigationBase(
- initialState.isConnected,
- initialState.settings.accountToken,
- );
+ const navigationBase = this.getNavigationBase();
this.history = new History(navigationBase);
}
@@ -264,24 +263,34 @@ export default class AppRenderer {
);
}
- public async login(accountToken: AccountToken) {
+ public login = async (accountToken: AccountToken) => {
const actions = this.reduxActions;
actions.account.startLogin(accountToken);
log.info('Logging in');
- this.doingLogin = true;
+ this.previousLoginState = this.loginState;
+ this.loginState = 'logging in';
try {
await IpcRendererEventChannel.account.login(accountToken);
- actions.account.updateAccountToken(accountToken);
- actions.account.loggedIn();
- this.redirectToConnect();
} catch (e) {
const error = e as Error;
- actions.account.loginFailed(error);
+ if (error.message === 'Too many devices') {
+ actions.account.loginTooManyDevices(error);
+ this.loginState = 'too many devices';
+ this.history.reset(RoutePath.tooManyDevices, transitions.push);
+ } else {
+ actions.account.loginFailed(error);
+ }
}
- }
+ };
+
+ public cancelLogin = (): void => {
+ const reduxAccount = this.reduxActions.account;
+ reduxAccount.loggedOut();
+ this.loginState = 'none';
+ };
public async logout() {
try {
@@ -292,17 +301,22 @@ export default class AppRenderer {
}
}
+ public leaveRevokedDevice = async () => {
+ const reduxAccount = this.reduxActions.account;
+ reduxAccount.loggedOut();
+ this.resetNavigation();
+ await this.disconnectTunnel();
+ };
+
public async createNewAccount() {
log.info('Creating account');
const actions = this.reduxActions;
actions.account.startCreateAccount();
- this.doingLogin = true;
+ this.loginState = 'creating account';
try {
- const accountToken = await IpcRendererEventChannel.account.create();
- const accountExpiry = new Date().toISOString();
- actions.account.accountCreated(accountToken, accountExpiry);
+ await IpcRendererEventChannel.account.create();
this.redirectToConnect();
} catch (e) {
const error = e as Error;
@@ -318,6 +332,20 @@ export default class AppRenderer {
IpcRendererEventChannel.account.updateData();
}
+ public getDevice = (): Promise<IDevice | undefined> => {
+ return IpcRendererEventChannel.account.getDevice();
+ };
+
+ public fetchDevices = async (accountToken: AccountToken): Promise<Array<IDevice>> => {
+ const devices = await IpcRendererEventChannel.account.listDevices(accountToken);
+ this.reduxActions.account.updateDevices(devices);
+ return devices;
+ };
+
+ public removeDevice(deviceRemoval: IDeviceRemoval): Promise<void> {
+ return IpcRendererEventChannel.account.removeDevice(deviceRemoval);
+ }
+
public async connectTunnel(): Promise<void> {
return IpcRendererEventChannel.tunnel.connect();
}
@@ -425,33 +453,6 @@ export default class AppRenderer {
IpcRendererEventChannel.guiSettings.setUnpinnedWindow(unpinnedWindow);
}
- public async verifyWireguardKey(publicKey: IWgKey) {
- const actions = this.reduxActions;
- actions.settings.verifyWireguardKey(publicKey);
- try {
- const valid = await IpcRendererEventChannel.wireguardKeys.verifyKey();
- actions.settings.completeWireguardKeyVerification(valid);
- } catch (e) {
- const error = e as Error;
- log.error(`Failed to verify WireGuard key - ${error.message}`);
- actions.settings.completeWireguardKeyVerification(undefined);
- }
- }
-
- public async generateWireguardKey() {
- const actions = this.reduxActions;
- actions.settings.generateWireguardKey();
- const keygenEvent = await IpcRendererEventChannel.wireguardKeys.generateKey();
- actions.settings.setWireguardKeygenEvent(keygenEvent);
- }
-
- public async replaceWireguardKey(oldKey: IWgKey) {
- const actions = this.reduxActions;
- actions.settings.replaceWireguardKey(oldKey);
- const keygenEvent = await IpcRendererEventChannel.wireguardKeys.generateKey();
- actions.settings.setWireguardKeygenEvent(keygenEvent);
- }
-
public getLinuxSplitTunnelingApplications() {
return IpcRendererEventChannel.linuxSplitTunneling.getApplications();
}
@@ -650,40 +651,56 @@ export default class AppRenderer {
private resetNavigation() {
if (this.history) {
- const pathname = this.history.location.pathname;
- const nextPath = this.getNavigationBase(this.connectedToDaemon, this.settings.accountToken);
+ const pathname = this.history.location.pathname as RoutePath;
+ const nextPath = this.getNavigationBase() as RoutePath;
- // First level contains the possible next locations and the second level contains the possible
- // current locations.
- const navigationTransitions: {
- [from: string]: { [to: string]: ITransitionSpecification };
- } = {
- '/': {
- '/login': transitions.pop,
- '/main': transitions.pop,
- '*': transitions.dismiss,
- },
- '/login': {
- '/': transitions.push,
- '/main': transitions.pop,
- '*': transitions.none,
- },
- '/main': {
- '/': transitions.push,
- '/login': transitions.push,
- '*': transitions.dismiss,
- },
- };
+ if (pathname !== nextPath) {
+ // First level contains the possible next locations and the second level contains the
+ // possible current locations.
+ const navigationTransitions: Partial<
+ Record<RoutePath, Partial<Record<RoutePath | '*', ITransitionSpecification>>>
+ > = {
+ [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.none,
+ },
+ [RoutePath.main]: {
+ [RoutePath.launch]: transitions.push,
+ [RoutePath.login]: transitions.push,
+ [RoutePath.tooManyDevices]: transitions.push,
+ '*': transitions.dismiss,
+ },
+ [RoutePath.deviceRevoked]: {
+ '*': transitions.pop,
+ },
+ };
- const transition =
- navigationTransitions[nextPath][pathname] ?? navigationTransitions[nextPath]['*'];
- this.history.reset(nextPath, transition);
+ const transition =
+ navigationTransitions[nextPath]?.[pathname] ?? navigationTransitions[nextPath]?.['*'];
+ this.history.reset(nextPath, transition);
+ }
}
}
- private getNavigationBase(connectedToDaemon: boolean, accountToken?: string): RoutePath {
- if (connectedToDaemon) {
- return accountToken ? RoutePath.main : RoutePath.login;
+ private getNavigationBase(): RoutePath {
+ if (this.connectedToDaemon) {
+ const loginState = this.reduxStore.getState().account.status;
+ const deviceRevoked = loginState.type === 'none' && loginState.deviceRevoked;
+
+ if (deviceRevoked) {
+ return RoutePath.deviceRevoked;
+ } else if (this.deviceConfig?.accountToken) {
+ return RoutePath.main;
+ } else {
+ return RoutePath.login;
+ }
} else {
return RoutePath.launch;
}
@@ -772,22 +789,49 @@ export default class AppRenderer {
}
}
- private handleAccountChange(oldAccount?: string, newAccount?: string) {
+ private handleAccountChange(newDeviceEvent: IDeviceEvent, oldAccount?: string) {
const reduxAccount = this.reduxActions.account;
+ this.deviceConfig = newDeviceEvent.deviceConfig;
+ const newAccount = newDeviceEvent.deviceConfig?.accountToken;
+ const newDevice = newDeviceEvent.deviceConfig?.device;
+
if (oldAccount && !newAccount) {
this.loginScheduler.cancel();
- reduxAccount.loggedOut();
+ if (!this.reduxStore.getState().account.loggingOut && newDeviceEvent.remote) {
+ reduxAccount.deviceRevoked();
+ } else {
+ reduxAccount.loggedOut();
+ }
this.resetNavigation();
- } else if (newAccount && oldAccount !== newAccount && !this.doingLogin) {
- reduxAccount.updateAccountToken(newAccount);
- reduxAccount.loggedIn();
+ } else if (newAccount !== undefined && newDevice !== undefined && oldAccount !== newAccount) {
+ switch (this.loginState) {
+ case 'none':
+ case 'logging in':
+ reduxAccount.loggedIn({ accountToken: newAccount, device: newDevice });
- this.resetNavigation();
+ if (this.previousLoginState === 'too many devices') {
+ this.resetNavigation();
+ } else {
+ this.redirectToConnect();
+ }
+ break;
+ case 'creating account':
+ reduxAccount.accountCreated(
+ { accountToken: newAccount, device: newDevice },
+ new Date().toISOString(),
+ );
+ break;
+ }
+
+ if (this.loginState !== 'logging in' && this.loginState !== 'creating account') {
+ this.resetNavigation();
+ }
}
- this.doingLogin = false;
+ this.previousLoginState = this.loginState;
+ this.loginState = 'none';
}
private setLocation(location: Partial<ILocation>) {
@@ -839,10 +883,6 @@ export default class AppRenderer {
this.reduxActions.settings.updateAutoStart(autoStart);
}
- private setWireguardPublicKey(publicKey?: IWireguardPublicKey) {
- this.reduxActions.settings.setWireguardKey(publicKey);
- }
-
private setChangelog(changelog: IChangelog) {
this.reduxActions.userInterface.setChangelog(changelog);
}
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx
index 833a06d1c2..4a84e3c110 100644
--- a/gui/src/renderer/components/Account.tsx
+++ b/gui/src/renderer/components/Account.tsx
@@ -9,6 +9,8 @@ import {
AccountRowLabel,
AccountRows,
AccountRowValue,
+ DeviceRowValue,
+ StyledSpinnerContainer,
StyledBuyCreditButton,
StyledContainer,
StyledRedeemVoucherButton,
@@ -17,24 +19,36 @@ import AccountTokenLabel from './AccountTokenLabel';
import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import { Layout } from './Layout';
+import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-import { AccountToken } from '../../shared/daemon-rpc-types';
+import { AccountToken, IDevice } from '../../shared/daemon-rpc-types';
+import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
interface IProps {
+ deviceName?: string;
accountToken?: AccountToken;
accountExpiry?: string;
expiryLocale: string;
isOffline: boolean;
+ prepareLogout: () => void;
+ cancelLogout: () => void;
onLogout: () => void;
onClose: () => void;
onBuyMore: () => Promise<void>;
updateAccountData: () => void;
+ getDevice: () => Promise<IDevice | undefined>;
}
-export default class Account extends React.Component<IProps> {
+interface IState {
+ logoutDialogState: 'hidden' | 'checking-ports' | 'confirm';
+}
+
+export default class Account extends React.Component<IProps, IState> {
+ public state: IState = { logoutDialogState: 'hidden' };
+
public componentDidMount() {
this.props.updateAccountData();
}
@@ -63,6 +77,13 @@ export default class Account extends React.Component<IProps> {
<AccountRows>
<AccountRow>
<AccountRowLabel>
+ {messages.pgettext('device-management', 'Device name')}
+ </AccountRowLabel>
+ <DeviceRowValue>{this.props.deviceName}</DeviceRowValue>
+ </AccountRow>
+
+ <AccountRow>
+ <AccountRowLabel>
{messages.pgettext('account-view', 'Account number')}
</AccountRowLabel>
<AccountRowValue
@@ -105,16 +126,88 @@ export default class Account extends React.Component<IProps> {
<StyledRedeemVoucherButton />
- <AppButton.RedButton onClick={this.props.onLogout}>
+ <AppButton.RedButton onClick={this.onTryLogout}>
{messages.pgettext('account-view', 'Log out')}
</AppButton.RedButton>
</AccountFooter>
</AccountContainer>
</StyledContainer>
+
+ {this.renderLogoutDialog()}
</Layout>
</BackAction>
);
}
+
+ private renderLogoutDialog() {
+ const modalType =
+ this.state.logoutDialogState === 'checking-ports' ? undefined : ModalAlertType.warning;
+
+ const message =
+ this.state.logoutDialogState === 'checking-ports' ? (
+ <StyledSpinnerContainer>
+ <ImageView source="icon-spinner" width={60} height={60} />
+ </StyledSpinnerContainer>
+ ) : (
+ <ModalMessage>
+ {
+ // TRANSLATORS: This is is a further explanation of what happens when logging out.
+ messages.pgettext(
+ 'device-management',
+ 'The ports forwarded to this device will be deleted if you log out.',
+ )
+ }
+ </ModalMessage>
+ );
+
+ const buttons =
+ this.state.logoutDialogState === 'checking-ports'
+ ? []
+ : [
+ <AppButton.RedButton key="logout" onClick={this.props.onLogout}>
+ {
+ // TRANSLATORS: Confirmation button when logging out
+ messages.pgettext('device-management', 'Log out anyway')
+ }
+ </AppButton.RedButton>,
+ <AppButton.BlueButton key="back" onClick={this.cancelLogout}>
+ {messages.gettext('Back')}
+ </AppButton.BlueButton>,
+ ];
+
+ return (
+ <ModalAlert
+ isOpen={this.state.logoutDialogState !== 'hidden'}
+ type={modalType}
+ buttons={buttons}>
+ {message}
+ </ModalAlert>
+ );
+ }
+
+ private onTryLogout = async () => {
+ this.setState({ logoutDialogState: 'checking-ports' });
+ this.props.prepareLogout();
+
+ const device = await this.props.getDevice();
+ if (device === undefined) {
+ this.onHideLogoutConfirmationDialog();
+ } else if (device.ports !== undefined && device.ports.length > 0) {
+ this.setState({ logoutDialogState: 'confirm' });
+ } else {
+ this.props.onLogout();
+ this.onHideLogoutConfirmationDialog();
+ }
+ };
+
+ private cancelLogout = () => {
+ this.props.cancelLogout();
+ this.onHideLogoutConfirmationDialog();
+ };
+
+ private onHideLogoutConfirmationDialog = () => {
+ this.setState({ logoutDialogState: 'hidden' });
+ };
}
function FormattedAccountExpiry(props: { expiry?: string; locale: string }) {
diff --git a/gui/src/renderer/components/AccountStyles.tsx b/gui/src/renderer/components/AccountStyles.tsx
index 0e403231da..549b0cda7d 100644
--- a/gui/src/renderer/components/AccountStyles.tsx
+++ b/gui/src/renderer/components/AccountStyles.tsx
@@ -44,10 +44,21 @@ export const AccountRowValue = styled(AccountRowText)(normalText, {
color: colors.white,
});
+export const DeviceRowValue = styled(AccountRowValue)({
+ textTransform: 'capitalize',
+});
+
export const AccountOutOfTime = styled(AccountRowValue)({
color: colors.red,
});
+export const StyledSpinnerContainer = styled.div({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: '8px 0',
+});
+
export const AccountFooter = styled.div({
display: 'flex',
flexDirection: 'column',
diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx
index 8ecf6c0052..596a638f51 100644
--- a/gui/src/renderer/components/AdvancedSettings.tsx
+++ b/gui/src/renderer/components/AdvancedSettings.tsx
@@ -1,12 +1,8 @@
import * as React from 'react';
-import { sprintf } from 'sprintf-js';
import { TunnelProtocol } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
-import { WgKeyState } from '../redux/settings/reducers';
import {
StyledNavigationScrollbars,
- StyledNoWireguardKeyError,
- StyledNoWireguardKeyErrorContainer,
StyledSelectorForFooter,
StyledTunnelProtocolContainer,
} from './AdvancedSettingsStyles';
@@ -28,7 +24,6 @@ interface IProps {
enableIpv6: boolean;
blockWhenDisconnected: boolean;
tunnelProtocol?: TunnelProtocol;
- wireguardKeyState: WgKeyState;
setEnableIpv6: (value: boolean) => void;
setBlockWhenDisconnected: (value: boolean) => void;
setTunnelProtocol: (value: OptionalTunnelProtocol) => void;
@@ -49,9 +44,28 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
private blockWhenDisconnectedRef = React.createRef<Switch>();
- public render() {
- const hasWireguardKey = this.props.wireguardKeyState.type === 'key-set';
+ private tunnelProtocolItems: Array<ISelectorItem<OptionalTunnelProtocol>>;
+
+ public constructor(props: IProps) {
+ super(props);
+
+ this.tunnelProtocolItems = [
+ {
+ label: messages.gettext('Automatic'),
+ value: undefined,
+ },
+ {
+ label: messages.pgettext('advanced-settings-view', 'WireGuard'),
+ value: 'wireguard',
+ },
+ {
+ label: messages.pgettext('advanced-settings-view', 'OpenVPN'),
+ value: 'openvpn',
+ },
+ ];
+ }
+ public render() {
return (
<BackAction action={this.props.onClose}>
<Layout>
@@ -141,22 +155,10 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
<StyledTunnelProtocolContainer>
<StyledSelectorForFooter
title={messages.pgettext('advanced-settings-view', 'Tunnel protocol')}
- values={this.tunnelProtocolItems(hasWireguardKey)}
+ values={this.tunnelProtocolItems}
value={this.props.tunnelProtocol}
onSelect={this.onSelectTunnelProtocol}
/>
- {!hasWireguardKey && (
- <StyledNoWireguardKeyErrorContainer>
- <AriaDescription>
- <StyledNoWireguardKeyError>
- {messages.pgettext(
- 'advanced-settings-view',
- 'To enable WireGuard, generate a key under the "WireGuard key" setting below.',
- )}
- </StyledNoWireguardKeyError>
- </AriaDescription>
- </StyledNoWireguardKeyErrorContainer>
- )}
</StyledTunnelProtocolContainer>
</AriaInputGroup>
@@ -191,31 +193,6 @@ export default class AdvancedSettings extends React.Component<IProps, IState> {
);
}
- private tunnelProtocolItems = (
- hasWireguardKey: boolean,
- ): Array<ISelectorItem<OptionalTunnelProtocol>> => {
- return [
- {
- label: messages.gettext('Automatic'),
- value: undefined,
- },
- {
- label: hasWireguardKey
- ? messages.pgettext('advanced-settings-view', 'WireGuard')
- : sprintf('%(label)s (%(error)s)', {
- label: messages.pgettext('advanced-settings-view', 'WireGuard'),
- error: messages.pgettext('advanced-settings-view', 'missing key'),
- }),
- value: 'wireguard',
- disabled: !hasWireguardKey,
- },
- {
- label: messages.pgettext('advanced-settings-view', 'OpenVPN'),
- value: 'openvpn',
- },
- ];
- };
-
private renderConfirmBlockWhenDisconnectedAlert = () => {
return (
<ModalAlert
diff --git a/gui/src/renderer/components/AdvancedSettingsStyles.tsx b/gui/src/renderer/components/AdvancedSettingsStyles.tsx
index bdc84701c6..f7a87b5311 100644
--- a/gui/src/renderer/components/AdvancedSettingsStyles.tsx
+++ b/gui/src/renderer/components/AdvancedSettingsStyles.tsx
@@ -1,6 +1,4 @@
import styled from 'styled-components';
-import { colors } from '../../config.json';
-import * as Cell from './cell';
import { NavigationScrollbars } from './NavigationBar';
import Selector from './cell/Selector';
@@ -19,12 +17,3 @@ export const StyledTunnelProtocolContainer = styled(StyledSelectorContainer)({
export const StyledNavigationScrollbars = styled(NavigationScrollbars)({
flex: 1,
});
-
-export const StyledNoWireguardKeyErrorContainer = styled(Cell.Footer)({
- paddingBottom: 0,
-});
-
-export const StyledNoWireguardKeyError = styled(Cell.FooterText)({
- fontWeight: 700,
- color: colors.red,
-});
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index f72de697e3..46b322785a 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -16,7 +16,6 @@ import SelectLanguagePage from '../containers/SelectLanguagePage';
import SelectLocationPage from '../containers/SelectLocationPage';
import SettingsPage from '../containers/SettingsPage';
import SupportPage from '../containers/SupportPage';
-import WireguardKeysPage from '../containers/WireguardKeysPage';
import WireguardSettingsPage from '../containers/WireguardSettingsPage';
import { IHistoryProps, ITransitionSpecification, transitions, withHistory } from '../lib/history';
import {
@@ -27,6 +26,8 @@ import {
} from './ExpiredAccountAddTime';
import { RoutePath } from '../lib/routes';
import FilterByProvider from './FilterByProvider';
+import TooManyDevices from './TooManyDevices';
+import { DeviceRevokedView } from './DeviceRevokedView';
interface IAppRoutesState {
currentLocation: IHistoryProps['history']['location'];
@@ -77,6 +78,8 @@ class AppRouter extends React.Component<IHistoryProps, IAppRoutesState> {
<Switch key={location.key} location={location}>
<Route exact path={RoutePath.launch} component={Launch} />
<Route exact path={RoutePath.login} component={LoginPage} />
+ <Route exact path={RoutePath.tooManyDevices} component={TooManyDevices} />
+ <Route exact path={RoutePath.deviceRevoked} component={DeviceRevokedView} />
<Route exact path={RoutePath.main} component={MainView} />
<Route exact path={RoutePath.redeemVoucher} component={VoucherInput} />
<Route
@@ -92,7 +95,6 @@ class AppRouter extends React.Component<IHistoryProps, IAppRoutesState> {
<Route exact path={RoutePath.preferences} component={PreferencesPage} />
<Route exact path={RoutePath.advancedSettings} component={AdvancedSettingsPage} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettingsPage} />
- <Route exact path={RoutePath.wireguardKeys} component={WireguardKeysPage} />
<Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettingsPage} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
<Route exact path={RoutePath.support} component={SupportPage} />
diff --git a/gui/src/renderer/components/DeviceRevokedView.tsx b/gui/src/renderer/components/DeviceRevokedView.tsx
new file mode 100644
index 0000000000..6a51694af1
--- /dev/null
+++ b/gui/src/renderer/components/DeviceRevokedView.tsx
@@ -0,0 +1,95 @@
+import styled from 'styled-components';
+import { colors } from '../../config.json';
+import { messages } from '../../shared/gettext';
+import { useAppContext } from '../context';
+import { useSelector } from '../redux/store';
+import * as AppButton from './AppButton';
+import CustomScrollbars from './CustomScrollbars';
+import { calculateHeaderBarStyle, DefaultHeaderBar } from './HeaderBar';
+import { Container } from './Layout';
+import ImageView from './ImageView';
+import { Layout } from './Layout';
+import { bigText, smallText } from './common-styles';
+
+export const StyledHeader = styled(DefaultHeaderBar)({
+ flex: 0,
+});
+
+export const StyledCustomScrollbars = styled(CustomScrollbars)({
+ flex: 1,
+});
+
+export const StyledContainer = styled(Container)({
+ paddingTop: '22px',
+ minHeight: '100%',
+ backgroundColor: colors.darkBlue,
+});
+
+export const StyledBody = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ padding: '0 22px',
+});
+
+export const StyledFooter = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 0,
+ padding: '18px 22px 22px',
+});
+
+export const StyledStatusIcon = styled.div({
+ alignSelf: 'center',
+ width: '60px',
+ height: '60px',
+ marginBottom: '18px',
+});
+
+export const StyledTitle = styled.span(bigText, {
+ lineHeight: '38px',
+ marginBottom: '8px',
+ color: colors.white,
+});
+
+export const StyledMessage = styled.span(smallText, {
+ marginBottom: '20px',
+ color: colors.white,
+});
+
+export function DeviceRevokedView() {
+ const { leaveRevokedDevice } = useAppContext();
+ const tunnelState = useSelector((state) => state.connection.status);
+
+ const Button = tunnelState.state === 'disconnected' ? AppButton.GreenButton : AppButton.RedButton;
+
+ return (
+ <Layout>
+ <StyledHeader barStyle={calculateHeaderBarStyle(tunnelState)} />
+ <StyledCustomScrollbars fillContainer>
+ <StyledContainer>
+ <StyledBody>
+ <StyledStatusIcon>
+ <ImageView source="icon-fail" height={60} width={60} />
+ </StyledStatusIcon>
+ <StyledTitle>
+ {messages.pgettext('device-management', 'Device is inactive')}
+ </StyledTitle>
+ <StyledMessage>
+ {messages.pgettext(
+ 'device-management',
+ 'You have removed this device from your list of active devices. To connect with this device again, log in.',
+ )}
+ </StyledMessage>
+ </StyledBody>
+
+ <StyledFooter>
+ <Button onClick={leaveRevokedDevice}>
+ {messages.pgettext('device-management', 'Go to login')}
+ </Button>
+ </StyledFooter>
+ </StyledContainer>
+ </StyledCustomScrollbars>
+ </Layout>
+ );
+}
diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
index fa98b1df59..47939b8c65 100644
--- a/gui/src/renderer/components/ExpiredAccountAddTime.tsx
+++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
@@ -273,7 +273,7 @@ function HeaderBar() {
}
function useFinishedCallback() {
- const { loggedIn } = useActions(account);
+ const { accountSetupFinished } = useActions(account);
const history = useHistory();
const isNewAccount = useSelector(
@@ -283,11 +283,11 @@ function useFinishedCallback() {
const callback = useCallback(() => {
// Changes login method from "new_account" to "existing_account"
if (isNewAccount) {
- loggedIn();
+ accountSetupFinished();
}
history.reset(RoutePath.main, undefined, transitions.push);
- }, [isNewAccount, loggedIn, history]);
+ }, [isNewAccount, accountSetupFinished, history]);
return callback;
}
diff --git a/gui/src/renderer/components/KeyboardNavigation.tsx b/gui/src/renderer/components/KeyboardNavigation.tsx
index 4cfba89452..6b3851a49f 100644
--- a/gui/src/renderer/components/KeyboardNavigation.tsx
+++ b/gui/src/renderer/components/KeyboardNavigation.tsx
@@ -1,5 +1,7 @@
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
+import { useLocation } from 'react-router';
import { useHistory } from '../lib/history';
+import { disableDismissForRoutes, RoutePath } from '../lib/routes';
interface IKeyboardNavigationProps {
children: React.ReactElement;
@@ -9,18 +11,22 @@ interface IKeyboardNavigationProps {
export default function KeyboardNavigation(props: IKeyboardNavigationProps) {
const history = useHistory();
const [backAction, setBackAction] = useState<IBackActionConfiguration>();
+ const location = useLocation();
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Escape') {
- if (event.shiftKey) {
- history.dismiss(true);
- } else {
- backAction?.action();
+ const path = location.pathname as RoutePath;
+ if (!disableDismissForRoutes.includes(path)) {
+ if (event.shiftKey) {
+ history.dismiss(true);
+ } else {
+ backAction?.action();
+ }
}
}
},
- [history.dismiss, backAction],
+ [history.dismiss, backAction, location.pathname],
);
useEffect(() => {
diff --git a/gui/src/renderer/components/Login.tsx b/gui/src/renderer/components/Login.tsx
index 31afee88b0..ab93e1d6ac 100644
--- a/gui/src/renderer/components/Login.tsx
+++ b/gui/src/renderer/components/Login.tsx
@@ -151,6 +151,7 @@ export default class Login extends React.Component<IProps, IState> {
private formTitle() {
switch (this.props.loginState.type) {
case 'logging in':
+ case 'too many devices':
return this.props.loginState.method === 'existing_account'
? messages.pgettext('login-view', 'Logging in...')
: messages.pgettext('login-view', 'Creating account...');
@@ -173,6 +174,8 @@ export default class Login extends React.Component<IProps, IState> {
return this.props.loginState.method === 'existing_account'
? this.props.loginState.error.message || messages.pgettext('login-view', 'Unknown error')
: messages.pgettext('login-view', 'Failed to create account');
+ case 'too many devices':
+ return messages.pgettext('login-view', 'Too many devices');
case 'logging in':
return this.props.loginState.method === 'existing_account'
? messages.pgettext('login-view', 'Checking account number')
@@ -209,7 +212,11 @@ export default class Login extends React.Component<IProps, IState> {
}
private allowInteraction() {
- return this.props.loginState.type !== 'logging in' && this.props.loginState.type !== 'ok';
+ return (
+ this.props.loginState.type !== 'logging in' &&
+ this.props.loginState.type !== 'ok' &&
+ this.props.loginState.type !== 'too many devices'
+ );
}
private allowCreateAccount() {
diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx
index c7a8851a90..56878fb521 100644
--- a/gui/src/renderer/components/MainView.tsx
+++ b/gui/src/renderer/components/MainView.tsx
@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react';
-import { useSelector } from 'react-redux';
import { hasExpired } from '../../shared/account-expiry';
-import { IReduxState } from '../redux/store';
+import { useSelector } from '../redux/store';
import ConnectPage from '../containers/ConnectPage';
import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer';
import { useHistory } from '../lib/history';
@@ -9,13 +8,15 @@ import { RoutePath } from '../lib/routes';
export default function MainView() {
const history = useHistory();
- const accountExpiry = useSelector((state: IReduxState) => state.account.expiry);
- const accountHasExpired = accountExpiry && hasExpired(accountExpiry);
+ const accountExpiry = useSelector((state) => state.account.expiry);
+ const accountHasExpired = accountExpiry !== undefined && hasExpired(accountExpiry);
const isNewAccount = useSelector(
- (state: IReduxState) =>
- state.account.status.type === 'ok' && state.account.status.method === 'new_account',
+ (state) => state.account.status.type === 'ok' && state.account.status.method === 'new_account',
+ );
+
+ const [showAccountExpired, setShowAccountExpired] = useState<boolean>(
+ isNewAccount || accountHasExpired,
);
- const [showAccountExpired, setShowAccountExpired] = useState(isNewAccount || accountHasExpired);
useEffect(() => {
if (accountHasExpired) {
@@ -25,5 +26,9 @@ export default function MainView() {
}
}, [showAccountExpired, accountHasExpired]);
- return showAccountExpired ? <ExpiredAccountErrorViewContainer /> : <ConnectPage />;
+ if (showAccountExpired) {
+ return <ExpiredAccountErrorViewContainer />;
+ } else {
+ return <ConnectPage />;
+ }
}
diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx
index 1f79d8b90d..de8b547087 100644
--- a/gui/src/renderer/components/NotificationArea.tsx
+++ b/gui/src/renderer/components/NotificationArea.tsx
@@ -9,7 +9,6 @@ import {
InAppNotificationProvider,
InconsistentVersionNotificationProvider,
NotificationAction,
- NoValidKeyNotificationProvider,
ReconnectingNotificationProvider,
UnsupportedVersionNotificationProvider,
UpdateAvailableNotificationProvider,
@@ -38,12 +37,6 @@ export default function NotificationArea(props: IProps) {
const blockWhenDisconnected = useSelector(
(state: IReduxState) => state.settings.blockWhenDisconnected,
);
- const tunnelProtocol = useSelector((state: IReduxState) =>
- 'normal' in state.settings.relaySettings
- ? state.settings.relaySettings.normal.tunnelProtocol
- : undefined,
- );
- const wireGuardKey = useSelector((state: IReduxState) => state.settings.wireguardKeyState);
const hasExcludedApps = useSelector(
(state: IReduxState) =>
state.settings.splitTunneling && state.settings.splitTunnelingApplications.length > 0,
@@ -58,7 +51,6 @@ export default function NotificationArea(props: IProps) {
hasExcludedApps,
}),
new ErrorNotificationProvider({ tunnelState, accountExpiry, hasExcludedApps }),
- new NoValidKeyNotificationProvider({ tunnelProtocol, wireGuardKey }),
new InconsistentVersionNotificationProvider({ consistent: version.consistent }),
new UnsupportedVersionNotificationProvider(version),
];
diff --git a/gui/src/renderer/components/TooManyDevices.tsx b/gui/src/renderer/components/TooManyDevices.tsx
new file mode 100644
index 0000000000..b743dd2aec
--- /dev/null
+++ b/gui/src/renderer/components/TooManyDevices.tsx
@@ -0,0 +1,317 @@
+import { useCallback, useEffect } from 'react';
+import { sprintf } from 'sprintf-js';
+import styled from 'styled-components';
+import { colors } from '../../config.json';
+import { IDevice } from '../../shared/daemon-rpc-types';
+import { messages } from '../../shared/gettext';
+import { capitalizeEveryWord } from '../../shared/string-helpers';
+import { useAppContext } from '../context';
+import { transitions, useHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
+import { useBoolean } from '../lib/utilityHooks';
+import { formatMarkdown } from '../markdown-formatter';
+import { useSelector } from '../redux/store';
+import * as AppButton from './AppButton';
+import * as Cell from './cell';
+import { bigText } from './common-styles';
+import CustomScrollbars from './CustomScrollbars';
+import { Brand, HeaderBarSettingsButton } from './HeaderBar';
+import ImageView from './ImageView';
+import { Header, Layout, SettingsContainer } from './Layout';
+import List from './List';
+import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal';
+
+const StyledCustomScrollbars = styled(CustomScrollbars)({
+ flex: 1,
+});
+
+const StyledContainer = styled(SettingsContainer)({
+ paddingTop: '14px',
+ minHeight: '100%',
+});
+
+const StyledBody = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 1,
+ paddingBottom: 'auto',
+});
+
+const StyledFooter = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 0,
+ padding: '18px 22px 22px',
+});
+
+const StyledStatusIcon = styled.div({
+ alignSelf: 'center',
+ width: '60px',
+ height: '60px',
+ marginBottom: '18px',
+});
+
+const StyledTitle = styled.span(bigText, {
+ lineHeight: '38px',
+ margin: '0 22px 8px',
+ color: colors.white,
+});
+
+const StyledLabel = styled.span({
+ fontFamily: 'Open Sans',
+ fontSize: '13px',
+ fontWeight: 600,
+ lineHeight: '20px',
+ color: colors.white,
+ margin: '0 22px 18px',
+});
+
+const StyledDeviceList = styled(Cell.CellButtonGroup)({
+ marginBottom: 0,
+ flex: '0 0',
+});
+
+const StyledSpacer = styled.div({
+ flex: '1',
+});
+
+const StyledCellContainer = styled(Cell.Container)({
+ marginBottom: '1px',
+});
+
+const StyledDeviceName = styled(Cell.Label)({
+ textTransform: 'capitalize',
+});
+
+const StyledRemoveDeviceButton = styled.button({
+ cursor: 'default',
+ padding: 0,
+ marginLeft: 8,
+ backgroundColor: 'transparent',
+ border: 'none',
+});
+
+export default function TooManyDevices() {
+ const history = useHistory();
+ const { fetchDevices, removeDevice, login, cancelLogin } = useAppContext();
+ const accountToken = useSelector((state) => state.account.accountToken)!;
+ const devices = useSelector((state) => state.account.devices);
+
+ const onRemoveDevice = useCallback(
+ async (deviceId: string) => {
+ await removeDevice({ accountToken, deviceId });
+ },
+ [removeDevice, accountToken],
+ );
+
+ const continueLogin = useCallback(() => login(accountToken), [login, accountToken]);
+ const cancel = useCallback(() => {
+ cancelLogin();
+ history.reset(RoutePath.login, transitions.pop);
+ }, [history.reset, cancelLogin]);
+
+ useEffect(() => void fetchDevices(accountToken), []);
+
+ const iconSource = getIconSource(devices);
+ const title = getTitle(devices);
+ const subtitle = getSubtitle(devices);
+
+ return (
+ <ModalContainer>
+ <Layout>
+ <Header>
+ <Brand />
+ <HeaderBarSettingsButton />
+ </Header>
+ <StyledCustomScrollbars fillContainer>
+ <StyledContainer>
+ <StyledBody>
+ <StyledStatusIcon>
+ <ImageView key={iconSource} source={iconSource} height={60} width={60} />
+ </StyledStatusIcon>
+ {devices !== undefined && (
+ <>
+ <StyledTitle>{title}</StyledTitle>
+ <StyledLabel>{subtitle}</StyledLabel>
+ <DeviceList devices={devices} onRemoveDevice={onRemoveDevice} />
+ </>
+ )}
+ </StyledBody>
+
+ {devices !== undefined && (
+ <StyledFooter>
+ <AppButton.ButtonGroup>
+ <AppButton.GreenButton onClick={continueLogin} disabled={devices.length === 5}>
+ {
+ // TRANSLATORS: Button for continuing login process.
+ messages.pgettext('device-management', 'Continue with login')
+ }
+ </AppButton.GreenButton>
+ <AppButton.BlueButton onClick={cancel}>
+ {messages.gettext('Back')}
+ </AppButton.BlueButton>
+ </AppButton.ButtonGroup>
+ </StyledFooter>
+ )}
+ </StyledContainer>
+ </StyledCustomScrollbars>
+ </Layout>
+ </ModalContainer>
+ );
+}
+
+interface IDeviceListProps {
+ devices: Array<IDevice>;
+ onRemoveDevice: (deviceId: string) => Promise<void>;
+}
+
+function DeviceList(props: IDeviceListProps) {
+ return (
+ <StyledSpacer>
+ <StyledDeviceList>
+ <List items={props.devices} getKey={getDeviceKey}>
+ {(device) => <Device device={device} onRemove={props.onRemoveDevice} />}
+ </List>
+ </StyledDeviceList>
+ </StyledSpacer>
+ );
+}
+
+const getDeviceKey = (device: IDevice): string => device.id;
+
+interface IDeviceProps {
+ device: IDevice;
+ onRemove: (deviceId: string) => Promise<void>;
+}
+
+function Device(props: IDeviceProps) {
+ const [confirmationVisible, showConfirmation, hideConfirmation] = useBoolean(false);
+ const [deleting, setDeleting] = useBoolean(false);
+
+ const onRemove = useCallback(async () => {
+ await props.onRemove(props.device.id);
+ hideConfirmation();
+ setDeleting();
+ }, [props.onRemove, props.device.id, hideConfirmation, setDeleting]);
+
+ const capitalizedDeviceName = capitalizeEveryWord(props.device.name);
+
+ return (
+ <>
+ <StyledCellContainer>
+ <StyledDeviceName aria-hidden>{props.device.name}</StyledDeviceName>
+ <StyledRemoveDeviceButton
+ onClick={showConfirmation}
+ aria-label={sprintf(
+ // TRANSLATORS: Button action description provided to accessibility tools such as screen
+ // TRANSLATORS: readers.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(deviceName)s - The device name to remove.
+ messages.pgettext('accessibility', 'Remove device named %(deviceName)s'),
+ { deviceName: props.device.name },
+ )}>
+ <ImageView
+ source="icon-close"
+ tintColor={colors.white40}
+ tintHoverColor={colors.white60}
+ />
+ </StyledRemoveDeviceButton>
+ </StyledCellContainer>
+ <ModalAlert
+ isOpen={confirmationVisible}
+ type={ModalAlertType.warning}
+ iconColor={colors.red}
+ buttons={[
+ <AppButton.RedButton key="remove" onClick={onRemove} disabled={deleting}>
+ {
+ // TRANSLATORS: Confirmation button when logging out other device.
+ messages.pgettext('device-management', 'Yes, log out device')
+ }
+ </AppButton.RedButton>,
+ <AppButton.BlueButton key="back" onClick={hideConfirmation} disabled={deleting}>
+ {messages.gettext('Back')}
+ </AppButton.BlueButton>,
+ ]}
+ close={hideConfirmation}>
+ {deleting ? (
+ <ImageView source="icon-spinner" />
+ ) : (
+ <>
+ <ModalMessage>
+ {formatMarkdown(
+ sprintf(
+ // TRANSLATORS: Text displayed above button which logs out another device.
+ // TRANSLATORS: The text enclosed in "**" will appear bold.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(deviceName)s - The name of the device to log out.
+ messages.pgettext(
+ 'device-management',
+ 'Are you sure you want to log out of **%(deviceName)s**?',
+ ),
+ { deviceName: capitalizedDeviceName },
+ ),
+ )}
+ </ModalMessage>
+ {props.device.ports && props.device.ports.length > 0 && (
+ <ModalMessage>
+ {
+ // TRANSLATORS: Further information about consequences of logging out device.
+ messages.pgettext(
+ 'device-management',
+ 'This will delete all forwarded ports. Local settings will be saved.',
+ )
+ }
+ </ModalMessage>
+ )}
+ </>
+ )}
+ </ModalAlert>
+ </>
+ );
+}
+
+function getIconSource(devices?: Array<IDevice>): string {
+ if (devices) {
+ if (devices.length === 5) {
+ return 'icon-fail';
+ } else {
+ return 'icon-success';
+ }
+ } else {
+ return 'icon-spinner';
+ }
+}
+
+function getTitle(devices?: Array<IDevice>): string | undefined {
+ if (devices) {
+ if (devices.length === 5) {
+ // TRANSLATORS: Page title informing user that the login failed due to too many registered
+ // TRANSLATORS: devices on account.
+ return messages.pgettext('device-management', 'Too many devices');
+ } else {
+ // TRANSLATORS: Page title informing user that enough devices has been removed to continue
+ // TRANSLATORS: login process.
+ return messages.pgettext('device-management', 'Super!');
+ }
+ } else {
+ return undefined;
+ }
+}
+
+function getSubtitle(devices?: Array<IDevice>): string | undefined {
+ if (devices) {
+ if (devices.length === 5) {
+ return messages.pgettext(
+ 'device-management',
+ 'You have too many active devices. Please log out of at least one by removing it from the list below. You can find the corresponding nickname under the device’s Account settings.',
+ );
+ } else {
+ return messages.pgettext(
+ 'device-management',
+ 'You can now continue logging in on this device.',
+ );
+ }
+ } else {
+ return undefined;
+ }
+}
diff --git a/gui/src/renderer/components/WireguardKeys.tsx b/gui/src/renderer/components/WireguardKeys.tsx
deleted file mode 100644
index 1a4dab38a6..0000000000
--- a/gui/src/renderer/components/WireguardKeys.tsx
+++ /dev/null
@@ -1,345 +0,0 @@
-import * as React from 'react';
-import { sprintf } from 'sprintf-js';
-import { TunnelState } from '../../shared/daemon-rpc-types';
-import { formatRelativeDate } from '../../shared/date-helper';
-import { messages } from '../../shared/gettext';
-import log from '../../shared/logging';
-import { IWgKey, WgKeyState } from '../redux/settings/reducers';
-import * as AppButton from './AppButton';
-import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
-import ClipboardLabel from './ClipboardLabel';
-import ImageView from './ImageView';
-import { BackAction } from './KeyboardNavigation';
-import { Layout } from './Layout';
-import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar';
-import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-import {
- StyledButtonRow,
- StyledContainer,
- StyledContent,
- StyledLastButtonRow,
- StyledMessage,
- StyledMessages,
- StyledNavigationScrollbars,
- StyledRow,
- StyledRowLabel,
- StyledRowLabelSpacer,
- StyledRowValue,
-} from './WireguardKeysStyles';
-
-export interface IProps {
- keyState: WgKeyState;
- isOffline: boolean;
- tunnelState: TunnelState;
- windowFocused: boolean;
-
- onClose: () => void;
- onGenerateKey: () => void;
- onReplaceKey: (old: IWgKey) => void;
- onVerifyKey: (publicKey: IWgKey) => void;
- onVisitWebsiteKey: () => Promise<void>;
-}
-
-export interface IState {
- recentlyGeneratedKey: boolean;
- userHasInitiatedVerification: boolean;
- ageOfKeyString: string;
-}
-
-export default class WireguardKeys extends React.Component<IProps, IState> {
- public state = {
- recentlyGeneratedKey: false,
- userHasInitiatedVerification: false,
- ageOfKeyString: WireguardKeys.ageOfKeyString(this.props.keyState),
- };
-
- private keyAgeUpdateInterval?: number;
-
- public static getDerivedStateFromProps(props: IProps) {
- return {
- ageOfKeyString: WireguardKeys.ageOfKeyString(props.keyState),
- };
- }
-
- public componentDidMount() {
- this.verifyKey();
- this.keyAgeUpdateInterval = window.setInterval(this.setAgeOfKeyStringState, 60 * 1000);
- }
-
- public componentWillUnmount() {
- clearInterval(this.keyAgeUpdateInterval);
- }
-
- public componentDidUpdate(prevProps: IProps) {
- const prevKey =
- prevProps.keyState.type === 'key-set' ? prevProps.keyState.key.publicKey : undefined;
- const key =
- this.props.keyState.type === 'key-set' ? this.props.keyState.key.publicKey : undefined;
- if (this.props.tunnelState.state === 'connected' && key !== undefined && key != prevKey) {
- this.setState({ recentlyGeneratedKey: true });
- }
-
- if (
- this.state.recentlyGeneratedKey &&
- prevProps.tunnelState.state !== 'connected' &&
- this.props.tunnelState.state === 'connected'
- ) {
- this.setState({ recentlyGeneratedKey: false });
- }
- }
-
- public render() {
- return (
- <BackAction action={this.props.onClose}>
- <Layout>
- <StyledContainer>
- <NavigationContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('wireguard-keys-nav', 'WireGuard key')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
-
- <StyledNavigationScrollbars fillContainer>
- <StyledContent>
- <SettingsHeader>
- <HeaderTitle>
- {messages.pgettext('wireguard-keys-nav', 'WireGuard key')}
- </HeaderTitle>
- </SettingsHeader>
-
- <StyledRow>
- <StyledRowLabel>
- <span>{messages.pgettext('wireguard-key-view', 'Public key')}</span>
- <StyledRowLabelSpacer />
- <span>{this.keyValidityLabel()}</span>
- </StyledRowLabel>
-
- <StyledRowValue>{this.getKeyText()}</StyledRowValue>
- </StyledRow>
- <StyledRow>
- <StyledRowLabel>
- {messages.pgettext('wireguard-key-view', 'Key generated')}
- </StyledRowLabel>
- <StyledRowValue>{this.state.ageOfKeyString}</StyledRowValue>
- </StyledRow>
-
- <StyledMessages>{this.getStatusMessage()}</StyledMessages>
-
- <StyledButtonRow>{this.getGenerateButton()}</StyledButtonRow>
- <StyledButtonRow>
- <AppButton.BlueButton
- disabled={this.isVerifyButtonDisabled()}
- onClick={this.handleVerifyKeyPress}>
- <AppButton.Label>
- {messages.pgettext('wireguard-key-view', 'Verify key')}
- </AppButton.Label>
- </AppButton.BlueButton>
- </StyledButtonRow>
- <StyledLastButtonRow>
- <AppButton.BlockingButton
- disabled={this.props.isOffline}
- onClick={this.props.onVisitWebsiteKey}>
- <AriaDescriptionGroup>
- <AriaDescribed>
- <AppButton.BlueButton>
- <AppButton.Label>
- {messages.pgettext('wireguard-key-view', 'Manage keys')}
- </AppButton.Label>
- <AriaDescription>
- <AppButton.Icon
- source="icon-extLink"
- height={16}
- width={16}
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </AppButton.BlueButton>
- </AriaDescribed>
- </AriaDescriptionGroup>
- </AppButton.BlockingButton>
- </StyledLastButtonRow>
- </StyledContent>
- </StyledNavigationScrollbars>
- </NavigationContainer>
- </StyledContainer>
- </Layout>
- </BackAction>
- );
- }
-
- private isVerifyButtonDisabled(): boolean {
- return this.props.keyState.type !== 'key-set';
- }
-
- private handleVerifyKeyPress = () => {
- this.setState({ userHasInitiatedVerification: true });
- this.verifyKey();
- };
-
- private verifyKey() {
- switch (this.props.keyState.type) {
- case 'key-set': {
- const key = this.props.keyState.key;
- this.props.onVerifyKey(key);
- break;
- }
- default:
- log.error(`onVerifyKey called from invalid state - ${this.props.keyState.type}`);
- }
- }
-
- /// Action button can either generate or verify a key
- private getGenerateButton() {
- let buttonText = messages.pgettext('wireguard-key-view', 'Generate key');
- const regenerateText = messages.pgettext('wireguard-key-view', 'Regenerate key');
-
- let disabled = false;
- let generateKey = this.props.onGenerateKey;
- switch (this.props.keyState.type) {
- case 'key-set': {
- buttonText = regenerateText;
- const key = this.props.keyState.key;
- generateKey = () => this.props.onReplaceKey(key);
- break;
- }
- case 'being-verified':
- disabled = true;
- buttonText = regenerateText;
- break;
- case 'being-replaced':
- case 'being-generated':
- disabled = true;
- buttonText = messages.pgettext('wireguard-key-view', 'Generating key');
- }
-
- return (
- <AppButton.GreenButton disabled={disabled} onClick={generateKey}>
- <AppButton.Label>{buttonText}</AppButton.Label>
- </AppButton.GreenButton>
- );
- }
-
- private getKeyText() {
- switch (this.props.keyState.type) {
- case 'being-verified':
- case 'key-set': {
- // mimicking the truncating of the key from website
- const publicKey = this.props.keyState.key.publicKey;
- return (
- <StyledRowValue title={this.props.keyState.key.publicKey}>
- <ClipboardLabel
- value={publicKey}
- displayValue={publicKey.substring(0, 20) + '...'}
- obscureValue={false}
- />
- </StyledRowValue>
- );
- }
- case 'being-replaced':
- case 'being-generated':
- return <ImageView source="icon-spinner" height={19} width={19} />;
- default:
- return (
- <StyledRowValue>{messages.pgettext('wireguard-key-view', 'No key set')}</StyledRowValue>
- );
- }
- }
-
- private keyValidityLabel() {
- const keyStateType = this.props.keyState.type;
- if (keyStateType === 'being-verified' && this.state.userHasInitiatedVerification) {
- return <ImageView source="icon-spinner" height={20} width={20} />;
- } else if (this.props.keyState.type === 'key-set') {
- const valid = this.props.keyState.key.valid;
- const show = this.state.userHasInitiatedVerification || valid === false;
- return show && valid !== undefined ? (
- <StyledMessage success={valid}>
- {valid
- ? messages.pgettext('wireguard-key-view', 'Key is valid')
- : messages.pgettext('wireguard-key-view', 'Key is invalid')}
- </StyledMessage>
- ) : null;
- } else {
- return null;
- }
- }
-
- private static ageOfKeyString(keyState: WgKeyState): string {
- switch (keyState.type) {
- case 'key-set':
- case 'being-verified': {
- const createdDate = Math.min(Date.parse(keyState.key.created), Date.now());
- return formatRelativeDate(new Date(), createdDate, true);
- }
- default:
- return '-';
- }
- }
-
- private setAgeOfKeyStringState = () => {
- this.setState({
- ageOfKeyString: WireguardKeys.ageOfKeyString(this.props.keyState),
- });
- };
-
- private getStatusMessage() {
- if (this.props.isOffline && this.state.recentlyGeneratedKey) {
- return (
- <StyledMessage success={this.state.recentlyGeneratedKey}>
- {messages.pgettext('wireguard-key-view', 'Reconnecting with new WireGuard key...')}
- </StyledMessage>
- );
- } else {
- let message = '';
- switch (this.props.keyState.type) {
- case 'key-set': {
- const key = this.props.keyState.key;
- if (key.replacementFailure) {
- switch (key.replacementFailure) {
- case 'too_many_keys':
- message = this.formatKeygenFailure('too-many-keys');
- break;
- case 'generation_failure':
- message = this.formatKeygenFailure('generation-failure');
- break;
- }
- } else if (key.verificationFailed) {
- message = messages.pgettext('wireguard-key-view', 'Key verification failed');
- }
-
- break;
- }
- case 'too-many-keys':
- case 'generation-failure':
- message = this.formatKeygenFailure(this.props.keyState.type);
- break;
- }
-
- return <StyledMessage success={false}>{message}</StyledMessage>;
- }
- }
-
- private formatKeygenFailure(failure: 'too-many-keys' | 'generation-failure'): string {
- switch (failure) {
- case 'too-many-keys':
- // TRANSLATORS: "%(manage)" is replaced with the text in the "Manage keys" button.
- return sprintf(
- messages.pgettext(
- 'wireguard-key-view',
- 'Unable to regenerate key: you already have the maximum number of keys. To generate a new key, you first need to revoke one under “Manage keys.”',
- ),
- { manage: messages.pgettext('wireguard-key-view', 'Manage keys') },
- );
- case 'generation-failure':
- return messages.pgettext('wireguard-key-view', 'Failed to generate a key');
- default:
- return failure;
- }
- }
-}
diff --git a/gui/src/renderer/components/WireguardKeysStyles.tsx b/gui/src/renderer/components/WireguardKeysStyles.tsx
deleted file mode 100644
index cf443b2fa5..0000000000
--- a/gui/src/renderer/components/WireguardKeysStyles.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import styled from 'styled-components';
-import { colors } from '../../config.json';
-import { normalText, smallText, tinyText } from './common-styles';
-import { Container } from './Layout';
-import { NavigationScrollbars } from './NavigationBar';
-
-export const StyledNavigationScrollbars = styled(NavigationScrollbars)({
- flex: 1,
-});
-
-export const StyledContainer = styled(Container)({
- backgroundColor: colors.darkBlue,
-});
-
-export const StyledContent = styled.div({
- display: 'flex',
- flexDirection: 'column',
- flex: 1,
-});
-
-export const StyledMessages = styled.div({
- padding: '0 22px 20px',
- flex: 1,
-});
-
-export const StyledMessage = styled.span(smallText, (props: { success: boolean }) => ({
- fontWeight: props.success ? 600 : 700,
- color: props.success ? colors.green : colors.red,
-}));
-
-export const StyledRow = styled.div({
- display: 'flex',
- flexDirection: 'column',
- padding: '0 22px',
- marginBottom: '20px',
-});
-
-export const StyledButtonRow = styled(StyledRow)({
- marginBottom: '18px',
-});
-
-export const StyledLastButtonRow = styled(StyledButtonRow)({
- marginBottom: '22px',
-});
-
-export const StyledRowLabel = styled.span(tinyText, {
- color: colors.white60,
- lineHeight: '20px',
- marginBottom: '5px',
-});
-
-export const StyledRowLabelSpacer = styled.div({
- flex: 1,
-});
-
-export const StyledRowValue = styled.span(normalText, {
- fontWeight: 600,
- color: colors.white,
-});
diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx
index f799cff335..8bec012986 100644
--- a/gui/src/renderer/components/WireguardSettings.tsx
+++ b/gui/src/renderer/components/WireguardSettings.tsx
@@ -51,7 +51,6 @@ interface IProps {
setWireguardMultihop: (value: boolean) => void;
setWireguardPort: (port?: number) => void;
setWireguardIpVersion: (ipVersion?: IpVersion) => void;
- onViewWireguardKeys: () => void;
onClose: () => void;
}
@@ -202,15 +201,6 @@ export default class WireguardSettings extends React.Component<IProps, IState> {
</Cell.Footer>
</AriaInputGroup>
- <Cell.CellButtonGroup>
- <Cell.CellButton onClick={this.props.onViewWireguardKeys}>
- <Cell.Label>
- {messages.pgettext('wireguard-settings-view', 'WireGuard key')}
- </Cell.Label>
- <Cell.Icon height={12} width={7} source="icon-chevron" />
- </Cell.CellButton>
- </Cell.CellButtonGroup>
-
<AriaInputGroup>
<Cell.Container>
<AriaLabel>
diff --git a/gui/src/renderer/containers/AccountPage.tsx b/gui/src/renderer/containers/AccountPage.tsx
index 641374c504..2309c614fb 100644
--- a/gui/src/renderer/containers/AccountPage.tsx
+++ b/gui/src/renderer/containers/AccountPage.tsx
@@ -1,19 +1,26 @@
import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
import { links } from '../../config.json';
import Account from '../components/Account';
import withAppContext, { IAppContext } from '../context';
import { IHistoryProps, withHistory } from '../lib/history';
import { IReduxState, ReduxDispatch } from '../redux/store';
+import accountActions from '../redux/account/actions';
const mapStateToProps = (state: IReduxState) => ({
+ deviceName: state.account.deviceName,
accountToken: state.account.accountToken,
accountExpiry: state.account.expiry,
expiryLocale: state.userInterface.locale,
isOffline: state.connection.isBlocked,
});
-const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
+const mapDispatchToProps = (dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
+ const account = bindActionCreators(accountActions, dispatch);
+
return {
+ prepareLogout: () => account.prepareLogout(),
+ cancelLogout: () => account.cancelLogout(),
onLogout: () => {
void props.app.logout();
},
@@ -22,6 +29,7 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp
},
onBuyMore: () => props.app.openLinkWithAuth(links.purchase),
updateAccountData: () => props.app.updateAccountData(),
+ getDevice: () => props.app.getDevice(),
};
};
diff --git a/gui/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/src/renderer/containers/AdvancedSettingsPage.tsx
index 51ba310aad..0bc00ee204 100644
--- a/gui/src/renderer/containers/AdvancedSettingsPage.tsx
+++ b/gui/src/renderer/containers/AdvancedSettingsPage.tsx
@@ -16,7 +16,6 @@ const mapStateToProps = (state: IReduxState) => {
return {
enableIpv6: state.settings.enableIpv6,
blockWhenDisconnected: state.settings.blockWhenDisconnected,
- wireguardKeyState: state.settings.wireguardKeyState,
tunnelProtocol,
};
};
diff --git a/gui/src/renderer/containers/WireguardKeysPage.tsx b/gui/src/renderer/containers/WireguardKeysPage.tsx
deleted file mode 100644
index b0c25e06a9..0000000000
--- a/gui/src/renderer/containers/WireguardKeysPage.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { connect } from 'react-redux';
-import { links } from '../../config.json';
-import WireguardKeys from '../components/WireguardKeys';
-import withAppContext, { IAppContext } from '../context';
-import { IHistoryProps, withHistory } from '../lib/history';
-import { IWgKey } from '../redux/settings/reducers';
-import { IReduxState, ReduxDispatch } from '../redux/store';
-
-const mapStateToProps = (state: IReduxState) => ({
- keyState: state.settings.wireguardKeyState,
- isOffline: state.connection.isBlocked,
- tunnelState: state.connection.status,
- windowFocused: state.userInterface.windowFocused,
-});
-const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
- return {
- onClose: () => props.history.pop(),
- onGenerateKey: () => props.app.generateWireguardKey(),
- onReplaceKey: (oldKey: IWgKey) => props.app.replaceWireguardKey(oldKey),
- onVerifyKey: (publicKey: IWgKey) => props.app.verifyWireguardKey(publicKey),
- onVisitWebsiteKey: () => props.app.openLinkWithAuth(links.manageKeys),
- };
-};
-
-export default withAppContext(
- withHistory(connect(mapStateToProps, mapDispatchToProps)(WireguardKeys)),
-);
diff --git a/gui/src/renderer/containers/WireguardSettingsPage.tsx b/gui/src/renderer/containers/WireguardSettingsPage.tsx
index 4b0af37510..d9ac31756d 100644
--- a/gui/src/renderer/containers/WireguardSettingsPage.tsx
+++ b/gui/src/renderer/containers/WireguardSettingsPage.tsx
@@ -6,7 +6,6 @@ import WireguardSettings from '../components/WireguardSettings';
import withAppContext, { IAppContext } from '../context';
import { createWireguardRelayUpdater } from '../lib/constraint-updater';
import { IHistoryProps, withHistory } from '../lib/history';
-import { RoutePath } from '../lib/routes';
import { RelaySettingsRedux } from '../redux/settings/reducers';
import { IReduxState, ReduxDispatch } from '../redux/store';
@@ -108,8 +107,6 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp
log.error('Failed to update mtu value', error.message);
}
},
-
- onViewWireguardKeys: () => props.history.push(RoutePath.wireguardKeys),
};
};
diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts
index 8a63cd6de0..994a9f6124 100644
--- a/gui/src/renderer/lib/routes.ts
+++ b/gui/src/renderer/lib/routes.ts
@@ -5,6 +5,8 @@ export type GeneratedRoutePath = { routePath: string };
export enum RoutePath {
launch = '/',
login = '/login',
+ tooManyDevices = '/login/too-many-devices',
+ deviceRevoked = '/login/device-revoked',
main = '/main',
redeemVoucher = '/main/voucher/redeem',
voucherSuccess = '/main/voucher/success/:newExpiry/:secondsAdded',
@@ -16,7 +18,6 @@ export enum RoutePath {
preferences = '/settings/preferences',
advancedSettings = '/settings/advanced',
wireguardSettings = '/settings/advanced/wireguard',
- wireguardKeys = '/settings/advanced/wireguard/keys',
openVpnSettings = '/settings/advanced/openvpn',
splitTunneling = '/settings/advanced/split-tunneling',
support = '/settings/support',
@@ -24,6 +25,18 @@ export enum RoutePath {
filterByProvider = '/select-location/filter-by-provider',
}
+export const disableDismissForRoutes = [
+ RoutePath.launch,
+ RoutePath.login,
+ RoutePath.tooManyDevices,
+ RoutePath.deviceRevoked,
+ RoutePath.main,
+ RoutePath.redeemVoucher,
+ RoutePath.voucherSuccess,
+ RoutePath.timeAdded,
+ RoutePath.setupFinished,
+];
+
export function generateRoutePath(
routePath: RoutePath,
parameters: Parameters<typeof generatePath>[1],
diff --git a/gui/src/renderer/redux/account/actions.ts b/gui/src/renderer/redux/account/actions.ts
index b8fbe94d39..aa0a949533 100644
--- a/gui/src/renderer/redux/account/actions.ts
+++ b/gui/src/renderer/redux/account/actions.ts
@@ -1,4 +1,4 @@
-import { AccountToken } from '../../../shared/daemon-rpc-types';
+import { AccountToken, IDeviceConfig, IDevice } from '../../../shared/daemon-rpc-types';
interface IStartLoginAction {
type: 'START_LOGIN';
@@ -7,6 +7,8 @@ interface IStartLoginAction {
interface ILoggedInAction {
type: 'LOGGED_IN';
+ accountToken: AccountToken;
+ deviceName?: string;
}
interface ILoginFailedAction {
@@ -14,6 +16,19 @@ interface ILoginFailedAction {
error: Error;
}
+interface ILoginTooManyDevicesAction {
+ type: 'TOO_MANY_DEVICES';
+ error: Error;
+}
+
+interface IPrepareLogoutAction {
+ type: 'PREPARE_LOG_OUT';
+}
+
+interface ICancelLogoutAction {
+ type: 'CANCEL_LOGOUT';
+}
+
interface ILoggedOutAction {
type: 'LOGGED_OUT';
}
@@ -22,6 +37,10 @@ interface IResetLoginErrorAction {
type: 'RESET_LOGIN_ERROR';
}
+interface IDeviceRevokedAction {
+ type: 'DEVICE_REVOKED';
+}
+
interface IStartCreateAccount {
type: 'START_CREATE_ACCOUNT';
}
@@ -33,13 +52,18 @@ interface ICreateAccountFailed {
interface IAccountCreated {
type: 'ACCOUNT_CREATED';
- token: AccountToken;
+ accountToken: AccountToken;
+ deviceName?: string;
expiry: string;
}
+interface IAccountSetupFinished {
+ type: 'ACCOUNT_SETUP_FINISHED';
+}
+
interface IUpdateAccountTokenAction {
type: 'UPDATE_ACCOUNT_TOKEN';
- token: AccountToken;
+ accountToken: AccountToken;
}
interface IUpdateAccountHistoryAction {
@@ -52,18 +76,29 @@ interface IUpdateAccountExpiryAction {
expiry?: string;
}
+interface IUpdateDevicesAction {
+ type: 'UPDATE_DEVICES';
+ devices: Array<IDevice>;
+}
+
export type AccountAction =
| IStartLoginAction
| ILoggedInAction
| ILoginFailedAction
+ | ILoginTooManyDevicesAction
+ | IPrepareLogoutAction
+ | ICancelLogoutAction
| ILoggedOutAction
| IResetLoginErrorAction
+ | IDeviceRevokedAction
| IStartCreateAccount
| ICreateAccountFailed
| IAccountCreated
+ | IAccountSetupFinished
| IUpdateAccountTokenAction
| IUpdateAccountHistoryAction
- | IUpdateAccountExpiryAction;
+ | IUpdateAccountExpiryAction
+ | IUpdateDevicesAction;
function startLogin(accountToken: AccountToken): IStartLoginAction {
return {
@@ -72,9 +107,11 @@ function startLogin(accountToken: AccountToken): IStartLoginAction {
};
}
-function loggedIn(): ILoggedInAction {
+function loggedIn(deviceConfig: IDeviceConfig): ILoggedInAction {
return {
type: 'LOGGED_IN',
+ accountToken: deviceConfig.accountToken,
+ deviceName: deviceConfig.device?.name,
};
}
@@ -85,6 +122,25 @@ function loginFailed(error: Error): ILoginFailedAction {
};
}
+function loginTooManyDevices(error: Error): ILoginTooManyDevicesAction {
+ return {
+ type: 'TOO_MANY_DEVICES',
+ error,
+ };
+}
+
+function prepareLogout(): IPrepareLogoutAction {
+ return {
+ type: 'PREPARE_LOG_OUT',
+ };
+}
+
+function cancelLogout(): ICancelLogoutAction {
+ return {
+ type: 'CANCEL_LOGOUT',
+ };
+}
+
function loggedOut(): ILoggedOutAction {
return {
type: 'LOGGED_OUT',
@@ -97,6 +153,12 @@ function resetLoginError(): IResetLoginErrorAction {
};
}
+function deviceRevoked(): IDeviceRevokedAction {
+ return {
+ type: 'DEVICE_REVOKED',
+ };
+}
+
function startCreateAccount(): IStartCreateAccount {
return {
type: 'START_CREATE_ACCOUNT',
@@ -110,18 +172,23 @@ function createAccountFailed(error: Error): ICreateAccountFailed {
};
}
-function accountCreated(token: AccountToken, expiry: string): IAccountCreated {
+function accountCreated(deviceConfig: IDeviceConfig, expiry: string): IAccountCreated {
return {
type: 'ACCOUNT_CREATED',
- token,
+ accountToken: deviceConfig.accountToken,
+ deviceName: deviceConfig.device?.name,
expiry,
};
}
-function updateAccountToken(token: AccountToken): IUpdateAccountTokenAction {
+function accountSetupFinished(): IAccountSetupFinished {
+ return { type: 'ACCOUNT_SETUP_FINISHED' };
+}
+
+function updateAccountToken(accountToken: AccountToken): IUpdateAccountTokenAction {
return {
type: 'UPDATE_ACCOUNT_TOKEN',
- token,
+ accountToken,
};
}
@@ -139,16 +206,29 @@ function updateAccountExpiry(expiry?: string): IUpdateAccountExpiryAction {
};
}
+function updateDevices(devices: Array<IDevice>): IUpdateDevicesAction {
+ return {
+ type: 'UPDATE_DEVICES',
+ devices: devices.sort((a, b) => a.name.localeCompare(b.name)),
+ };
+}
+
export default {
startLogin,
loggedIn,
loginFailed,
+ loginTooManyDevices,
+ prepareLogout,
+ cancelLogout,
loggedOut,
resetLoginError,
+ deviceRevoked,
startCreateAccount,
createAccountFailed,
accountCreated,
+ accountSetupFinished,
updateAccountToken,
updateAccountHistory,
updateAccountExpiry,
+ updateDevices,
};
diff --git a/gui/src/renderer/redux/account/reducers.ts b/gui/src/renderer/redux/account/reducers.ts
index 53bc55db1b..00f2ef7bb5 100644
--- a/gui/src/renderer/redux/account/reducers.ts
+++ b/gui/src/renderer/redux/account/reducers.ts
@@ -1,23 +1,29 @@
-import { AccountToken } from '../../../shared/daemon-rpc-types';
+import { AccountToken, IDevice } from '../../../shared/daemon-rpc-types';
import { ReduxAction } from '../store';
type LoginMethod = 'existing_account' | 'new_account';
export type LoginState =
- | { type: 'none' }
+ | { type: 'none'; deviceRevoked: boolean }
| { type: 'logging in' | 'ok'; method: LoginMethod }
- | { type: 'failed'; method: LoginMethod; error: Error };
+ | { type: 'failed' | 'too many devices'; method: LoginMethod; error: Error };
export interface IAccountReduxState {
accountToken?: AccountToken;
+ deviceName?: string;
+ devices: Array<IDevice>;
accountHistory?: AccountToken;
expiry?: string; // ISO8601
status: LoginState;
+ loggingOut: boolean;
}
const initialState: IAccountReduxState = {
accountToken: undefined,
+ deviceName: undefined,
+ devices: [],
accountHistory: undefined,
expiry: undefined,
- status: { type: 'none' },
+ status: { type: 'none', deviceRevoked: false },
+ loggingOut: false,
};
export default function (
@@ -35,6 +41,8 @@ export default function (
return {
...state,
status: { type: 'ok', method: 'existing_account' },
+ accountToken: action.accountToken,
+ deviceName: action.deviceName,
};
case 'LOGIN_FAILED':
return {
@@ -42,17 +50,38 @@ export default function (
status: { type: 'failed', method: 'existing_account', error: action.error },
accountToken: undefined,
};
+ case 'TOO_MANY_DEVICES':
+ return {
+ ...state,
+ status: { type: 'too many devices', method: 'existing_account', error: action.error },
+ };
+ case 'PREPARE_LOG_OUT':
+ return {
+ ...state,
+ loggingOut: true,
+ };
+ case 'CANCEL_LOGOUT':
+ return {
+ ...state,
+ loggingOut: false,
+ };
case 'LOGGED_OUT':
return {
...state,
- status: { type: 'none' },
+ status: { type: 'none', deviceRevoked: false },
accountToken: undefined,
expiry: undefined,
+ loggingOut: false,
};
case 'RESET_LOGIN_ERROR':
return {
...state,
- status: { type: 'none' },
+ status: { type: 'none', deviceRevoked: false },
+ };
+ case 'DEVICE_REVOKED':
+ return {
+ ...state,
+ status: { type: 'none', deviceRevoked: true },
};
case 'START_CREATE_ACCOUNT':
return {
@@ -68,13 +97,19 @@ export default function (
return {
...state,
status: { type: 'ok', method: 'new_account' },
- accountToken: action.token,
+ accountToken: action.accountToken,
+ deviceName: action.deviceName,
expiry: action.expiry,
};
+ case 'ACCOUNT_SETUP_FINISHED':
+ return {
+ ...state,
+ status: { type: 'ok', method: 'existing_account' },
+ };
case 'UPDATE_ACCOUNT_TOKEN':
return {
...state,
- accountToken: action.token,
+ accountToken: action.accountToken,
};
case 'UPDATE_ACCOUNT_HISTORY':
return {
@@ -86,6 +121,11 @@ export default function (
...state,
expiry: action.expiry,
};
+ case 'UPDATE_DEVICES':
+ return {
+ ...state,
+ devices: action.devices,
+ };
}
return state;
diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts
index 1b1e48265c..32cd157157 100644
--- a/gui/src/renderer/redux/settings/actions.ts
+++ b/gui/src/renderer/redux/settings/actions.ts
@@ -1,12 +1,7 @@
-import {
- BridgeState,
- IDnsOptions,
- IWireguardPublicKey,
- KeygenEvent,
-} from '../../../shared/daemon-rpc-types';
+import { BridgeState, IDnsOptions } from '../../../shared/daemon-rpc-types';
import { IGuiSettingsState } from '../../../shared/gui-settings-state';
import { IApplication } from '../../../shared/application-types';
-import { BridgeSettingsRedux, IRelayLocationRedux, IWgKey, RelaySettingsRedux } from './reducers';
+import { BridgeSettingsRedux, IRelayLocationRedux, RelaySettingsRedux } from './reducers';
export interface IUpdateGuiSettingsAction {
type: 'UPDATE_GUI_SETTINGS';
@@ -73,36 +68,6 @@ export interface IUpdateAutoStartAction {
autoStart: boolean;
}
-// Used to set wireguard key when accounts are changed.
-export interface IWireguardSetKey {
- type: 'SET_WIREGUARD_KEY';
- key?: IWgKey;
-}
-
-export interface IWireguardGenerateKey {
- type: 'GENERATE_WIREGUARD_KEY';
-}
-
-export interface IWireguardReplaceKey {
- type: 'REPLACE_WIREGUARD_KEY';
- oldKey: IWgKey;
-}
-
-export interface IWireguardVerifyKey {
- type: 'VERIFY_WIREGUARD_KEY';
- key: IWgKey;
-}
-
-export interface IWireguardKeygenEvent {
- type: 'WIREGUARD_KEYGEN_EVENT';
- event: KeygenEvent;
-}
-
-export interface IWireguardKeyVerifiedAction {
- type: 'WIREGUARD_KEY_VERIFICATION_COMPLETE';
- verified?: boolean;
-}
-
export interface IUpdateDnsOptionsAction {
type: 'UPDATE_DNS_OPTIONS';
dns: IDnsOptions;
@@ -132,12 +97,6 @@ export type SettingsAction =
| IUpdateOpenVpnMssfixAction
| IUpdateWireguardMtuAction
| IUpdateAutoStartAction
- | IWireguardSetKey
- | IWireguardVerifyKey
- | IWireguardGenerateKey
- | IWireguardReplaceKey
- | IWireguardKeygenEvent
- | IWireguardKeyVerifiedAction
| IUpdateDnsOptionsAction
| IUpdateSplitTunnelingStateAction
| ISetSplitTunnelingApplicationsAction;
@@ -237,54 +196,6 @@ function updateAutoStart(autoStart: boolean): IUpdateAutoStartAction {
};
}
-function setWireguardKey(publicKey?: IWireguardPublicKey): IWireguardSetKey {
- const key = publicKey
- ? {
- publicKey: publicKey.key,
- created: publicKey.created,
- valid: undefined,
- }
- : undefined;
- return {
- type: 'SET_WIREGUARD_KEY',
- key,
- };
-}
-
-function setWireguardKeygenEvent(event: KeygenEvent): IWireguardKeygenEvent {
- return {
- type: 'WIREGUARD_KEYGEN_EVENT',
- event,
- };
-}
-
-function generateWireguardKey(): IWireguardGenerateKey {
- return {
- type: 'GENERATE_WIREGUARD_KEY',
- };
-}
-
-function replaceWireguardKey(oldKey: IWgKey): IWireguardReplaceKey {
- return {
- type: 'REPLACE_WIREGUARD_KEY',
- oldKey,
- };
-}
-
-function verifyWireguardKey(key: IWgKey): IWireguardVerifyKey {
- return {
- type: 'VERIFY_WIREGUARD_KEY',
- key,
- };
-}
-
-function completeWireguardKeyVerification(verified?: boolean): IWireguardKeyVerifiedAction {
- return {
- type: 'WIREGUARD_KEY_VERIFICATION_COMPLETE',
- verified,
- };
-}
-
function updateDnsOptions(dns: IDnsOptions): IUpdateDnsOptionsAction {
return {
type: 'UPDATE_DNS_OPTIONS',
@@ -322,12 +233,6 @@ export default {
updateOpenVpnMssfix,
updateWireguardMtu,
updateAutoStart,
- setWireguardKey,
- setWireguardKeygenEvent,
- generateWireguardKey,
- replaceWireguardKey,
- verifyWireguardKey,
- completeWireguardKeyVerification,
updateDnsOptions,
updateSplitTunnelingState,
setSplitTunnelingApplications,
diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts
index 449ff2b563..9b55160a5e 100644
--- a/gui/src/renderer/redux/settings/reducers.ts
+++ b/gui/src/renderer/redux/settings/reducers.ts
@@ -1,7 +1,6 @@
import { IApplication } from '../../../shared/application-types';
import {
BridgeState,
- KeygenEvent,
LiftedConstraint,
ProxySettings,
RelayLocation,
@@ -11,7 +10,6 @@ import {
IpVersion,
} from '../../../shared/daemon-rpc-types';
import { IGuiSettingsState } from '../../../shared/gui-settings-state';
-import log from '../../../shared/logging';
import { ReduxAction } from '../store';
export type RelaySettingsRedux =
@@ -73,54 +71,6 @@ export interface IRelayLocationRedux {
cities: IRelayLocationCityRedux[];
}
-export interface IWgKey {
- publicKey: string;
- created: string;
- valid?: boolean;
- replacementFailure?: KeygenEvent;
- verificationFailed?: boolean;
-}
-
-interface IWgKeySet {
- type: 'key-set';
- key: IWgKey;
-}
-
-interface IWgKeyNotSet {
- type: 'key-not-set';
-}
-
-interface IWgTooManyKeys {
- type: 'too-many-keys';
-}
-
-interface IWgKeyGenerationFailure {
- type: 'generation-failure';
-}
-
-interface IWgKeyBeingGenerated {
- type: 'being-generated';
-}
-
-interface IWgKeyBeingReplaced {
- type: 'being-replaced';
- oldKey: IWgKey;
-}
-
-interface IWgKeyBeingVerified {
- type: 'being-verified';
- key: IWgKey;
-}
-
-export type WgKeyState =
- | IWgKeySet
- | IWgKeyNotSet
- | IWgKeyGenerationFailure
- | IWgTooManyKeys
- | IWgKeyBeingVerified
- | IWgKeyBeingReplaced
- | IWgKeyBeingGenerated;
-
export interface ISettingsReduxState {
autoStart: boolean;
guiSettings: IGuiSettingsState;
@@ -140,7 +90,6 @@ export interface ISettingsReduxState {
mtu?: number;
};
dns: IDnsOptions;
- wireguardKeyState: WgKeyState;
splitTunneling: boolean;
splitTunnelingApplications: IApplication[];
}
@@ -183,9 +132,6 @@ const initialState: ISettingsReduxState = {
showBetaReleases: false,
openVpn: {},
wireguard: {},
- wireguardKeyState: {
- type: 'key-not-set',
- },
dns: {
state: 'default',
defaultOptions: {
@@ -290,42 +236,6 @@ export default function (
bridgeState: action.bridgeState,
};
- case 'SET_WIREGUARD_KEY':
- return {
- ...state,
- wireguardKeyState: setWireguardKey(action.key),
- };
- case 'WIREGUARD_KEYGEN_EVENT':
- return {
- ...state,
- wireguardKeyState: setWireguardKeygenEvent(state, action.event),
- };
- case 'WIREGUARD_KEY_VERIFICATION_COMPLETE':
- return {
- ...state,
- wireguardKeyState: applyKeyVerification(state.wireguardKeyState, action.verified),
- };
- case 'VERIFY_WIREGUARD_KEY':
- return {
- ...state,
- wireguardKeyState: { type: 'being-verified', key: resetWireguardKeyErrors(action.key) },
- };
-
- case 'GENERATE_WIREGUARD_KEY':
- return {
- ...state,
- wireguardKeyState: { type: 'being-generated' },
- };
-
- case 'REPLACE_WIREGUARD_KEY':
- return {
- ...state,
- wireguardKeyState: {
- type: 'being-replaced',
- oldKey: resetWireguardKeyErrors(action.oldKey),
- },
- };
-
case 'UPDATE_DNS_OPTIONS':
return {
...state,
@@ -348,76 +258,3 @@ export default function (
return state;
}
}
-
-function setWireguardKey(key?: IWgKey): WgKeyState {
- if (key) {
- return {
- type: 'key-set',
- key,
- };
- } else {
- return {
- type: 'key-not-set',
- };
- }
-}
-
-function resetWireguardKeyErrors(key: IWgKey): IWgKey {
- return {
- publicKey: key.publicKey,
- created: key.created,
- };
-}
-
-function setWireguardKeygenEvent(state: ISettingsReduxState, keygenEvent: KeygenEvent): WgKeyState {
- const oldKeyState = state.wireguardKeyState;
- if (oldKeyState.type === 'being-replaced') {
- switch (keygenEvent) {
- case 'too_many_keys':
- case 'generation_failure':
- return {
- type: 'key-set',
- key: {
- ...oldKeyState.oldKey,
- replacementFailure: keygenEvent,
- },
- };
- default:
- break;
- }
- }
- switch (keygenEvent) {
- case 'too_many_keys':
- return { type: 'too-many-keys' };
- case 'generation_failure':
- return { type: 'generation-failure' };
- default:
- return {
- type: 'key-set',
- key: {
- publicKey: keygenEvent.newKey.key,
- created: keygenEvent.newKey.created,
- valid: undefined,
- },
- };
- }
-}
-
-function applyKeyVerification(state: WgKeyState, verified?: boolean): WgKeyState {
- const verificationFailed = verified === undefined ? true : undefined;
- switch (state.type) {
- case 'being-verified':
- return {
- type: 'key-set',
- key: {
- ...state.key,
- valid: verified,
- verificationFailed,
- },
- };
- // drop the verification event if the key wasn't being verified.
- default:
- log.error("Received key verification event when key wasn't being verified");
- return state;
- }
-}