summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/components
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2021-06-28 12:12:35 +0200
committerOskar Nyberg <oskar@mullvad.net>2021-06-28 12:12:35 +0200
commit4d95c0a019c2d7bfb2c3ca4951febfded93e9758 (patch)
tree1cb7b54c080a865e597102e16b1b9b32e4ca3505 /gui/src/renderer/components
parent09a3e447f47591643032fbf988dd9017db629b25 (diff)
parentb51b561baabebf3210f936eddc90390488e8819b (diff)
downloadmullvadvpn-4d95c0a019c2d7bfb2c3ca4951febfded93e9758.tar.xz
mullvadvpn-4d95c0a019c2d7bfb2c3ca4951febfded93e9758.zip
Merge branch 'new-account-flow'
Diffstat (limited to 'gui/src/renderer/components')
-rw-r--r--gui/src/renderer/components/AppButton.tsx23
-rw-r--r--gui/src/renderer/components/Connect.tsx44
-rw-r--r--gui/src/renderer/components/ExpiredAccountAddTime.tsx274
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx90
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx13
-rw-r--r--gui/src/renderer/components/HeaderBar.tsx33
-rw-r--r--gui/src/renderer/components/MainView.tsx28
-rw-r--r--gui/src/renderer/components/RedeemVoucher.tsx42
8 files changed, 436 insertions, 111 deletions
diff --git a/gui/src/renderer/components/AppButton.tsx b/gui/src/renderer/components/AppButton.tsx
index 352d70d9e9..d424e8b54e 100644
--- a/gui/src/renderer/components/AppButton.tsx
+++ b/gui/src/renderer/components/AppButton.tsx
@@ -191,3 +191,26 @@ export const RedTransparentButton = styled(BaseButton)({
backgroundColor: colors.red80,
},
});
+
+const StyledButtonWrapper = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ flex: 0,
+ ':not(:last-child)': {
+ marginBottom: '18px',
+ },
+});
+
+interface IButtonGroupProps {
+ children: React.ReactElement[];
+}
+
+export function ButtonGroup(props: IButtonGroupProps) {
+ return (
+ <>
+ {React.Children.map(props.children, (button, index) => (
+ <StyledButtonWrapper key={index}>{button}</StyledButtonWrapper>
+ ))}
+ </>
+ );
+}
diff --git a/gui/src/renderer/components/Connect.tsx b/gui/src/renderer/components/Connect.tsx
index a6654ad8c0..e48abf8d96 100644
--- a/gui/src/renderer/components/Connect.tsx
+++ b/gui/src/renderer/components/Connect.tsx
@@ -1,15 +1,13 @@
import * as React from 'react';
import styled from 'styled-components';
import { hasExpired } from '../../shared/account-expiry';
-import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer';
import NotificationArea from '../components/NotificationArea';
import { AuthFailureKind, parseAuthFailure } from '../../shared/auth-failure';
import { LoginState } from '../redux/account/reducers';
import { IConnectionReduxState } from '../redux/connection/reducers';
-import { FocusFallback } from './Focus';
-import { Brand, HeaderBarStyle, HeaderBarSettingsButton } from './HeaderBar';
+import { calculateHeaderBarStyle, DefaultHeaderBar } from './HeaderBar';
import ImageView from './ImageView';
-import { Container, Header, Layout } from './Layout';
+import { Container, Layout } from './Layout';
import Map, { MarkerStyle, ZoomLevel } from './Map';
import { ModalContainer } from './Modal';
import TunnelControl from './TunnelControl';
@@ -89,21 +87,8 @@ export default class Connect extends React.Component<IProps, IState> {
return (
<ModalContainer>
<Layout>
- <Header barStyle={this.headerBarStyle()}>
- <FocusFallback>
- <Brand />
- </FocusFallback>
- <HeaderBarSettingsButton />
- </Header>
- <StyledContainer>
- {this.state.isAccountExpired ||
- (this.props.loginState.type === 'ok' &&
- this.props.loginState.method === 'new_account') ? (
- <ExpiredAccountErrorViewContainer />
- ) : (
- this.renderMap()
- )}
- </StyledContainer>
+ <DefaultHeaderBar barStyle={calculateHeaderBarStyle(this.props.connection.status)} />
+ <StyledContainer>{this.renderMap()}</StyledContainer>
</Layout>
</ModalContainer>
);
@@ -171,27 +156,6 @@ export default class Connect extends React.Component<IProps, IState> {
);
}
- private headerBarStyle(): HeaderBarStyle {
- const { status } = this.props.connection;
- switch (status.state) {
- case 'disconnected':
- return HeaderBarStyle.error;
- case 'connecting':
- case 'connected':
- return HeaderBarStyle.success;
- case 'error':
- return !status.details.blockFailure ? HeaderBarStyle.success : HeaderBarStyle.error;
- case 'disconnecting':
- switch (status.details) {
- case 'block':
- case 'reconnect':
- return HeaderBarStyle.success;
- case 'nothing':
- return HeaderBarStyle.error;
- }
- }
- }
-
private getMapProps(): Map['props'] {
const {
longitude,
diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
new file mode 100644
index 0000000000..9831430d4e
--- /dev/null
+++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
@@ -0,0 +1,274 @@
+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 useActions from '../lib/actionsHook';
+import History from '../lib/history';
+import account from '../redux/account/actions';
+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 finish = useFinishedCallback();
+ const accountData = useSelector((state: IReduxState) => state.account);
+ const isNewAccount = useSelector(
+ (state: IReduxState) =>
+ state.account.status.type === 'ok' && state.account.status.method === 'new_account',
+ );
+
+ const navigateToSetupFinished = useCallback(() => {
+ if (isNewAccount) {
+ history.push('/main/setup-finished');
+ } else {
+ finish();
+ }
+ }, [history, finish]);
+
+ 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 finish = useFinishedCallback();
+ const { openUrl } = useAppContext();
+
+ 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 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={finish}>
+ {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} />;
+}
+
+function useFinishedCallback() {
+ const { loggedIn } = useActions(account);
+
+ const history = useHistory() as History;
+ const isNewAccount = useSelector(
+ (state: IReduxState) =>
+ state.account.status.type === 'ok' && state.account.status.method === 'new_account',
+ );
+
+ const callback = useCallback(() => {
+ // Changes login method from "new_account" to "existing_account"
+ if (isNewAccount) {
+ loggedIn();
+ }
+
+ history.resetWith('/main');
+ }, [isNewAccount, loggedIn, history]);
+
+ return callback;
+}
diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
index a01993787b..04d9f96156 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -1,8 +1,7 @@
import * as React from 'react';
import { sprintf } from 'sprintf-js';
import { links } from '../../config.json';
-import { hasExpired } from '../../shared/account-expiry';
-import { AccountToken } from '../../shared/daemon-rpc-types';
+import { AccountToken, TunnelState } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { LoginState } from '../redux/account/reducers';
import * as AppButton from './AppButton';
@@ -18,14 +17,16 @@ import {
StyledCustomScrollbars,
StyledDisconnectButton,
StyledFooter,
+ StyledHeader,
StyledMessage,
StyledModalCellContainer,
StyledStatusIcon,
StyledTitle,
} from './ExpiredAccountErrorViewStyles';
+import { calculateHeaderBarStyle, HeaderBarStyle } from './HeaderBar';
import ImageView from './ImageView';
-import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
-import { RedeemVoucherContainer, RedeemVoucherAlert } from './RedeemVoucher';
+import { Layout } from './Layout';
+import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal';
export enum RecoveryAction {
openBrowser,
@@ -37,17 +38,16 @@ interface IExpiredAccountErrorViewProps {
isBlocked: boolean;
blockWhenDisconnected: boolean;
accountToken?: AccountToken;
- accountExpiry?: string;
loginState: LoginState;
- hideWelcomeView: () => void;
+ tunnelState: TunnelState;
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<
@@ -56,43 +56,45 @@ export default class ExpiredAccountErrorView extends React.Component<
> {
public state: IExpiredAccountErrorViewState = {
showBlockWhenDisconnectedAlert: false,
- showRedeemVoucherAlert: false,
};
- public componentDidUpdate() {
- if (this.props.accountExpiry && !hasExpired(this.props.accountExpiry)) {
- this.props.hideWelcomeView();
- }
- }
-
public render() {
+ const headerBarStyle =
+ this.props.loginState.type === 'ok' && this.props.loginState.method === 'new_account'
+ ? HeaderBarStyle.default
+ : calculateHeaderBarStyle(this.props.tunnelState);
+
return (
- <StyledCustomScrollbars fillContainer>
- <StyledContainer>
- <StyledBody>{this.renderContent()}</StyledBody>
+ <ModalContainer>
+ <Layout>
+ <StyledHeader barStyle={headerBarStyle} />
+ <StyledCustomScrollbars fillContainer>
+ <StyledContainer>
+ <StyledBody>{this.renderContent()}</StyledBody>
- <StyledFooter>
- {this.getRecoveryAction() === RecoveryAction.disconnect && (
- <AppButton.BlockingButton onClick={this.props.onDisconnect}>
- <StyledDisconnectButton>
- {messages.pgettext('connect-view', 'Disconnect')}
- </StyledDisconnectButton>
- </AppButton.BlockingButton>
- )}
+ <StyledFooter>
+ {this.getRecoveryAction() === RecoveryAction.disconnect && (
+ <AppButton.BlockingButton onClick={this.props.onDisconnect}>
+ <StyledDisconnectButton>
+ {messages.pgettext('connect-view', 'Disconnect')}
+ </StyledDisconnectButton>
+ </AppButton.BlockingButton>
+ )}
- {this.renderExternalPaymentButton()}
+ {this.renderExternalPaymentButton()}
- <AppButton.GreenButton
- disabled={this.getRecoveryAction() === RecoveryAction.disconnect}
- onClick={this.onOpenRedeemVoucherAlert}>
- {messages.pgettext('connect-view', 'Redeem voucher')}
- </AppButton.GreenButton>
- </StyledFooter>
+ <AppButton.GreenButton
+ disabled={this.getRecoveryAction() === RecoveryAction.disconnect}
+ 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>
+ {this.state.showBlockWhenDisconnectedAlert && this.renderBlockWhenDisconnectedAlert()}
+ </StyledContainer>
+ </StyledCustomScrollbars>
+ </Layout>
+ </ModalContainer>
);
}
@@ -188,14 +190,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
@@ -255,14 +249,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/ExpiredAccountErrorViewStyles.tsx b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
index 3559d3b8ad..8872bbb312 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx
@@ -5,6 +5,12 @@ import * as AppButton from './AppButton';
import * as Cell from './cell';
import { bigText, smallText } from './common-styles';
import CustomScrollbars from './CustomScrollbars';
+import { DefaultHeaderBar } from './HeaderBar';
+import { Container } from './Layout';
+
+export const StyledHeader = styled(DefaultHeaderBar)({
+ flex: 0,
+});
export const StyledAccountTokenLabel = styled(AccountTokenLabel)({
fontFamily: 'Open Sans',
@@ -31,12 +37,10 @@ export const StyledCustomScrollbars = styled(CustomScrollbars)({
flex: 1,
});
-export const StyledContainer = styled.div({
- display: 'flex',
- flexDirection: 'column',
- flex: 1,
+export const StyledContainer = styled(Container)({
paddingTop: '22px',
minHeight: '100%',
+ backgroundColor: colors.darkBlue,
});
export const StyledBody = styled.div({
@@ -51,7 +55,6 @@ export const StyledFooter = styled.div({
flexDirection: 'column',
flex: 0,
padding: '18px 22px 22px',
- backgroundColor: colors.darkBlue,
});
export const StyledTitle = styled.span(bigText, {
diff --git a/gui/src/renderer/components/HeaderBar.tsx b/gui/src/renderer/components/HeaderBar.tsx
index bdf7d51924..b89811913e 100644
--- a/gui/src/renderer/components/HeaderBar.tsx
+++ b/gui/src/renderer/components/HeaderBar.tsx
@@ -3,8 +3,10 @@ import { useSelector } from 'react-redux';
import { useHistory } from 'react-router';
import styled from 'styled-components';
import { colors } from '../../config.json';
+import { TunnelState } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { IReduxState } from '../redux/store';
+import { FocusFallback } from './Focus';
import ImageView from './ImageView';
export enum HeaderBarStyle {
@@ -118,3 +120,34 @@ export function HeaderBarSettingsButton() {
</HeaderBarSettingsButtonContainer>
);
}
+
+export function DefaultHeaderBar(props: IHeaderBarProps) {
+ return (
+ <HeaderBar {...props}>
+ <FocusFallback>
+ <Brand />
+ </FocusFallback>
+ <HeaderBarSettingsButton />
+ </HeaderBar>
+ );
+}
+
+export function calculateHeaderBarStyle(tunnelState: TunnelState): HeaderBarStyle {
+ switch (tunnelState.state) {
+ case 'disconnected':
+ return HeaderBarStyle.error;
+ case 'connecting':
+ case 'connected':
+ return HeaderBarStyle.success;
+ case 'error':
+ return !tunnelState.details.blockFailure ? HeaderBarStyle.success : HeaderBarStyle.error;
+ case 'disconnecting':
+ switch (tunnelState.details) {
+ case 'block':
+ case 'reconnect':
+ return HeaderBarStyle.success;
+ case 'nothing':
+ return HeaderBarStyle.error;
+ }
+ }
+}
diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx
new file mode 100644
index 0000000000..9698321940
--- /dev/null
+++ b/gui/src/renderer/components/MainView.tsx
@@ -0,0 +1,28 @@
+import React, { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
+import { useHistory } from 'react-router';
+import { hasExpired } from '../../shared/account-expiry';
+import { IReduxState } from '../redux/store';
+import ConnectPage from '../containers/ConnectPage';
+import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer';
+
+export default function MainView() {
+ const history = useHistory();
+ const accountExpiry = useSelector((state: IReduxState) => state.account.expiry);
+ const accountHasExpired = accountExpiry && hasExpired(accountExpiry);
+ const isNewAccount = useSelector(
+ (state: IReduxState) =>
+ state.account.status.type === 'ok' && state.account.status.method === 'new_account',
+ );
+ const [showAccountExpired, setShowAccountExpired] = useState(isNewAccount || accountHasExpired);
+
+ useEffect(() => {
+ if (accountHasExpired) {
+ setShowAccountExpired(true);
+ } else if (showAccountExpired && !accountHasExpired) {
+ history.push('/main/time-added');
+ }
+ }, [showAccountExpired, accountHasExpired]);
+
+ return showAccountExpired ? <ExpiredAccountErrorViewContainer /> : <ConnectPage />;
+}
diff --git a/gui/src/renderer/components/RedeemVoucher.tsx b/gui/src/renderer/components/RedeemVoucher.tsx
index f8c0d19b83..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);
@@ -75,18 +73,21 @@ export function RedeemVoucherContainer(props: IRedeemVoucherProps) {
return;
}
+ const submitTimestamp = Date.now();
setSubmitting(true);
onSubmit?.();
const response = await submitVoucher(value);
+ // Show the spinner for at least half a second if it isn't successful.
+ const submitDuration = Date.now() - submitTimestamp;
+ if (response.type !== 'success' && submitDuration < 500) {
+ await new Promise((resolve) => setTimeout(resolve, 500 - submitDuration));
+ }
+
setSubmitting(false);
setResponse(response);
if (response.type === 'success') {
- setValue('');
- closeScheduler.schedule(() => {
- updateAccountExpiry(response.newExpiry);
- onSuccess?.();
- }, 1000);
+ onSuccess?.();
} else {
onFailure?.();
}
@@ -100,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';
@@ -122,6 +127,7 @@ export function RedeemVoucherInput() {
return (
<StyledInput
+ className={props.className}
allowedCharacters="[A-Z0-9]"
separator="-"
uppercaseOnly
@@ -137,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) {
@@ -147,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>