summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/components
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-04-06 11:03:15 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-04-06 11:03:15 +0200
commit9894c710edf64d9bfe9f53746b3d885780197c73 (patch)
tree8c5cf3423b90c990672d774abe5b0b11e7921058 /gui/src/renderer/components
parent72415b7193772224b88d315593d64354a1677657 (diff)
parentd326e5088a6898aa3605a853aad53daba7173024 (diff)
downloadmullvadvpn-9894c710edf64d9bfe9f53746b3d885780197c73.tar.xz
mullvadvpn-9894c710edf64d9bfe9f53746b3d885780197c73.zip
Merge branch 'add-new-account-view'
Diffstat (limited to 'gui/src/renderer/components')
-rw-r--r--gui/src/renderer/components/Connect.tsx74
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx295
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx69
-rw-r--r--gui/src/renderer/components/Modal.tsx8
4 files changed, 271 insertions, 175 deletions
diff --git a/gui/src/renderer/components/Connect.tsx b/gui/src/renderer/components/Connect.tsx
index ebc818e767..55bc49c53a 100644
--- a/gui/src/renderer/components/Connect.tsx
+++ b/gui/src/renderer/components/Connect.tsx
@@ -1,30 +1,28 @@
import * as React from 'react';
import { Component, Styles, View } from 'reactxp';
-import { links } from '../../config.json';
import AccountExpiry from '../../shared/account-expiry';
+import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer';
import NotificationAreaContainer from '../containers/NotificationAreaContainer';
import { AuthFailureKind, parseAuthFailure } from '../lib/auth-failure';
+import { LoginState } from '../redux/account/reducers';
import { IConnectionReduxState } from '../redux/connection/reducers';
-import { IVersionReduxState } from '../redux/version/reducers';
-import ExpiredAccountErrorView, { RecoveryAction } from './ExpiredAccountErrorView';
import { Brand, HeaderBarStyle, SettingsBarButton } from './HeaderBar';
import ImageView from './ImageView';
import { Container, Header, Layout } from './Layout';
import Map, { MarkerStyle, ZoomLevel } from './Map';
+import { ModalContainer } from './Modal';
import TunnelControl from './TunnelControl';
interface IProps {
connection: IConnectionReduxState;
- version: IVersionReduxState;
+ loginState: LoginState;
accountExpiry?: AccountExpiry;
selectedRelayName: string;
- blockWhenDisconnected: boolean;
onSettings: () => void;
onSelectLocation: () => void;
onConnect: () => void;
onDisconnect: () => void;
onReconnect: () => void;
- onExternalLinkWithAuth: (url: string) => Promise<void>;
}
type MarkerOrSpinner = 'marker' | 'spinner';
@@ -81,7 +79,7 @@ export default class Connect extends Component<IProps, IState> {
super(props);
this.state = {
- isAccountExpired: this.checkAccountExpired(props, false),
+ isAccountExpired: this.checkAccountExpired(false),
};
}
@@ -91,20 +89,28 @@ export default class Connect extends Component<IProps, IState> {
public render() {
return (
- <Layout>
- <Header barStyle={this.headerBarStyle()}>
- <Brand />
- <SettingsBarButton onPress={this.props.onSettings} />
- </Header>
- <Container>
- {this.state.isAccountExpired ? this.renderExpiredAccountView() : this.renderMap()}
- </Container>
- </Layout>
+ <ModalContainer>
+ <Layout>
+ <Header barStyle={this.headerBarStyle()}>
+ <Brand />
+ <SettingsBarButton onPress={this.props.onSettings} />
+ </Header>
+ <Container>
+ {this.state.isAccountExpired ||
+ (this.props.loginState.type === 'ok' &&
+ this.props.loginState.method === 'new_account') ? (
+ <ExpiredAccountErrorViewContainer />
+ ) : (
+ this.renderMap()
+ )}
+ </Container>
+ </Layout>
+ </ModalContainer>
);
}
private updateAccountExpired() {
- const nextAccountExpired = this.checkAccountExpired(this.props, this.state.isAccountExpired);
+ const nextAccountExpired = this.checkAccountExpired(this.state.isAccountExpired);
if (nextAccountExpired !== this.state.isAccountExpired) {
this.setState({
@@ -113,8 +119,8 @@ export default class Connect extends Component<IProps, IState> {
}
}
- private checkAccountExpired(props: IProps, prevAccountExpired: boolean): boolean {
- const tunnelState = props.connection.status;
+ private checkAccountExpired(prevAccountExpired: boolean): boolean {
+ const tunnelState = this.props.connection.status;
// Blocked with auth failure / expired account
if (
@@ -135,16 +141,6 @@ export default class Connect extends Component<IProps, IState> {
return prevAccountExpired;
}
- private renderExpiredAccountView() {
- return (
- <ExpiredAccountErrorView
- blockWhenDisconnected={this.props.blockWhenDisconnected}
- isBlocked={this.props.connection.isBlocked}
- action={this.handleExpiredAccountRecovery}
- />
- );
- }
-
private renderMap() {
return (
<View style={styles.connect}>
@@ -174,26 +170,6 @@ export default class Connect extends Component<IProps, IState> {
);
}
- private handleExpiredAccountRecovery = async (recoveryAction: RecoveryAction): Promise<void> => {
- switch (recoveryAction) {
- case RecoveryAction.disableBlockedWhenDisconnected:
- break;
-
- case RecoveryAction.openBrowser:
- await this.props.onExternalLinkWithAuth(links.purchase);
- break;
-
- case RecoveryAction.disconnectAndOpenBrowser:
- try {
- await this.props.onDisconnect();
- await this.props.onExternalLinkWithAuth(links.purchase);
- } catch (error) {
- // no-op
- }
- break;
- }
- };
-
private headerBarStyle(): HeaderBarStyle {
const { status } = this.props.connection;
switch (status.state) {
diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
index 1b0ceeb9f8..7f50d7340f 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -1,174 +1,221 @@
import * as React from 'react';
-import { Component, Styles, View } from 'reactxp';
-import { colors } from '../../config.json';
+import { Component, Text, View } from 'reactxp';
+import { sprintf } from 'sprintf-js';
+import { links } from '../../config.json';
+import AccountExpiry from '../../shared/account-expiry';
+import { AccountToken } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
+import { LoginState } from '../redux/account/reducers';
+import AccountTokenLabel from './AccountTokenLabel';
import * as AppButton from './AppButton';
+import * as Cell from './Cell';
+import styles from './ExpiredAccountErrorViewStyles';
import ImageView from './ImageView';
+import { ModalAlert, ModalAlertType } from './Modal';
export enum RecoveryAction {
openBrowser,
- disconnectAndOpenBrowser,
+ disconnect,
disableBlockedWhenDisconnected,
}
-interface IProps {
+interface IExpiredAccountErrorViewProps {
isBlocked: boolean;
blockWhenDisconnected: boolean;
- action: (recoveryAction: RecoveryAction) => Promise<void>;
+ accountToken?: AccountToken;
+ accountExpiry?: AccountExpiry;
+ loginState: LoginState;
+ hideWelcomeView: () => void;
+ onExternalLinkWithAuth: (url: string) => Promise<void>;
+ onDisconnect: () => Promise<void>;
+ setBlockWhenDisconnected: (value: boolean) => void;
}
-interface IState {
- recoveryAction: RecoveryAction;
+interface IExpiredAccountErrorViewState {
+ showBlockWhenDisconnectedAlert: boolean;
}
-const styles = {
- container: Styles.createViewStyle({
- flex: 1,
- paddingTop: 94,
- }),
- body: Styles.createViewStyle({
- flex: 1,
- paddingHorizontal: 24,
- }),
- title: Styles.createTextStyle({
- fontFamily: 'DINPro',
- fontSize: 32,
- fontWeight: '900',
- lineHeight: 40,
- color: colors.white,
- marginBottom: 8,
- }),
- message: Styles.createTextStyle({
- fontFamily: 'Open Sans',
- fontSize: 13,
- lineHeight: 20,
- fontWeight: '600',
- color: colors.white,
- marginBottom: 24,
- }),
- statusIcon: Styles.createViewStyle({
- alignSelf: 'center',
- width: 60,
- height: 60,
- marginBottom: 32,
- }),
-};
-
-export default class ExpiredAccountErrorView extends Component<IProps, IState> {
- public static getDerivedStateFromProps(props: IProps): IState {
- const { blockWhenDisconnected, isBlocked } = props;
+export default class ExpiredAccountErrorView extends Component<
+ IExpiredAccountErrorViewProps,
+ IExpiredAccountErrorViewState
+> {
+ public state: IExpiredAccountErrorViewState = {
+ showBlockWhenDisconnectedAlert: false,
+ };
- if (blockWhenDisconnected && isBlocked) {
- return { recoveryAction: RecoveryAction.disableBlockedWhenDisconnected };
- } else if (!blockWhenDisconnected && isBlocked) {
- return { recoveryAction: RecoveryAction.disconnectAndOpenBrowser };
- } else {
- return { recoveryAction: RecoveryAction.openBrowser };
+ public componentDidUpdate() {
+ if (this.props.accountExpiry && !this.props.accountExpiry.hasExpired()) {
+ this.props.hideWelcomeView();
}
}
- public state: IState = { recoveryAction: RecoveryAction.openBrowser };
public render() {
return (
<View style={styles.container}>
- <View style={styles.statusIcon}>
- <ImageView source="icon-fail" height={60} width={60} />
- </View>
- <View style={styles.body}>
- <View style={styles.title}>{messages.pgettext('connect-view', 'Out of time')}</View>
- {this.renderContent()}
+ <View style={styles.body}>{this.renderContent()}</View>
+
+ <View style={styles.footer}>
+ {this.getRecoveryAction() === RecoveryAction.disconnect && (
+ <AppButton.BlockingButton onPress={this.props.onDisconnect}>
+ <AppButton.RedButton style={styles.button}>
+ {messages.pgettext('connect-view', 'Disconnect')}
+ </AppButton.RedButton>
+ </AppButton.BlockingButton>
+ )}
+
+ {this.renderExternalPaymentButton()}
</View>
+
+ {this.state.showBlockWhenDisconnectedAlert && this.renderBlockWhenDisconnectedAlert()}
</View>
);
}
private renderContent() {
- switch (this.state.recoveryAction) {
- case RecoveryAction.disconnectAndOpenBrowser:
- return <DisconnectAndOpenBrowserContentView actionHandler={this.handleAction} />;
- case RecoveryAction.openBrowser:
- return <OpenBrowserContentView actionHandler={this.handleAction} />;
- case RecoveryAction.disableBlockedWhenDisconnected:
- return <DisableBlockWhenDisconnectedContentView />;
+ if (this.isNewAccount()) {
+ return this.renderWelcomeView();
}
- }
-
- private handleAction = (): Promise<void> => {
- return this.props.action(this.state.recoveryAction);
- };
-}
-class DisconnectAndOpenBrowserContentView extends Component<{
- actionHandler: () => Promise<void>;
-}> {
- public render() {
return (
- <View>
- <View style={styles.message}>
- {messages.pgettext(
- 'connect-view',
- 'You have no more VPN time left on this account. To buy more credit on our website, you will need to access the Internet with an unsecured connection.',
- )}
+ <>
+ <View style={styles.statusIcon}>
+ <ImageView source="icon-fail" height={60} width={60} />
</View>
- <View>
- <AppButton.BlockingButton onPress={this.props.actionHandler}>
- <AppButton.RedButton>
- <AppButton.Label>
- {messages.pgettext('connect-view', 'Disconnect and buy more credit')}
- </AppButton.Label>
- <AppButton.Icon source="icon-extLink" height={16} width={16} />
- </AppButton.RedButton>
- </AppButton.BlockingButton>
+ <View style={styles.title}>{messages.pgettext('connect-view', 'Out of time')}</View>
+ <View style={styles.message}>
+ {sprintf('%(introduction)s %(recoveryMessage)s', {
+ introduction: messages.pgettext(
+ 'connect-view',
+ 'You have no more VPN time left on this account.',
+ ),
+ recoveryMessage: this.getRecoveryActionMessage(),
+ })}
</View>
- </View>
+ </>
);
}
-}
-class OpenBrowserContentView extends Component<{ actionHandler: () => Promise<void> }> {
- public render() {
+ private renderWelcomeView() {
return (
- <View>
- <View style={styles.message}>
- {messages.pgettext(
- 'connect-view',
- 'You have no more VPN time left on this account. Please log in on our website to buy more credit.',
- )}
+ <>
+ <View style={styles.title}>{messages.pgettext('connect-view', 'Congrats!')}</View>
+ <View style={[styles.message, styles.accountTokenMessage]}>
+ <Text style={[styles.fieldLabel, styles.accountTokenFieldLabel]}>
+ {messages.pgettext('connect-view', 'Here’s your account number. Save it!')}
+ </Text>
+ <View style={styles.accountTokenContainer}>
+ <AccountTokenLabel
+ style={styles.accountToken}
+ accountToken={this.props.accountToken || ''}
+ />
+ </View>
</View>
- <View>
- <AppButton.BlockingButton onPress={this.props.actionHandler}>
- <AppButton.GreenButton>
- <AppButton.Label>
- {messages.pgettext('connect-view', 'Buy more credit')}
- </AppButton.Label>
- <AppButton.Icon source="icon-extLink" height={16} width={16} />
- </AppButton.GreenButton>
- </AppButton.BlockingButton>
+
+ <View style={styles.message}>
+ {sprintf('%(introduction)s %(recoveryMessage)s', {
+ introduction: messages.pgettext(
+ 'connect-view',
+ 'To start using the app, you first need to add time to your account.',
+ ),
+ recoveryMessage: this.getRecoveryActionMessage(),
+ })}
</View>
- </View>
+ </>
);
}
-}
-class DisableBlockWhenDisconnectedContentView extends Component {
- public render() {
+ private getRecoveryActionMessage() {
+ switch (this.getRecoveryAction()) {
+ case RecoveryAction.openBrowser:
+ case RecoveryAction.disableBlockedWhenDisconnected:
+ return messages.pgettext(
+ 'connect-view',
+ 'Either buy credit on our website or redeem a voucher.',
+ );
+ case RecoveryAction.disconnect:
+ return messages.pgettext(
+ 'connect-view',
+ 'To add more, you will need to disconnect and access the Internet with an unsecured connection.',
+ );
+ }
+ }
+
+ private renderExternalPaymentButton() {
+ const buttonText = this.isNewAccount()
+ ? messages.pgettext('connect-view', 'Buy credit')
+ : messages.pgettext('connect-view', 'Buy more credit');
+
return (
- <View>
- <View style={styles.message}>
+ <AppButton.BlockingButton
+ disabled={this.getRecoveryAction() === RecoveryAction.disconnect}
+ onPress={this.onOpenExternalPayment}>
+ <AppButton.GreenButton>
+ <AppButton.Label>{buttonText}</AppButton.Label>
+ <AppButton.Icon source="icon-extLink" height={16} width={16} />
+ </AppButton.GreenButton>
+ </AppButton.BlockingButton>
+ );
+ }
+
+ private isNewAccount() {
+ return this.props.loginState.type === 'ok' && this.props.loginState.method === 'new_account';
+ }
+
+ private renderBlockWhenDisconnectedAlert() {
+ return (
+ <ModalAlert
+ type={ModalAlertType.Info}
+ buttons={[
+ <AppButton.BlueButton
+ key="cancel"
+ onPress={this.onCloseBlockWhenDisconnectedInstructions}>
+ {messages.pgettext('connect-view', 'Close')}
+ </AppButton.BlueButton>,
+ ]}>
+ <Text style={styles.fieldLabel}>
{messages.pgettext(
'connect-view',
- 'You have no more VPN time left on this account. Before you can buy more credit on our website, you first need to turn off the app\'s "Block when disconnected" option under Advanced settings.',
+ 'You need to disable “Block when disconnected” in order to access the Internet to add time.',
)}
- </View>
- <View>
- <AppButton.GreenButton disabled={true}>
- <AppButton.Label>
- {messages.pgettext('connect-view', 'Buy more credit')}
- </AppButton.Label>
- <AppButton.Icon source="icon-extLink" height={16} width={16} />
- </AppButton.GreenButton>
- </View>
- </View>
+ </Text>
+ <Text style={styles.fieldLabel}>
+ {messages.pgettext(
+ 'connect-view',
+ 'Remember, turning it off will allow network traffic while the VPN is disconnected until you turn it back on under Advanced settings.',
+ )}
+ </Text>
+ <Cell.Container>
+ <Cell.Label>{messages.pgettext('connect-view', 'Block when disconnected')}</Cell.Label>
+ <Cell.Switch
+ isOn={this.props.blockWhenDisconnected}
+ onChange={this.props.setBlockWhenDisconnected}
+ />
+ </Cell.Container>
+ </ModalAlert>
);
}
+
+ private onOpenExternalPayment = async (): Promise<void> => {
+ if (this.getRecoveryAction() === RecoveryAction.disableBlockedWhenDisconnected) {
+ this.setState({ showBlockWhenDisconnectedAlert: true });
+ } else {
+ await this.props.onExternalLinkWithAuth(links.purchase);
+ }
+ };
+
+ private getRecoveryAction() {
+ const { blockWhenDisconnected, isBlocked } = this.props;
+
+ if (blockWhenDisconnected && isBlocked) {
+ return RecoveryAction.disableBlockedWhenDisconnected;
+ } else if (!blockWhenDisconnected && isBlocked) {
+ return RecoveryAction.disconnect;
+ } else {
+ return RecoveryAction.openBrowser;
+ }
+ }
+
+ private onCloseBlockWhenDisconnectedInstructions = () => {
+ this.setState({ showBlockWhenDisconnectedAlert: false });
+ };
}
diff --git a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
new file mode 100644
index 0000000000..93535a043b
--- /dev/null
+++ b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
@@ -0,0 +1,69 @@
+import { Styles } from 'reactxp';
+import { colors } from '../../config.json';
+
+export default {
+ container: Styles.createViewStyle({
+ flex: 1,
+ paddingTop: 24,
+ }),
+ body: Styles.createViewStyle({
+ flex: 1,
+ paddingHorizontal: 24,
+ }),
+ footer: Styles.createViewStyle({
+ flex: 0,
+ paddingVertical: 24,
+ paddingHorizontal: 24,
+ backgroundColor: colors.darkBlue,
+ }),
+ title: Styles.createTextStyle({
+ fontFamily: 'DINPro',
+ fontSize: 32,
+ fontWeight: '900',
+ lineHeight: 40,
+ color: colors.white,
+ marginBottom: 8,
+ }),
+ message: Styles.createTextStyle({
+ fontFamily: 'Open Sans',
+ fontSize: 13,
+ lineHeight: 20,
+ fontWeight: '600',
+ color: colors.white,
+ marginBottom: 24,
+ }),
+ statusIcon: Styles.createViewStyle({
+ alignSelf: 'center',
+ width: 60,
+ height: 60,
+ marginBottom: 18,
+ }),
+ fieldLabel: Styles.createTextStyle({
+ fontFamily: 'Open Sans',
+ fontSize: 13,
+ fontWeight: '600',
+ lineHeight: 20,
+ color: colors.white,
+ marginBottom: 9,
+ }),
+ accountTokenMessage: Styles.createViewStyle({
+ marginBottom: 0,
+ }),
+ accountTokenFieldLabel: Styles.createTextStyle({
+ marginBottom: 0,
+ }),
+ accountTokenContainer: Styles.createViewStyle({
+ height: 68,
+ justifyContent: 'center',
+ }),
+ accountToken: Styles.createTextStyle({
+ fontFamily: 'Open Sans',
+ lineHeight: 24,
+ fontSize: 24,
+ fontWeight: '800',
+ color: colors.white,
+ }),
+ button: Styles.createViewStyle({
+ marginBottom: 24,
+ }),
+};
diff --git a/gui/src/renderer/components/Modal.tsx b/gui/src/renderer/components/Modal.tsx
index db7aca982b..b4c6ace833 100644
--- a/gui/src/renderer/components/Modal.tsx
+++ b/gui/src/renderer/components/Modal.tsx
@@ -99,8 +99,9 @@ export enum ModalAlertType {
interface IModalAlertProps {
type?: ModalAlertType;
- message: string;
+ message?: string;
buttons: React.ReactNode[];
+ children?: React.ReactNode;
}
export class ModalAlert extends Component<IModalAlertProps> {
@@ -121,7 +122,10 @@ export class ModalAlert extends Component<IModalAlertProps> {
{this.props.type && (
<View style={styles.modalAlertIcon}>{this.renderTypeIcon(this.props.type)}</View>
)}
- <Text style={styles.modalAlertMessage}>{this.props.message}</Text>
+ {this.props.message && (
+ <Text style={styles.modalAlertMessage}>{this.props.message}</Text>
+ )}
+ {this.props.children}
{this.props.buttons.map((button, index) => (
<View key={index} style={styles.modalAlertButtonContainer}>
{button}