summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorHank <hank@mullvad.net>2022-10-11 18:36:03 +0200
committerHank <hank@mullvad.net>2022-10-17 07:49:47 +0200
commit11097c1d42572d7389d8a897fc098d58b74f51a9 (patch)
treedb08dc9ad623b9a3c6a82b6596c94e85df06c9ad /gui/src
parent169121cd2a078fd89abb01672464658c47fe48f5 (diff)
downloadmullvadvpn-11097c1d42572d7389d8a897fc098d58b74f51a9.tar.xz
mullvadvpn-11097c1d42572d7389d8a897fc098d58b74f51a9.zip
Make ExpiredAccountErrorView.tsx a functional component
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx458
-rw-r--r--gui/src/renderer/components/MainView.tsx4
2 files changed, 270 insertions, 192 deletions
diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
index 2d4fcdb216..7c2e8ead51 100644
--- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx
+++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx
@@ -1,10 +1,15 @@
-import * as React from 'react';
+import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react';
import { sprintf } from 'sprintf-js';
import { links } from '../../config.json';
-import { AccountToken, TunnelState } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
-import { LoginState } from '../redux/account/reducers';
+import log from '../../shared/logging';
+import { useAppContext } from '../context';
+import { useHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
+import { IAccountReduxState } from '../redux/account/reducers';
+import { IConnectionReduxState } from '../redux/connection/reducers';
+import { useSelector } from '../redux/store';
import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import * as Cell from './cell';
@@ -26,220 +31,242 @@ import ImageView from './ImageView';
import { Footer, Layout } from './Layout';
import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
-export enum RecoveryAction {
+enum RecoveryAction {
openBrowser,
disconnect,
disableBlockedWhenDisconnected,
}
-interface IExpiredAccountErrorViewProps {
- isBlocked: boolean;
- blockWhenDisconnected: boolean;
- accountToken?: AccountToken;
- loginState: LoginState;
- tunnelState: TunnelState;
- onExternalLinkWithAuth: (url: string) => Promise<void>;
- onDisconnect: () => Promise<void>;
- setBlockWhenDisconnected: (value: boolean) => void;
- navigateToRedeemVoucher: () => void;
+export default function ExpiredAccountErrorView() {
+ return (
+ <ExpiredAccountContextProvider>
+ <ExpiredAccountErrorViewComponent />
+ </ExpiredAccountContextProvider>
+ );
}
-interface IExpiredAccountErrorViewState {
- showBlockWhenDisconnectedAlert: boolean;
+function ExpiredAccountErrorViewComponent() {
+ const { account, connection, getRecoveryAction, isNewAccount } = useExpiredAccountContext();
+
+ const history = useHistory();
+ const { disconnectTunnel } = useAppContext();
+
+ const headerBarStyle = useMemo(() => {
+ return isNewAccount ? HeaderBarStyle.default : calculateHeaderBarStyle(connection.status);
+ }, [account.status, connection.status]);
+
+ const onDisconnect = useCallback(async () => {
+ try {
+ await disconnectTunnel();
+ } catch (e) {
+ const error = e as Error;
+ log.error(`Failed to disconnect the tunnel: ${error.message}`);
+ }
+ }, []);
+
+ const navigateToRedeemVoucher = useCallback(() => {
+ history.push(RoutePath.redeemVoucher);
+ }, [history.push]);
+
+ return (
+ <Layout>
+ <StyledHeader barStyle={headerBarStyle} />
+ <StyledCustomScrollbars fillContainer>
+ <StyledContainer>
+ <StyledBody>{isNewAccount ? <WelcomeView /> : <Content />}</StyledBody>
+
+ <Footer>
+ <AppButton.ButtonGroup>
+ {getRecoveryAction() === RecoveryAction.disconnect && (
+ <AppButton.BlockingButton onClick={onDisconnect}>
+ <AppButton.RedButton>
+ {messages.pgettext('connect-view', 'Disconnect')}
+ </AppButton.RedButton>
+ </AppButton.BlockingButton>
+ )}
+
+ <ExternalPaymentButton />
+
+ <AppButton.GreenButton onClick={navigateToRedeemVoucher}>
+ {messages.pgettext('connect-view', 'Redeem voucher')}
+ </AppButton.GreenButton>
+ </AppButton.ButtonGroup>
+ </Footer>
+
+ <BlockWhenDisconnectedAlert />
+ </StyledContainer>
+ </StyledCustomScrollbars>
+ </Layout>
+ );
}
-export default class ExpiredAccountErrorView extends React.Component<
- IExpiredAccountErrorViewProps,
- IExpiredAccountErrorViewState
-> {
- public state: IExpiredAccountErrorViewState = {
- showBlockWhenDisconnectedAlert: false,
- };
+function WelcomeView() {
+ const { account, getRecoveryActionMessage } = useExpiredAccountContext();
- public render() {
- const headerBarStyle =
- this.props.loginState.type === 'ok' && this.props.loginState.method === 'new_account'
- ? HeaderBarStyle.default
- : calculateHeaderBarStyle(this.props.tunnelState);
+ return (
+ <>
+ <StyledTitle>{messages.pgettext('connect-view', 'Congrats!')}</StyledTitle>
+ <StyledAccountTokenMessage>
+ {messages.pgettext('connect-view', 'Here’s your account number. Save it!')}
+ <StyledAccountTokenContainer>
+ <StyledAccountTokenLabel accountToken={account.accountToken || ''} obscureValue={false} />
+ </StyledAccountTokenContainer>
+ </StyledAccountTokenMessage>
- return (
- <Layout>
- <StyledHeader barStyle={headerBarStyle} />
- <StyledCustomScrollbars fillContainer>
- <StyledContainer>
- <StyledBody>{this.renderContent()}</StyledBody>
+ <StyledMessage>
+ {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: getRecoveryActionMessage(),
+ })}
+ </StyledMessage>
+ </>
+ );
+}
- <Footer>
- <AppButton.ButtonGroup>
- {this.getRecoveryAction() === RecoveryAction.disconnect && (
- <AppButton.BlockingButton onClick={this.props.onDisconnect}>
- <AppButton.RedButton>
- {messages.pgettext('connect-view', 'Disconnect')}
- </AppButton.RedButton>
- </AppButton.BlockingButton>
- )}
+function Content() {
+ const { getRecoveryActionMessage } = useExpiredAccountContext();
- {this.renderExternalPaymentButton()}
+ return (
+ <>
+ <StyledStatusIcon>
+ <ImageView source="icon-fail" height={60} width={60} />
+ </StyledStatusIcon>
+ <StyledTitle>{messages.pgettext('connect-view', 'Out of time')}</StyledTitle>
+ <StyledMessage>
+ {sprintf('%(introduction)s %(recoveryMessage)s', {
+ introduction: messages.pgettext(
+ 'connect-view',
+ 'You have no more VPN time left on this account.',
+ ),
+ recoveryMessage: getRecoveryActionMessage(),
+ })}
+ </StyledMessage>
+ </>
+ );
+}
- <AppButton.GreenButton onClick={this.props.navigateToRedeemVoucher}>
- {messages.pgettext('connect-view', 'Redeem voucher')}
- </AppButton.GreenButton>
- </AppButton.ButtonGroup>
- </Footer>
+function ExternalPaymentButton() {
+ const { getRecoveryAction, isNewAccount, onOpenExternalPayment } = useExpiredAccountContext();
- {this.renderBlockWhenDisconnectedAlert()}
- </StyledContainer>
- </StyledCustomScrollbars>
- </Layout>
- );
- }
+ const buttonText = isNewAccount
+ ? messages.gettext('Buy credit')
+ : messages.gettext('Buy more credit');
- private renderContent() {
- if (this.isNewAccount()) {
- return this.renderWelcomeView();
- }
+ return (
+ <AppButton.BlockingButton
+ disabled={getRecoveryAction() === RecoveryAction.disconnect}
+ onClick={onOpenExternalPayment}>
+ <AriaDescriptionGroup>
+ <AriaDescribed>
+ <AppButton.GreenButton>
+ <AppButton.Label>{buttonText}</AppButton.Label>
+ <AriaDescription>
+ <AppButton.Icon
+ source="icon-extLink"
+ height={16}
+ width={16}
+ aria-label={messages.pgettext('accessibility', 'Opens externally')}
+ />
+ </AriaDescription>
+ </AppButton.GreenButton>
+ </AriaDescribed>
+ </AriaDescriptionGroup>
+ </AppButton.BlockingButton>
+ );
+}
- return (
- <>
- <StyledStatusIcon>
- <ImageView source="icon-fail" height={60} width={60} />
- </StyledStatusIcon>
- <StyledTitle>{messages.pgettext('connect-view', 'Out of time')}</StyledTitle>
- <StyledMessage>
- {sprintf('%(introduction)s %(recoveryMessage)s', {
- introduction: messages.pgettext(
- 'connect-view',
- 'You have no more VPN time left on this account.',
- ),
- recoveryMessage: this.getRecoveryActionMessage(),
- })}
- </StyledMessage>
- </>
- );
- }
+function BlockWhenDisconnectedAlert() {
+ const {
+ blockWhenDisconnected,
+ setBlockWhenDisconnected,
+ showBlockWhenDisconnectedAlert,
+ setShowBlockWhenDisconnectedAlert,
+ } = useExpiredAccountContext();
- private renderWelcomeView() {
- return (
- <>
- <StyledTitle>{messages.pgettext('connect-view', 'Congrats!')}</StyledTitle>
- <StyledAccountTokenMessage>
- {messages.pgettext('connect-view', 'Here’s your account number. Save it!')}
- <StyledAccountTokenContainer>
- <StyledAccountTokenLabel
- accountToken={this.props.accountToken || ''}
- obscureValue={false}
- />
- </StyledAccountTokenContainer>
- </StyledAccountTokenMessage>
+ const onCloseBlockWhenDisconnectedInstructions = useCallback(() => {
+ setShowBlockWhenDisconnectedAlert(false);
+ }, []);
- <StyledMessage>
- {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(),
- })}
- </StyledMessage>
- </>
- );
- }
+ const onChange = useCallback(async (blockWhenDisconnected: boolean) => {
+ try {
+ await setBlockWhenDisconnected(blockWhenDisconnected);
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to update block when disconnected', error.message);
+ }
+ }, []);
- private getRecoveryActionMessage() {
- switch (this.getRecoveryAction()) {
- case RecoveryAction.openBrowser:
- case RecoveryAction.disableBlockedWhenDisconnected:
- return messages.pgettext(
+ return (
+ <ModalAlert
+ isOpen={showBlockWhenDisconnectedAlert}
+ type={ModalAlertType.caution}
+ buttons={[
+ <AppButton.BlueButton key="cancel" onClick={onCloseBlockWhenDisconnectedInstructions}>
+ {messages.gettext('Close')}
+ </AppButton.BlueButton>,
+ ]}
+ close={onCloseBlockWhenDisconnectedInstructions}>
+ <ModalMessage>
+ {messages.pgettext(
'connect-view',
- 'Either buy credit on our website or redeem a voucher.',
- );
- case RecoveryAction.disconnect:
- return messages.pgettext(
+ 'You need to disable "Lockdown mode" in order to access the Internet to add time.',
+ )}
+ </ModalMessage>
+ <ModalMessage>
+ {messages.pgettext(
'connect-view',
- 'To add more, you will need to disconnect and access the Internet with an unsecure connection.',
- );
- }
- }
+ 'Remember, turning it off will allow network traffic while the VPN is disconnected until you turn it back on under Advanced settings.',
+ )}
+ </ModalMessage>
+ <StyledModalCellContainer>
+ <Cell.Label>{messages.pgettext('vpn-settings-view', 'Lockdown mode')}</Cell.Label>
+ <Cell.Switch isOn={blockWhenDisconnected} onChange={onChange} />
+ </StyledModalCellContainer>
+ </ModalAlert>
+ );
+}
- private renderExternalPaymentButton() {
- const buttonText = this.isNewAccount()
- ? messages.gettext('Buy credit')
- : messages.gettext('Buy more credit');
+type ExpiredAccountContextType = {
+ account: IAccountReduxState;
+ blockWhenDisconnected: boolean;
+ connection: IConnectionReduxState;
+ getRecoveryAction: () => RecoveryAction;
+ getRecoveryActionMessage: () => string;
+ isNewAccount: boolean;
+ onOpenExternalPayment: () => Promise<void>;
+ setBlockWhenDisconnected: (val: boolean) => Promise<void>;
+ setShowBlockWhenDisconnectedAlert: (val: boolean) => void;
+ showBlockWhenDisconnectedAlert: boolean;
+};
- return (
- <AppButton.BlockingButton
- disabled={this.getRecoveryAction() === RecoveryAction.disconnect}
- onClick={this.onOpenExternalPayment}>
- <AriaDescriptionGroup>
- <AriaDescribed>
- <AppButton.GreenButton>
- <AppButton.Label>{buttonText}</AppButton.Label>
- <AriaDescription>
- <AppButton.Icon
- source="icon-extLink"
- height={16}
- width={16}
- aria-label={messages.pgettext('accessibility', 'Opens externally')}
- />
- </AriaDescription>
- </AppButton.GreenButton>
- </AriaDescribed>
- </AriaDescriptionGroup>
- </AppButton.BlockingButton>
- );
- }
+const ExpiredAccountContext = createContext<ExpiredAccountContextType | undefined>(undefined);
- private renderBlockWhenDisconnectedAlert() {
- return (
- <ModalAlert
- isOpen={this.state.showBlockWhenDisconnectedAlert}
- type={ModalAlertType.caution}
- buttons={[
- <AppButton.BlueButton
- key="cancel"
- onClick={this.onCloseBlockWhenDisconnectedInstructions}>
- {messages.gettext('Close')}
- </AppButton.BlueButton>,
- ]}
- close={this.onCloseBlockWhenDisconnectedInstructions}>
- <ModalMessage>
- {messages.pgettext(
- 'connect-view',
- 'You need to disable "Lockdown mode" in order to access the Internet to add time.',
- )}
- </ModalMessage>
- <ModalMessage>
- {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.',
- )}
- </ModalMessage>
- <StyledModalCellContainer>
- <Cell.Label>{messages.pgettext('vpn-settings-view', 'Lockdown mode')}</Cell.Label>
- <Cell.Switch
- isOn={this.props.blockWhenDisconnected}
- onChange={this.props.setBlockWhenDisconnected}
- />
- </StyledModalCellContainer>
- </ModalAlert>
- );
- }
+const ExpiredAccountContextProvider = ({ children }: { children: ReactNode }) => {
+ const account = useSelector((state) => state.account);
+ const blockWhenDisconnected = useSelector((state) => state.settings.blockWhenDisconnected);
+ const connection = useSelector((state) => state.connection);
+ const { setBlockWhenDisconnected, openLinkWithAuth } = useAppContext();
- private isNewAccount() {
- return this.props.loginState.type === 'ok' && this.props.loginState.method === 'new_account';
- }
+ const isBlocked = connection.isBlocked;
+ const [showBlockWhenDisconnectedAlert, setShowBlockWhenDisconnectedAlert] = useState(false);
+
+ const isNewAccount = useMemo(
+ () => account.status.type === 'ok' && account.status.method === 'new_account',
+ [account.status],
+ );
- private onOpenExternalPayment = async (): Promise<void> => {
- if (this.getRecoveryAction() === RecoveryAction.disableBlockedWhenDisconnected) {
- this.setState({ showBlockWhenDisconnectedAlert: true });
+ const onOpenExternalPayment = async () => {
+ if (getRecoveryAction() === RecoveryAction.disableBlockedWhenDisconnected) {
+ setShowBlockWhenDisconnectedAlert(true);
} else {
- await this.props.onExternalLinkWithAuth(links.purchase);
+ await openLinkWithAuth(links.purchase);
}
};
- private getRecoveryAction() {
- const { blockWhenDisconnected, isBlocked } = this.props;
-
+ const getRecoveryAction = () => {
if (blockWhenDisconnected && isBlocked) {
return RecoveryAction.disableBlockedWhenDisconnected;
} else if (!blockWhenDisconnected && isBlocked) {
@@ -247,9 +274,60 @@ export default class ExpiredAccountErrorView extends React.Component<
} else {
return RecoveryAction.openBrowser;
}
- }
+ };
- private onCloseBlockWhenDisconnectedInstructions = () => {
- this.setState({ showBlockWhenDisconnectedAlert: false });
+ const getRecoveryActionMessage = () => {
+ switch (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 unsecure connection.',
+ );
+ }
};
-}
+
+ const value: ExpiredAccountContextType = useMemo(
+ () => ({
+ account,
+ blockWhenDisconnected,
+ connection,
+ getRecoveryAction,
+ getRecoveryActionMessage,
+ isNewAccount,
+ onOpenExternalPayment,
+ setBlockWhenDisconnected,
+ setShowBlockWhenDisconnectedAlert,
+ showBlockWhenDisconnectedAlert,
+ }),
+ [
+ account,
+ blockWhenDisconnected,
+ connection,
+ getRecoveryAction,
+ getRecoveryActionMessage,
+ isNewAccount,
+ onOpenExternalPayment,
+ setBlockWhenDisconnected,
+ setShowBlockWhenDisconnectedAlert,
+ showBlockWhenDisconnectedAlert,
+ ],
+ );
+ return <ExpiredAccountContext.Provider value={value}>{children}</ExpiredAccountContext.Provider>;
+};
+
+const useExpiredAccountContext = () => {
+ const context = useContext(ExpiredAccountContext);
+ if (!context) {
+ throw new Error(
+ 'useExpiredAccountContext must be used within an ExpiredAccountContextProvider',
+ );
+ }
+
+ return context;
+};
diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx
index 33f5f92ad5..cc2419ddfd 100644
--- a/gui/src/renderer/components/MainView.tsx
+++ b/gui/src/renderer/components/MainView.tsx
@@ -2,10 +2,10 @@ import { useEffect, useState } from 'react';
import { hasExpired } from '../../shared/account-expiry';
import Connect from '../components/Connect';
-import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer';
import { useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useSelector } from '../redux/store';
+import ExpiredAccountErrorView from './ExpiredAccountErrorView';
type ExpiryData = { show: false } | { show: true; expiry: string | undefined };
@@ -37,7 +37,7 @@ export default function MainView() {
}, [showAccountExpired, accountHasExpired]);
if (showAccountExpired.show) {
- return <ExpiredAccountErrorViewContainer />;
+ return <ExpiredAccountErrorView />;
} else {
return <Connect />;
}