diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-02-25 13:56:23 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-04-03 13:36:43 +0200 |
| commit | 83d212b8cca484bd4b694e45e132341ca0c26768 (patch) | |
| tree | 852db24d05f71d440983a17b2e72c6ac47726973 | |
| parent | ba31bee6e84bea28ee0e01cf4da414b15826f985 (diff) | |
| download | mullvadvpn-83d212b8cca484bd4b694e45e132341ca0c26768.tar.xz mullvadvpn-83d212b8cca484bd4b694e45e132341ca0c26768.zip | |
Create new account when pressing create account button
| -rw-r--r-- | gui/src/config.json | 1 | ||||
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 5 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 10 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 68 | ||||
| -rw-r--r-- | gui/src/renderer/components/Login.tsx | 77 | ||||
| -rw-r--r-- | gui/src/renderer/components/Settings.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/containers/LoginPage.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/redux/account/actions.ts | 42 | ||||
| -rw-r--r-- | gui/src/renderer/redux/account/reducers.ts | 48 | ||||
| -rw-r--r-- | gui/src/shared/ipc-event-channel.ts | 5 |
10 files changed, 183 insertions, 81 deletions
diff --git a/gui/src/config.json b/gui/src/config.json index d81a0a5357..f5d4de5d74 100644 --- a/gui/src/config.json +++ b/gui/src/config.json @@ -1,6 +1,5 @@ { "links": { - "createAccount": "https://mullvad.net/account/create/", "purchase": "https://mullvad.net/account/", "manageKeys": "https://mullvad.net/account/ports/", "faq": "https://mullvad.net/help/tag/mullvad-app/", diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index b4147ff821..02b9deceec 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -452,6 +452,11 @@ export class DaemonRpc { } } + public async createNewAccount(): Promise<string> { + const response = await this.transport.send('create_new_account'); + return validate(string, response); + } + public async setAccount(accountToken?: AccountToken): Promise<void> { await this.transport.send('set_account', [accountToken]); } diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 417a6648cb..2dd41afbb3 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -985,6 +985,7 @@ class ApplicationMain { this.didChangeLocale(); }); + IpcMainEventChannel.account.handleCreate(() => this.createNewAccount()); IpcMainEventChannel.account.handleLogin((token: AccountToken) => this.login(token)); IpcMainEventChannel.account.handleLogout(() => this.logout()); IpcMainEventChannel.account.handleWwwAuthToken(() => this.daemonRpc.getWwwAuthToken()); @@ -1080,6 +1081,15 @@ class ApplicationMain { ); } + private async createNewAccount(): Promise<string> { + try { + return await this.daemonRpc.createNewAccount(); + } catch (error) { + log.error(`Failed to create account: ${error.message}`); + throw error; + } + } + private async login(accountToken: AccountToken): Promise<void> { try { const verification = await this.verifyAccount(accountToken); 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': diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts index 9931ecf56b..5268415bfa 100644 --- a/gui/src/shared/ipc-event-channel.ts +++ b/gui/src/shared/ipc-event-channel.ts @@ -106,12 +106,14 @@ interface IGuiSettingsHandlers extends ISender<IGuiSettingsState> { } interface IAccountHandlers extends ISender<IAccountData | undefined> { + handleCreate(fn: () => Promise<string>): void; handleLogin(fn: (token: AccountToken) => Promise<void>): void; handleLogout(fn: () => Promise<void>): void; handleWwwAuthToken(fn: () => Promise<string>): void; } interface IAccountMethods extends IReceiver<IAccountData | undefined> { + create(): Promise<string>; login(token: AccountToken): Promise<void>; logout(): Promise<void>; getWwwAuthToken(): Promise<string>; @@ -186,6 +188,7 @@ const GET_APP_STATE = 'get-app-state'; const ACCOUNT_HISTORY_CHANGED = 'account-history-changed'; const REMOVE_ACCOUNT_HISTORY_ITEM = 'remove-account-history-item'; +const CREATE_NEW_ACCOUNT = 'create-new-account'; const DO_LOGIN = 'do-login'; const DO_LOGOUT = 'do-logout'; const DO_GET_WWW_AUTH_TOKEN = 'do-get-www-auth-token'; @@ -280,6 +283,7 @@ export class IpcRendererEventChannel { public static account: IAccountMethods = { listen: listen(ACCOUNT_DATA_CHANGED), + create: requestSender(CREATE_NEW_ACCOUNT), login: requestSender(DO_LOGIN), logout: requestSender(DO_LOGOUT), getWwwAuthToken: requestSender(DO_GET_WWW_AUTH_TOKEN), @@ -375,6 +379,7 @@ export class IpcMainEventChannel { public static account: IAccountHandlers = { notify: sender<IAccountData | undefined>(ACCOUNT_DATA_CHANGED), + handleCreate: requestHandler(CREATE_NEW_ACCOUNT), handleLogin: requestHandler(DO_LOGIN), handleLogout: requestHandler(DO_LOGOUT), handleWwwAuthToken: requestHandler(DO_GET_WWW_AUTH_TOKEN), |
