diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-06-15 12:42:09 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-06-28 09:00:49 +0200 |
| commit | ddcc5faae92a1d4f0f8fd42364fc5b6245057f7a (patch) | |
| tree | a8c13d6c52c5389abe2dcf636d94c3ee218fbce3 | |
| parent | 7a2593e312083c7bb7ef71982307dea8e62e41cc (diff) | |
| download | mullvadvpn-ddcc5faae92a1d4f0f8fd42364fc5b6245057f7a.tar.xz mullvadvpn-ddcc5faae92a1d4f0f8fd42364fc5b6245057f7a.zip | |
Implement new new account flow
| -rw-r--r-- | gui/src/config.json | 1 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountAddTime.tsx | 246 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorView.tsx | 23 | ||||
| -rw-r--r-- | gui/src/renderer/components/RedeemVoucher.tsx | 33 | ||||
| -rw-r--r-- | gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx | 8 | ||||
| -rw-r--r-- | gui/src/renderer/routes.tsx | 14 | ||||
| -rw-r--r-- | gui/src/renderer/transitions.ts | 6 |
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), ]; |
