summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2021-06-15 12:42:09 +0200
committerOskar Nyberg <oskar@mullvad.net>2021-06-28 09:00:49 +0200
commitddcc5faae92a1d4f0f8fd42364fc5b6245057f7a (patch)
treea8c13d6c52c5389abe2dcf636d94c3ee218fbce3
parent7a2593e312083c7bb7ef71982307dea8e62e41cc (diff)
downloadmullvadvpn-ddcc5faae92a1d4f0f8fd42364fc5b6245057f7a.tar.xz
mullvadvpn-ddcc5faae92a1d4f0f8fd42364fc5b6245057f7a.zip
Implement new new account flow
-rw-r--r--gui/src/config.json1
-rw-r--r--gui/src/renderer/components/ExpiredAccountAddTime.tsx246
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx23
-rw-r--r--gui/src/renderer/components/RedeemVoucher.tsx33
-rw-r--r--gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx8
-rw-r--r--gui/src/renderer/routes.tsx14
-rw-r--r--gui/src/renderer/transitions.ts6
7 files changed, 296 insertions, 35 deletions
diff --git a/gui/src/config.json b/gui/src/config.json
index 59a214b604..112391c360 100644
--- a/gui/src/config.json
+++ b/gui/src/config.json
@@ -4,6 +4,7 @@
"purchase": "https://mullvad.net/account/",
"manageKeys": "https://mullvad.net/account/ports/",
"faq": "https://mullvad.net/help/tag/mullvad-app/",
+ "privacyGuide": "https://mullvad.net/help/first-steps-towards-online-privacy/",
"download": "https://mullvad.net/download/",
"betaDownload": "https://mullvad.net/download/beta"
},
diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
new file mode 100644
index 0000000000..0e001aa358
--- /dev/null
+++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
@@ -0,0 +1,246 @@
+import React, { useCallback } from 'react';
+import { useSelector } from 'react-redux';
+import { useHistory } from 'react-router';
+import { sprintf } from 'sprintf-js';
+import styled from 'styled-components';
+import { links, colors } from '../../config.json';
+import { formatRelativeDate } from '../../shared/date-helper';
+import { messages } from '../../shared/gettext';
+import { useAppContext } from '../context';
+import History from '../lib/history';
+import { IReduxState } from '../redux/store';
+import * as AppButton from './AppButton';
+import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
+import { bigText } from './common-styles';
+import CustomScrollbars from './CustomScrollbars';
+import { calculateHeaderBarStyle, DefaultHeaderBar, HeaderBarStyle } from './HeaderBar';
+import ImageView from './ImageView';
+import { Container, Layout } from './Layout';
+import {
+ RedeemVoucherContainer,
+ RedeemVoucherInput,
+ RedeemVoucherResponse,
+ RedeemVoucherSubmitButton,
+} from './RedeemVoucher';
+
+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',
+ paddingBottom: 'auto',
+});
+
+export const StyledFooter = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 0,
+ padding: '18px 22px 22px',
+});
+
+export const StyledTitle = styled.span(bigText, {
+ lineHeight: '38px',
+ marginBottom: '8px',
+});
+
+export const StyledLabel = styled.span({
+ fontFamily: 'Open Sans',
+ fontSize: '13px',
+ fontWeight: 600,
+ lineHeight: '20px',
+ color: colors.white,
+ marginBottom: '9px',
+});
+
+export const StyledRedeemVoucherInput = styled(RedeemVoucherInput)({
+ flex: 0,
+});
+
+export const StyledStatusIcon = styled.div({
+ alignSelf: 'center',
+ width: '60px',
+ height: '60px',
+ marginBottom: '18px',
+});
+
+export function VoucherInput() {
+ const history = useHistory();
+
+ const onSuccess = useCallback(() => {
+ history.push('/main/voucher/success');
+ }, [history]);
+
+ const navigateBack = useCallback(() => {
+ history.goBack();
+ }, [history]);
+
+ return (
+ <Layout>
+ <HeaderBar />
+ <StyledCustomScrollbars fillContainer>
+ <StyledContainer>
+ <RedeemVoucherContainer onSuccess={onSuccess}>
+ <StyledBody>
+ <StyledTitle>{messages.pgettext('connect-view', 'Redeem voucher')}</StyledTitle>
+ <StyledLabel>{messages.pgettext('connect-view', 'Enter voucher code')}</StyledLabel>
+ <StyledRedeemVoucherInput />
+ <RedeemVoucherResponse disableSuccessMessage />
+ </StyledBody>
+
+ <StyledFooter>
+ <AppButton.ButtonGroup>
+ <RedeemVoucherSubmitButton />
+ <AppButton.BlueButton onClick={navigateBack}>
+ {messages.gettext('Cancel')}
+ </AppButton.BlueButton>
+ </AppButton.ButtonGroup>
+ </StyledFooter>
+ </RedeemVoucherContainer>
+ </StyledContainer>
+ </StyledCustomScrollbars>
+ </Layout>
+ );
+}
+
+export function VoucherVerificationSuccess() {
+ return (
+ <TimeAdded title={messages.pgettext('connect-view', 'Voucher was successfully redeemed')} />
+ );
+}
+
+interface ITimeAddedProps {
+ title?: string;
+}
+
+export function TimeAdded(props: ITimeAddedProps) {
+ const history = useHistory();
+ const accountData = useSelector((state: IReduxState) => state.account);
+
+ const navigateToSetupFinished = useCallback(() => {
+ history.push('/main/setup-finished');
+ }, [history]);
+
+ const duration =
+ (accountData.expiry &&
+ accountData.previousExpiry &&
+ formatRelativeDate(accountData.expiry, accountData.previousExpiry)) ??
+ '';
+
+ return (
+ <Layout>
+ <HeaderBar />
+ <StyledCustomScrollbars fillContainer>
+ <StyledContainer>
+ <StyledBody>
+ <StyledStatusIcon>
+ <ImageView source="icon-success" height={60} width={60} />
+ </StyledStatusIcon>
+ <StyledTitle>
+ {props.title ?? messages.pgettext('connect-view', 'Time was successfully added')}
+ </StyledTitle>
+ <StyledLabel>
+ {sprintf(
+ messages.pgettext('connect-view', '%(duration)s was added to your account.'),
+ { duration },
+ )}
+ </StyledLabel>
+ </StyledBody>
+
+ <StyledFooter>
+ <AppButton.BlueButton onClick={navigateToSetupFinished}>
+ {messages.gettext('Next')}
+ </AppButton.BlueButton>
+ </StyledFooter>
+ </StyledContainer>
+ </StyledCustomScrollbars>
+ </Layout>
+ );
+}
+
+export function SetupFinished() {
+ const history = useHistory() as History;
+ const { openUrl } = useAppContext();
+
+ const navigateToMain = useCallback(() => {
+ history.resetWith('/main');
+ }, [history]);
+
+ const openPrivacyLink = useCallback(() => openUrl(links.privacyGuide), [openUrl]);
+
+ return (
+ <Layout>
+ <HeaderBar />
+ <StyledCustomScrollbars fillContainer>
+ <StyledContainer>
+ <StyledBody>
+ <StyledTitle>{messages.pgettext('connect-view', "You're all set!")}</StyledTitle>
+ <StyledLabel>
+ {messages.pgettext(
+ 'connect-view',
+ 'Go ahead and start using the app to begin reclaiming your online privacy.',
+ )}
+ </StyledLabel>
+ <StyledLabel>
+ {messages.pgettext(
+ 'connect-view',
+ 'To continue your journey as a privacy ninja, visit our website to pick up other privacy-friendly habits and tools.',
+ )}
+ </StyledLabel>
+ </StyledBody>
+
+ <StyledFooter>
+ <AppButton.ButtonGroup>
+ <AriaDescriptionGroup>
+ <AriaDescribed>
+ <AppButton.BlueButton onClick={openPrivacyLink}>
+ <AppButton.Label>
+ {messages.pgettext('connect-view', 'Learn more about privacy')}
+ </AppButton.Label>
+ <AriaDescription>
+ <AppButton.Icon
+ height={16}
+ width={16}
+ source="icon-extLink"
+ aria-label={messages.pgettext('accessibility', 'Opens externally')}
+ />
+ </AriaDescription>
+ </AppButton.BlueButton>
+ </AriaDescribed>
+ </AriaDescriptionGroup>
+ <AppButton.GreenButton onClick={navigateToMain}>
+ {messages.pgettext('connect-view', 'Start using the app')}
+ </AppButton.GreenButton>
+ </AppButton.ButtonGroup>
+ </StyledFooter>
+ </StyledContainer>
+ </StyledCustomScrollbars>
+ </Layout>
+ );
+}
+
+function HeaderBar() {
+ const isNewAccount = useSelector(
+ (state: IReduxState) =>
+ state.account.status.type === 'ok' && state.account.status.method === 'new_account',
+ );
+ const tunnelState = useSelector((state: IReduxState) => state.connection.status);
+ const headerBarStyle = isNewAccount
+ ? HeaderBarStyle.default
+ : calculateHeaderBarStyle(tunnelState);
+
+ return <StyledHeader barStyle={headerBarStyle} />;
+}
diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
index 8623531cfc..90e3148bbb 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -28,7 +28,6 @@ import { calculateHeaderBarStyle, HeaderBarStyle } from './HeaderBar';
import ImageView from './ImageView';
import { Layout } from './Layout';
import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal';
-import { RedeemVoucherContainer, RedeemVoucherAlert } from './RedeemVoucher';
export enum RecoveryAction {
openBrowser,
@@ -47,11 +46,11 @@ interface IExpiredAccountErrorViewProps {
onExternalLinkWithAuth: (url: string) => Promise<void>;
onDisconnect: () => Promise<void>;
setBlockWhenDisconnected: (value: boolean) => void;
+ navigateToRedeemVoucher: () => void;
}
interface IExpiredAccountErrorViewState {
showBlockWhenDisconnectedAlert: boolean;
- showRedeemVoucherAlert: boolean;
}
export default class ExpiredAccountErrorView extends React.Component<
@@ -60,7 +59,6 @@ export default class ExpiredAccountErrorView extends React.Component<
> {
public state: IExpiredAccountErrorViewState = {
showBlockWhenDisconnectedAlert: false,
- showRedeemVoucherAlert: false,
};
public componentDidUpdate() {
@@ -96,12 +94,11 @@ export default class ExpiredAccountErrorView extends React.Component<
<AppButton.GreenButton
disabled={this.getRecoveryAction() === RecoveryAction.disconnect}
- onClick={this.onOpenRedeemVoucherAlert}>
+ onClick={this.props.navigateToRedeemVoucher}>
{messages.pgettext('connect-view', 'Redeem voucher')}
</AppButton.GreenButton>
</StyledFooter>
- {this.state.showRedeemVoucherAlert && this.renderRedeemVoucherAlert()}
{this.state.showBlockWhenDisconnectedAlert && this.renderBlockWhenDisconnectedAlert()}
</StyledContainer>
</StyledCustomScrollbars>
@@ -202,14 +199,6 @@ export default class ExpiredAccountErrorView extends React.Component<
);
}
- private renderRedeemVoucherAlert() {
- return (
- <RedeemVoucherContainer onSuccess={this.props.hideWelcomeView}>
- <RedeemVoucherAlert onClose={this.onCloseRedeemVoucherAlert} />
- </RedeemVoucherContainer>
- );
- }
-
private renderBlockWhenDisconnectedAlert() {
return (
<ModalAlert
@@ -269,14 +258,6 @@ export default class ExpiredAccountErrorView extends React.Component<
}
}
- private onOpenRedeemVoucherAlert = () => {
- this.setState({ showRedeemVoucherAlert: true });
- };
-
- private onCloseRedeemVoucherAlert = () => {
- this.setState({ showRedeemVoucherAlert: false });
- };
-
private onCloseBlockWhenDisconnectedInstructions = () => {
this.setState({ showBlockWhenDisconnectedAlert: false });
};
diff --git a/gui/src/renderer/components/RedeemVoucher.tsx b/gui/src/renderer/components/RedeemVoucher.tsx
index 0e837502b9..b878a94c4f 100644
--- a/gui/src/renderer/components/RedeemVoucher.tsx
+++ b/gui/src/renderer/components/RedeemVoucher.tsx
@@ -1,7 +1,6 @@
import React, { useCallback, useContext, useState } from 'react';
import { VoucherResponse } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
-import { useScheduler } from '../../shared/scheduler';
import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
import accountActions from '../redux/account/actions';
@@ -60,7 +59,6 @@ interface IRedeemVoucherProps {
export function RedeemVoucherContainer(props: IRedeemVoucherProps) {
const { onSubmit, onSuccess, onFailure } = props;
- const closeScheduler = useScheduler();
const { submitVoucher } = useAppContext();
const { updateAccountExpiry } = useActions(accountActions);
@@ -89,9 +87,7 @@ export function RedeemVoucherContainer(props: IRedeemVoucherProps) {
setSubmitting(false);
setResponse(response);
if (response.type === 'success') {
- closeScheduler.schedule(() => {
- onSuccess?.();
- }, 1000);
+ onSuccess?.();
} else {
onFailure?.();
}
@@ -105,7 +101,11 @@ export function RedeemVoucherContainer(props: IRedeemVoucherProps) {
);
}
-export function RedeemVoucherInput() {
+interface IRedeemVoucherInputProps {
+ className?: string;
+}
+
+export function RedeemVoucherInput(props: IRedeemVoucherInputProps) {
const { value, setValue, onSubmit, submitting, response } = useContext(RedeemVoucherContext);
const disabled = submitting || response?.type === 'success';
@@ -127,6 +127,7 @@ export function RedeemVoucherInput() {
return (
<StyledInput
+ className={props.className}
allowedCharacters="[A-Z0-9]"
separator="-"
uppercaseOnly
@@ -142,7 +143,11 @@ export function RedeemVoucherInput() {
);
}
-export function RedeemVoucherResponse() {
+interface IRedeemVoucherResponseProps {
+ disableSuccessMessage?: boolean;
+}
+
+export function RedeemVoucherResponse(props: IRedeemVoucherResponseProps) {
const { response, submitting } = useContext(RedeemVoucherContext);
if (submitting) {
@@ -152,11 +157,15 @@ export function RedeemVoucherResponse() {
if (response) {
switch (response.type) {
case 'success':
- return (
- <StyledSuccessResponse>
- {messages.pgettext('redeem-voucher-view', 'Voucher was successfully redeemed.')}
- </StyledSuccessResponse>
- );
+ if (props.disableSuccessMessage) {
+ return <StyledEmptyResponse />;
+ } else {
+ return (
+ <StyledSuccessResponse>
+ {messages.pgettext('redeem-voucher-view', 'Voucher was successfully redeemed.')}
+ </StyledSuccessResponse>
+ );
+ }
case 'invalid':
return (
<StyledErrorResponse>
diff --git a/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx b/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx
index 46890cb169..4d3d8c1271 100644
--- a/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx
+++ b/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx
@@ -1,4 +1,5 @@
import { connect } from 'react-redux';
+import { RouteComponentProps, withRouter } from 'react-router';
import { bindActionCreators } from 'redux';
import log from '../../shared/logging';
import ExpiredAccountErrorView from '../components/ExpiredAccountErrorView';
@@ -15,7 +16,7 @@ const mapStateToProps = (state: IReduxState) => ({
isBlocked: state.connection.isBlocked,
blockWhenDisconnected: state.settings.blockWhenDisconnected,
});
-const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext) => {
+const mapDispatchToProps = (dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => {
const account = bindActionCreators(accountActions, dispatch);
return {
// Changes login method from "new_account" to "existing_account"
@@ -35,9 +36,12 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext) => {
log.error('Failed to update block when disconnected', e.message);
}
},
+ navigateToRedeemVoucher: () => {
+ props.history.push('/main/voucher/redeem');
+ },
};
};
export default withAppContext(
- connect(mapStateToProps, mapDispatchToProps)(ExpiredAccountErrorView),
+ withRouter(connect(mapStateToProps, mapDispatchToProps)(ExpiredAccountErrorView)),
);
diff --git a/gui/src/renderer/routes.tsx b/gui/src/renderer/routes.tsx
index 4fb92a237e..2c2325db19 100644
--- a/gui/src/renderer/routes.tsx
+++ b/gui/src/renderer/routes.tsx
@@ -18,6 +18,12 @@ import SupportPage from './containers/SupportPage';
import WireguardKeysPage from './containers/WireguardKeysPage';
import History from './lib/history';
import { getTransitionProps } from './transitions';
+import {
+ SetupFinished,
+ TimeAdded,
+ VoucherInput,
+ VoucherVerificationSuccess,
+} from './components/ExpiredAccountAddTime';
interface IAppRoutesState {
previousLocation?: RouteComponentProps['location'];
@@ -73,6 +79,14 @@ class AppRoutes extends React.Component<RouteComponentProps, IAppRoutesState> {
<Route exact={true} path="/" component={Launch} />
<Route exact={true} path="/login" component={LoginPage} />
<Route exact={true} path="/main" component={MainView} />
+ <Route exact={true} path="/main/voucher/redeem" component={VoucherInput} />
+ <Route
+ exact={true}
+ path="/main/voucher/success"
+ component={VoucherVerificationSuccess}
+ />
+ <Route exact={true} path="/main/time-added" component={TimeAdded} />
+ <Route exact={true} path="/main/setup-finished" component={SetupFinished} />
<Route exact={true} path="/settings" component={SettingsPage} />
<Route exact={true} path="/settings/language" component={SelectLanguagePage} />
<Route exact={true} path="/settings/account" component={AccountPage} />
diff --git a/gui/src/renderer/transitions.ts b/gui/src/renderer/transitions.ts
index da1a45ac01..9dbf971c45 100644
--- a/gui/src/renderer/transitions.ts
+++ b/gui/src/renderer/transitions.ts
@@ -47,6 +47,12 @@ const transitionRules = [
r('/settings/advanced', '/settings/advanced/wireguard-keys', transitions.push),
r('/settings/advanced', '/settings/advanced/linux-split-tunneling', transitions.push),
r('/settings', '/settings/support', transitions.push),
+ r('/main', '/main/time-added', transitions.push),
+ r('/main/time-added', '/main/setup-finished', transitions.push),
+ r('/main', '/main/voucher/redeem', transitions.push),
+ r('/main/voucher/redeem', '/main/voucher/success', transitions.push),
+ r('/main/voucher/success', '/main/setup-finished', transitions.push),
+ r('/main/setup-finished', '/main', transitions.push),
r(null, '/settings', transitions.slide),
r(null, '/select-location', transitions.slide),
];