summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-02-25 13:56:23 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-04-03 13:36:43 +0200
commit83d212b8cca484bd4b694e45e132341ca0c26768 (patch)
tree852db24d05f71d440983a17b2e72c6ac47726973 /gui/src/renderer
parentba31bee6e84bea28ee0e01cf4da414b15826f985 (diff)
downloadmullvadvpn-83d212b8cca484bd4b694e45e132341ca0c26768.tar.xz
mullvadvpn-83d212b8cca484bd4b694e45e132341ca0c26768.zip
Create new account when pressing create account button
Diffstat (limited to 'gui/src/renderer')
-rw-r--r--gui/src/renderer/app.tsx68
-rw-r--r--gui/src/renderer/components/Login.tsx77
-rw-r--r--gui/src/renderer/components/Settings.tsx4
-rw-r--r--gui/src/renderer/containers/LoginPage.tsx4
-rw-r--r--gui/src/renderer/redux/account/actions.ts42
-rw-r--r--gui/src/renderer/redux/account/reducers.ts48
6 files changed, 163 insertions, 80 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 0758f6496a..f47e4c58bf 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -190,6 +190,7 @@ export default class AppRenderer {
this.setLocale(initialState.locale);
this.setAccountExpiry(initialState.accountData && initialState.accountData.expiry);
+ this.handleAccountChange(undefined, initialState.settings.accountToken);
this.setAccountHistory(initialState.accountHistory);
this.setSettings(initialState.settings);
this.setTunnelState(initialState.tunnelState);
@@ -238,18 +239,9 @@ export default class AppRenderer {
try {
await IpcRendererEventChannel.account.login(accountToken);
-
- // Redirect the user after some time to allow for the 'Logged in' screen to be visible
- this.loginTimer = global.setTimeout(async () => {
- this.memoryHistory.replace('/connect');
-
- try {
- log.info('Auto-connecting the tunnel');
- await this.connectTunnel();
- } catch (error) {
- log.error(`Failed to auto-connect the tunnel: ${error.message}`);
- }
- }, 1000);
+ actions.account.updateAccountToken(accountToken);
+ actions.account.loggedIn();
+ this.redirectToConnect(true);
} catch (error) {
actions.account.loginFailed(error);
}
@@ -263,6 +255,23 @@ export default class AppRenderer {
}
}
+ public async createNewAccount() {
+ log.info('Creating account');
+
+ const actions = this.reduxActions;
+ actions.account.startCreateAccount();
+ this.doingLogin = true;
+
+ try {
+ const accountToken = await IpcRendererEventChannel.account.create();
+ const accountExpiry = new Date().toISOString();
+ actions.account.accountCreated(accountToken, accountExpiry);
+ this.redirectToConnect(false);
+ } catch (error) {
+ actions.account.createAccountFailed(error);
+ }
+ }
+
public async connectTunnel(): Promise<void> {
const state = this.tunnelState.state;
@@ -417,6 +426,22 @@ export default class AppRenderer {
return preferredLocale ? preferredLocale.name : '';
}
+ private redirectToConnect(connect: boolean) {
+ // Redirect the user after some time to allow for the 'Logged in' screen to be visible
+ this.loginTimer = global.setTimeout(async () => {
+ this.memoryHistory.replace('/connect');
+
+ if (connect) {
+ try {
+ log.info('Auto-connecting the tunnel');
+ await this.connectTunnel();
+ } catch (error) {
+ log.error(`Failed to auto-connect the tunnel: ${error.message}`);
+ }
+ }
+ }, 1000);
+ }
+
private loadTranslations(locale: string) {
for (const catalogue of [messages, relayLocations]) {
loadTranslations(locale, catalogue);
@@ -578,7 +603,6 @@ export default class AppRenderer {
this.settings = newSettings;
const reduxSettings = this.reduxActions.settings;
- const reduxAccount = this.reduxActions.account;
reduxSettings.updateAllowLan(newSettings.allowLan);
reduxSettings.updateEnableIpv6(newSettings.tunnelOptions.generic.enableIpv6);
@@ -590,13 +614,6 @@ export default class AppRenderer {
this.setRelaySettings(newSettings.relaySettings);
this.setBridgeSettings(newSettings.bridgeSettings);
-
- if (newSettings.accountToken) {
- reduxAccount.updateAccountToken(newSettings.accountToken);
- reduxAccount.loggedIn();
- } else {
- reduxAccount.loggedOut();
- }
}
private updateBlockedState(tunnelState: TunnelState, blockWhenDisconnected: boolean) {
@@ -625,13 +642,20 @@ export default class AppRenderer {
}
private handleAccountChange(oldAccount?: string, newAccount?: string) {
+ const reduxAccount = this.reduxActions.account;
+
if (oldAccount && !newAccount) {
if (this.loginTimer) {
clearTimeout(this.loginTimer);
}
+ reduxAccount.loggedOut();
this.memoryHistory.replace('/login');
- } else if (!oldAccount && newAccount && !this.doingLogin) {
- this.memoryHistory.replace('/connect');
+ } else if (newAccount && oldAccount !== newAccount && !this.doingLogin) {
+ reduxAccount.updateAccountToken(newAccount);
+ reduxAccount.loggedIn();
+ if (!oldAccount) {
+ this.memoryHistory.replace('/connect');
+ }
}
this.doingLogin = false;
diff --git a/gui/src/renderer/components/Login.tsx b/gui/src/renderer/components/Login.tsx
index 82566996db..2991f7aa26 100644
--- a/gui/src/renderer/components/Login.tsx
+++ b/gui/src/renderer/components/Login.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Animated, Component, Styles, Text, TextInput, Types, UserInterface, View } from 'reactxp';
-import { colors, links } from '../../config.json';
+import { colors } from '../../config.json';
import consumePromise from '../../shared/promise';
import { messages } from '../../shared/gettext';
import { formatAccountToken } from '../lib/account';
@@ -18,7 +18,6 @@ import { LoginState } from '../redux/account/reducers';
interface IProps {
accountToken?: AccountToken;
accountHistory: AccountToken[];
- loginError?: Error;
loginState: LoginState;
openSettings?: () => void;
openExternalLink: (type: string) => void;
@@ -26,6 +25,7 @@ interface IProps {
resetLoginError: () => void;
updateAccountToken: (accountToken: AccountToken) => void;
removeAccountTokenFromHistory: (accountToken: AccountToken) => Promise<void>;
+ createNewAccount: () => void;
}
interface IState {
@@ -56,7 +56,7 @@ export default class Login extends Component<IProps, IState> {
constructor(props: IProps) {
super(props);
- if (props.loginState === 'failed') {
+ if (props.loginState.type === 'failed') {
this.shouldResetLoginError = true;
}
@@ -79,8 +79,8 @@ export default class Login extends Component<IProps, IState> {
public componentDidUpdate(prevProps: IProps, _prevState: IState) {
if (
- this.props.loginState !== prevProps.loginState &&
- this.props.loginState === 'failed' &&
+ this.props.loginState.type !== prevProps.loginState.type &&
+ this.props.loginState.type === 'failed' &&
!this.shouldResetLoginError
) {
this.shouldResetLoginError = true;
@@ -121,8 +121,6 @@ export default class Login extends Component<IProps, IState> {
);
}
- private onCreateAccount = () => this.props.openExternalLink(links.createAccount);
-
private onFocus = () => {
this.setState({ isActive: true });
};
@@ -212,7 +210,7 @@ export default class Login extends Component<IProps, IState> {
};
private formTitle() {
- switch (this.props.loginState) {
+ switch (this.props.loginState.type) {
case 'logging in':
return messages.pgettext('login-view', 'Logging in...');
case 'failed':
@@ -225,16 +223,19 @@ export default class Login extends Component<IProps, IState> {
}
private formSubtitle() {
- const { loginState, loginError } = this.props;
- switch (loginState) {
+ switch (this.props.loginState.type) {
case 'failed':
- return (
- (loginError && loginError.message) || messages.pgettext('login-view', 'Unknown error')
- );
+ 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 'logging in':
- return messages.pgettext('login-view', 'Checking account number');
+ return this.props.loginState.method === 'existing_account'
+ ? messages.pgettext('login-view', 'Checking account number')
+ : messages.pgettext('login-view', 'Creating new account');
case 'ok':
- return messages.pgettext('login-view', 'Correct account number');
+ return this.props.loginState.method === 'existing_account'
+ ? messages.pgettext('login-view', 'Correct account number')
+ : messages.pgettext('login-view', 'Account created');
default:
return messages.pgettext('login-view', 'Enter your account number');
}
@@ -250,7 +251,7 @@ export default class Login extends Component<IProps, IState> {
}
private getStatusIconPath(): string | undefined {
- switch (this.props.loginState) {
+ switch (this.props.loginState.type) {
case 'logging in':
return 'icon-spinner';
case 'failed':
@@ -268,14 +269,13 @@ export default class Login extends Component<IProps, IState> {
classes.push(styles.account_input_group__active);
}
- switch (this.props.loginState) {
- case 'logging in':
- case 'ok':
- classes.push(styles.account_input_group__inactive);
- break;
- case 'failed':
- classes.push(styles.account_input_group__error);
- break;
+ if (!this.allowInteraction()) {
+ classes.push(styles.account_input_group__inactive);
+ } else if (
+ this.props.loginState.type === 'failed' &&
+ this.props.loginState.method === 'existing_account'
+ ) {
+ classes.push(styles.account_input_group__error);
}
return classes;
@@ -286,7 +286,7 @@ export default class Login extends Component<IProps, IState> {
Types.StyleRuleSet<Types.AnimatedViewStyle> | Types.StyleRuleSet<Types.ViewStyle>
> = [styles.input_button];
- if (this.props.loginState === 'logging in' || this.props.loginState === 'ok') {
+ if (!this.allowInteraction()) {
classes.push(styles.input_button__invisible);
}
@@ -296,16 +296,19 @@ export default class Login extends Component<IProps, IState> {
}
private accountInputArrowStyles(): Types.ViewStyleRuleSet[] {
- const { loginState } = this.props;
const classes = [styles.input_arrow];
- if (loginState === 'logging in') {
+ if (this.props.loginState.type === 'logging in') {
classes.push(styles.input_arrow__invisible);
}
return classes;
}
+ private allowInteraction() {
+ return this.props.loginState.type !== 'logging in' && this.props.loginState.type !== 'ok';
+ }
+
private shouldActivateLoginButton(): boolean {
const { accountToken } = this.props;
if (accountToken && accountToken.length >= MIN_ACCOUNT_TOKEN_LENGTH) {
@@ -314,20 +317,13 @@ export default class Login extends Component<IProps, IState> {
return false;
}
- private shouldEnableAccountInput() {
- // enable account input always except when "logging in" or "logged in"
- return this.props.loginState !== 'logging in' && this.props.loginState !== 'ok';
- }
-
private shouldShowAccountHistory() {
- return (
- this.shouldEnableAccountInput() && this.state.isActive && this.props.accountHistory.length > 0
- );
+ return this.allowInteraction() && this.state.isActive && this.props.accountHistory.length > 0;
}
private shouldShowFooter() {
return (
- (this.props.loginState === 'none' || this.props.loginState === 'failed') &&
+ (this.props.loginState.type === 'none' || this.props.loginState.type === 'failed') &&
!this.shouldShowAccountHistory()
);
}
@@ -363,7 +359,7 @@ export default class Login extends Component<IProps, IState> {
placeholderTextColor={colors.blue40}
value={this.props.accountToken || ''}
autoCorrect={false}
- editable={this.shouldEnableAccountInput()}
+ editable={this.allowInteraction()}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChangeText={this.onInputChange}
@@ -403,9 +399,10 @@ export default class Login extends Component<IProps, IState> {
<Text style={styles.login_footer__prompt}>
{messages.pgettext('login-view', "Don't have an account number?")}
</Text>
- <AppButton.BlueButton onPress={this.onCreateAccount}>
- <AppButton.Label>{messages.pgettext('login-view', 'Create account')}</AppButton.Label>
- <AppButton.Icon source="icon-extLink" height={16} width={16} />
+ <AppButton.BlueButton
+ onPress={this.props.createNewAccount}
+ disabled={!this.allowInteraction()}>
+ {messages.pgettext('login-view', 'Create account')}
</AppButton.BlueButton>
</View>
);
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index ee5cc8c340..f1e1893646 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -40,7 +40,7 @@ export interface IProps {
export default class Settings extends Component<IProps> {
public render() {
- const showLargeTitle = this.props.loginState !== 'ok';
+ const showLargeTitle = this.props.loginState.type !== 'ok';
return (
<Layout>
@@ -95,7 +95,7 @@ export default class Settings extends Component<IProps> {
}
private renderTopButtons() {
- const isLoggedIn = this.props.loginState === 'ok';
+ const isLoggedIn = this.props.loginState.type === 'ok';
if (!isLoggedIn) {
return null;
}
diff --git a/gui/src/renderer/containers/LoginPage.tsx b/gui/src/renderer/containers/LoginPage.tsx
index 2f221ad875..2cadcca426 100644
--- a/gui/src/renderer/containers/LoginPage.tsx
+++ b/gui/src/renderer/containers/LoginPage.tsx
@@ -9,11 +9,10 @@ import accountActions from '../redux/account/actions';
import { IReduxState, ReduxDispatch } from '../redux/store';
const mapStateToProps = (state: IReduxState) => {
- const { accountToken, accountHistory, error, status } = state.account;
+ const { accountToken, accountHistory, status } = state.account;
return {
accountToken,
accountHistory,
- loginError: error,
loginState: status,
};
};
@@ -33,6 +32,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext) => {
openExternalLink: (url: string) => shell.openExternal(url),
updateAccountToken,
removeAccountTokenFromHistory: (token: string) => props.app.removeAccountFromHistory(token),
+ createNewAccount: () => consumePromise(props.app.createNewAccount()),
};
};
diff --git a/gui/src/renderer/redux/account/actions.ts b/gui/src/renderer/redux/account/actions.ts
index e7f6ade346..3c0bad3c7b 100644
--- a/gui/src/renderer/redux/account/actions.ts
+++ b/gui/src/renderer/redux/account/actions.ts
@@ -22,6 +22,21 @@ interface IResetLoginErrorAction {
type: 'RESET_LOGIN_ERROR';
}
+interface IStartCreateAccount {
+ type: 'START_CREATE_ACCOUNT';
+}
+
+interface ICreateAccountFailed {
+ type: 'CREATE_ACCOUNT_FAILED';
+ error: Error;
+}
+
+interface IAccountCreated {
+ type: 'ACCOUNT_CREATED';
+ token: AccountToken;
+ expiry: string;
+}
+
interface IUpdateAccountTokenAction {
type: 'UPDATE_ACCOUNT_TOKEN';
token: AccountToken;
@@ -43,6 +58,9 @@ export type AccountAction =
| ILoginFailedAction
| ILoggedOutAction
| IResetLoginErrorAction
+ | IStartCreateAccount
+ | ICreateAccountFailed
+ | IAccountCreated
| IUpdateAccountTokenAction
| IUpdateAccountHistoryAction
| IUpdateAccountExpiryAction;
@@ -79,6 +97,27 @@ function resetLoginError(): IResetLoginErrorAction {
};
}
+function startCreateAccount(): IStartCreateAccount {
+ return {
+ type: 'START_CREATE_ACCOUNT',
+ };
+}
+
+function createAccountFailed(error: Error): ICreateAccountFailed {
+ return {
+ type: 'CREATE_ACCOUNT_FAILED',
+ error,
+ };
+}
+
+function accountCreated(token: AccountToken, expiry: string): IAccountCreated {
+ return {
+ type: 'ACCOUNT_CREATED',
+ token,
+ expiry,
+ };
+}
+
function updateAccountToken(token: AccountToken): IUpdateAccountTokenAction {
return {
type: 'UPDATE_ACCOUNT_TOKEN',
@@ -106,6 +145,9 @@ export default {
loginFailed,
loggedOut,
resetLoginError,
+ startCreateAccount,
+ createAccountFailed,
+ accountCreated,
updateAccountToken,
updateAccountHistory,
updateAccountExpiry,
diff --git a/gui/src/renderer/redux/account/reducers.ts b/gui/src/renderer/redux/account/reducers.ts
index 754ea09d13..f429192de5 100644
--- a/gui/src/renderer/redux/account/reducers.ts
+++ b/gui/src/renderer/redux/account/reducers.ts
@@ -1,21 +1,23 @@
import { AccountToken } from '../../../shared/daemon-rpc-types';
import { ReduxAction } from '../store';
-export type LoginState = 'none' | 'logging in' | 'failed' | 'ok';
+type LoginMethod = 'existing_account' | 'new_account';
+export type LoginState =
+ | { type: 'none' }
+ | { type: 'logging in' | 'ok'; method: LoginMethod }
+ | { type: 'failed'; method: LoginMethod; error: Error };
export interface IAccountReduxState {
accountToken?: AccountToken;
accountHistory: AccountToken[];
expiry?: string; // ISO8601
status: LoginState;
- error?: Error;
}
const initialState: IAccountReduxState = {
accountToken: undefined,
accountHistory: [],
expiry: undefined,
- status: 'none',
- error: undefined,
+ status: { type: 'none' },
};
export default function(
@@ -27,44 +29,62 @@ export default function(
return {
...state,
...{
- status: 'logging in',
+ status: { type: 'logging in', method: 'existing_account' },
accountToken: action.accountToken,
- error: undefined,
},
};
case 'LOGGED_IN':
return {
...state,
...{
- status: 'ok',
- error: undefined,
+ status: { type: 'ok', method: 'existing_account' },
},
};
case 'LOGIN_FAILED':
return {
...state,
...{
- status: 'failed',
+ status: { type: 'failed', method: 'existing_account', error: action.error },
accountToken: undefined,
- error: action.error,
},
};
case 'LOGGED_OUT':
return {
...state,
...{
- status: 'none',
+ status: { type: 'none' },
accountToken: undefined,
expiry: undefined,
- error: undefined,
},
};
case 'RESET_LOGIN_ERROR':
return {
...state,
...{
- status: 'none',
- error: undefined,
+ status: { type: 'none' },
+ },
+ };
+ case 'START_CREATE_ACCOUNT':
+ return {
+ ...state,
+ ...{
+ status: { type: 'logging in', method: 'new_account' },
+ },
+ };
+ case 'CREATE_ACCOUNT_FAILED':
+ return {
+ ...state,
+ ...{
+ status: { type: 'failed', method: 'new_account', error: action.error },
+ },
+ };
+ case 'ACCOUNT_CREATED':
+ return {
+ ...state,
+ ...{
+ status: { type: 'ok', method: 'new_account' },
+ accountToken: action.token,
+ expiry: action.expiry,
},
};
case 'UPDATE_ACCOUNT_TOKEN':