summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorHank <hank@mullvad.net>2022-10-11 15:44:12 +0200
committerHank <hank@mullvad.net>2022-10-13 13:48:17 +0200
commit2a8bd8b9cfbc06b41596c2333f8a39ecc8c987cf (patch)
treec388c59f51b5542eb045e9a64689b0caa04116cf /gui/src
parentf0821f443b5b91e7d197f3dc8980931cbb2d287b (diff)
downloadmullvadvpn-2a8bd8b9cfbc06b41596c2333f8a39ecc8c987cf.tar.xz
mullvadvpn-2a8bd8b9cfbc06b41596c2333f8a39ecc8c987cf.zip
Create functional component
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/app.tsx4
-rw-r--r--gui/src/renderer/components/Account.tsx337
-rw-r--r--gui/src/renderer/components/AppRouter.tsx4
-rw-r--r--gui/src/renderer/containers/AccountPage.tsx32
4 files changed, 176 insertions, 201 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index c991e07104..ca349b073b 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -367,7 +367,7 @@ export default class AppRenderer {
this.loginState = 'none';
};
- public async logout(transition = transitions.dismiss) {
+ public logout = async (transition = transitions.dismiss) => {
try {
this.history.reset(RoutePath.login, transition);
await IpcRendererEventChannel.account.logout();
@@ -375,7 +375,7 @@ export default class AppRenderer {
const error = e as Error;
log.info('Failed to logout: ', error.message);
}
- }
+ };
public leaveRevokedDevice = async () => {
await this.logout(transitions.pop);
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx
index 83d06fa290..d46b9e3a03 100644
--- a/gui/src/renderer/components/Account.tsx
+++ b/gui/src/renderer/components/Account.tsx
@@ -1,8 +1,12 @@
-import * as React from 'react';
+import { useCallback, useEffect, useState } from 'react';
+import { links } from '../../config.json';
import { formatDate, hasExpired } from '../../shared/account-expiry';
-import { DeviceState } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
+import { useAppContext } from '../context';
+import useActions from '../lib/actionsHook';
+import { useHistory } from '../lib/history';
+import account from '../redux/account/actions';
import { useSelector } from '../redux/store';
import {
AccountContainer,
@@ -25,185 +29,193 @@ import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import { RedeemVoucherButton } from './RedeemVoucher';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-interface IProps {
- isOffline: boolean;
- prepareLogout: () => void;
- cancelLogout: () => void;
- onLogout: () => void;
- onClose: () => void;
- onBuyMore: () => Promise<void>;
- updateAccountData: () => void;
- getDeviceState: () => Promise<DeviceState | undefined>;
-}
+type LogoutDialogStage = 'checking-ports' | 'confirm' | undefined;
-interface IState {
- logoutDialogVisible: boolean;
- logoutDialogStage?: 'checking-ports' | 'confirm';
-}
+export default function Account() {
+ const history = useHistory();
+ const [logoutDialogStage, setLogoutDialogStage] = useState<LogoutDialogStage>();
+ const [logoutDialogVisible, setLogoutDialogVisible] = useState(false);
+ const isOffline = useSelector((state) => state.connection.isBlocked);
-export default class Account extends React.Component<IProps, IState> {
- public state: IState = {
- logoutDialogVisible: false,
- };
+ const { logout, updateAccountData, openLinkWithAuth, getDeviceState } = useAppContext();
- public componentDidMount() {
- this.props.updateAccountData();
- }
+ const { cancelLogout, prepareLogout } = useActions(account);
- public render() {
- return (
- <BackAction action={this.props.onClose}>
- <Layout>
- <SettingsContainer>
- <NavigationBar>
- <NavigationItems>
- <TitleBarItem>
- {
- // TRANSLATORS: Title label in navigation bar
- messages.pgettext('account-view', 'Account')
- }
- </TitleBarItem>
- </NavigationItems>
- </NavigationBar>
+ const confirmLogout = useCallback(async () => {
+ onHideLogoutConfirmationDialog();
+ await logout();
+ }, []);
- <AccountContainer>
- <SettingsHeader>
- <HeaderTitle>{messages.pgettext('account-view', 'Account')}</HeaderTitle>
- </SettingsHeader>
+ const onCancelLogout = useCallback(() => {
+ cancelLogout();
+ onHideLogoutConfirmationDialog();
+ }, []);
- <AccountRows>
- <AccountRow>
- <AccountRowLabel>
- {messages.pgettext('device-management', 'Device name')}
- </AccountRowLabel>
- <DeviceNameRow />
- </AccountRow>
+ const onBuyMore = useCallback(async () => {
+ await openLinkWithAuth(links.purchase);
+ }, []);
- <AccountRow>
- <AccountRowLabel>
- {messages.pgettext('account-view', 'Account number')}
- </AccountRowLabel>
- <AccountNumberRow />
- </AccountRow>
+ const onHideLogoutConfirmationDialog = () => setLogoutDialogVisible(false);
- <AccountRow>
- <AccountRowLabel>
- {messages.pgettext('account-view', 'Paid until')}
- </AccountRowLabel>
- <AccountExpiryRow />
- </AccountRow>
- </AccountRows>
+ const onTryLogout = useCallback(async () => {
+ setLogoutDialogVisible(true);
+ setLogoutDialogStage('checking-ports');
+ prepareLogout();
- <Footer>
- <AppButton.ButtonGroup>
- <AppButton.BlockingButton
- disabled={this.props.isOffline}
- onClick={this.props.onBuyMore}>
- <AriaDescriptionGroup>
- <AriaDescribed>
- <AppButton.GreenButton>
- <AppButton.Label>{messages.gettext('Buy more credit')}</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 deviceState = await getDeviceState();
- <RedeemVoucherButton />
+ if (
+ deviceState?.type === 'logged in' &&
+ deviceState.accountAndDevice.device?.ports !== undefined &&
+ deviceState.accountAndDevice.device.ports.length > 0
+ ) {
+ setLogoutDialogStage('confirm');
+ } else {
+ await confirmLogout();
+ }
+ }, []);
- <AppButton.RedButton onClick={this.onTryLogout}>
- {messages.pgettext('account-view', 'Log out')}
- </AppButton.RedButton>
- </AppButton.ButtonGroup>
- </Footer>
- </AccountContainer>
- </SettingsContainer>
+ useEffect(() => {
+ updateAccountData();
+ }, []);
- {this.renderLogoutDialog()}
- </Layout>
- </BackAction>
- );
- }
+ return (
+ <BackAction action={history.pop}>
+ <Layout>
+ <SettingsContainer>
+ <NavigationBar>
+ <NavigationItems>
+ <TitleBarItem>
+ {
+ // TRANSLATORS: Title label in navigation bar
+ messages.pgettext('account-view', 'Account')
+ }
+ </TitleBarItem>
+ </NavigationItems>
+ </NavigationBar>
- private renderLogoutDialog() {
- const modalType =
- this.state.logoutDialogStage === 'checking-ports' ? undefined : ModalAlertType.warning;
+ <AccountContainer>
+ <SettingsHeader>
+ <HeaderTitle>{messages.pgettext('account-view', 'Account')}</HeaderTitle>
+ </SettingsHeader>
- const message =
- this.state.logoutDialogStage === 'checking-ports' ? (
- <StyledSpinnerContainer>
- <ImageView source="icon-spinner" width={60} height={60} />
- </StyledSpinnerContainer>
- ) : (
- <ModalMessage>
- {
- // TRANSLATORS: This is is a further explanation of what happens when logging out.
- messages.pgettext(
- 'device-management',
- 'The ports forwarded to this device will be deleted if you log out.',
- )
- }
- </ModalMessage>
- );
+ <AccountRows>
+ <AccountRow>
+ <AccountRowLabel>
+ {messages.pgettext('device-management', 'Device name')}
+ </AccountRowLabel>
+ <DeviceNameRow />
+ </AccountRow>
- const buttons =
- this.state.logoutDialogStage === 'checking-ports'
- ? []
- : [
- <AppButton.RedButton key="logout" onClick={this.confirmLogout}>
- {
- // TRANSLATORS: Confirmation button when logging out
- messages.pgettext('device-management', 'Log out anyway')
- }
- </AppButton.RedButton>,
- <AppButton.BlueButton key="back" onClick={this.cancelLogout}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
- ];
+ <AccountRow>
+ <AccountRowLabel>
+ {messages.pgettext('account-view', 'Account number')}
+ </AccountRowLabel>
+ <AccountNumberRow />
+ </AccountRow>
- return (
- <ModalAlert isOpen={this.state.logoutDialogVisible} type={modalType} buttons={buttons}>
- {message}
- </ModalAlert>
- );
- }
+ <AccountRow>
+ <AccountRowLabel>{messages.pgettext('account-view', 'Paid until')}</AccountRowLabel>
+ <AccountExpiryRow />
+ </AccountRow>
+ </AccountRows>
- private onTryLogout = async () => {
- this.setState({ logoutDialogVisible: true, logoutDialogStage: 'checking-ports' });
- this.props.prepareLogout();
+ <Footer>
+ <AppButton.ButtonGroup>
+ <AppButton.BlockingButton disabled={isOffline} onClick={onBuyMore}>
+ <AriaDescriptionGroup>
+ <AriaDescribed>
+ <AppButton.GreenButton>
+ <AppButton.Label>{messages.gettext('Buy more credit')}</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 deviceState = await this.props.getDeviceState();
- if (
- deviceState?.type === 'logged in' &&
- deviceState.accountAndDevice.device?.ports !== undefined &&
- deviceState.accountAndDevice.device.ports.length > 0
- ) {
- this.setState({ logoutDialogStage: 'confirm' });
- } else {
- this.confirmLogout();
- }
- };
+ <RedeemVoucherButton />
- private confirmLogout = () => {
- this.onHideLogoutConfirmationDialog();
- this.props.onLogout();
- };
+ <AppButton.RedButton onClick={onTryLogout}>
+ {messages.pgettext('account-view', 'Log out')}
+ </AppButton.RedButton>
+ </AppButton.ButtonGroup>
+ </Footer>
+ </AccountContainer>
+ </SettingsContainer>
- private cancelLogout = () => {
- this.props.cancelLogout();
- this.onHideLogoutConfirmationDialog();
- };
+ <LogoutDialog
+ logoutDialogStage={logoutDialogStage}
+ logoutDialogVisible={logoutDialogVisible}
+ confirmLogout={confirmLogout}
+ cancelLogout={onCancelLogout}
+ />
+ </Layout>
+ </BackAction>
+ );
+}
+
+type LogoutDialogProps = {
+ logoutDialogStage: LogoutDialogStage;
+ logoutDialogVisible: boolean;
+ confirmLogout: () => void;
+ cancelLogout: () => void;
+};
+
+function LogoutDialog({
+ logoutDialogStage,
+ logoutDialogVisible,
+ confirmLogout,
+ cancelLogout,
+}: LogoutDialogProps) {
+ const modalType = logoutDialogStage === 'checking-ports' ? undefined : ModalAlertType.warning;
+ const message =
+ logoutDialogStage === 'checking-ports' ? (
+ <StyledSpinnerContainer>
+ <ImageView source="icon-spinner" width={60} height={60} />
+ </StyledSpinnerContainer>
+ ) : (
+ <ModalMessage>
+ {
+ // TRANSLATORS: This is a further explanation of what happens when logging out.
+ messages.pgettext(
+ 'device-management',
+ 'The ports forwarded to this device will be deleted if you log out.',
+ )
+ }
+ </ModalMessage>
+ );
- private onHideLogoutConfirmationDialog = () => {
- this.setState({ logoutDialogVisible: false });
- };
+ const buttons =
+ logoutDialogStage === 'checking-ports'
+ ? []
+ : [
+ <AppButton.RedButton key="logout" onClick={confirmLogout}>
+ {
+ // TRANSLATORS: Confirmation button when logging out
+ messages.pgettext('device-management', 'Log out anyway')
+ }
+ </AppButton.RedButton>,
+ <AppButton.BlueButton key="back" onClick={cancelLogout}>
+ {messages.gettext('Back')}
+ </AppButton.BlueButton>,
+ ];
+ return (
+ <ModalAlert isOpen={logoutDialogVisible} type={modalType} buttons={buttons}>
+ {message}
+ </ModalAlert>
+ );
+}
+
+function DeviceNameRow() {
+ const deviceName = useSelector((state) => state.account.deviceName);
+ return <DeviceRowValue>{deviceName}</DeviceRowValue>;
}
function AccountNumberRow() {
@@ -217,11 +229,6 @@ function AccountExpiryRow() {
return <FormattedAccountExpiry expiry={accountExpiry} locale={expiryLocale} />;
}
-function DeviceNameRow() {
- const deviceName = useSelector((state) => state.account.deviceName);
- return <DeviceRowValue>{deviceName}</DeviceRowValue>;
-}
-
function FormattedAccountExpiry(props: { expiry?: string; locale: string }) {
if (props.expiry) {
if (hasExpired(props.expiry)) {
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index 36e7ea2239..1f0d36fc24 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -1,13 +1,13 @@
import { createRef, useCallback, useEffect, useState } from 'react';
import { Route, Switch } from 'react-router';
-import AccountPage from '../containers/AccountPage';
import LoginPage from '../containers/LoginPage';
import ProblemReportPage from '../containers/ProblemReportPage';
import SelectLocationPage from '../containers/SelectLocationPage';
import { useAppContext } from '../context';
import { ITransitionSpecification, transitions, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
+import Account from './Account';
import { DeviceRevokedView } from './DeviceRevokedView';
import {
SetupFinished,
@@ -71,7 +71,7 @@ export default function AppRouter() {
<Route exact path={RoutePath.setupFinished} component={SetupFinished} />
<Route exact path={RoutePath.settings} component={Settings} />
<Route exact path={RoutePath.selectLanguage} component={SelectLanguage} />
- <Route exact path={RoutePath.accountSettings} component={AccountPage} />
+ <Route exact path={RoutePath.accountSettings} component={Account} />
<Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
diff --git a/gui/src/renderer/containers/AccountPage.tsx b/gui/src/renderer/containers/AccountPage.tsx
deleted file mode 100644
index 359d33b5c0..0000000000
--- a/gui/src/renderer/containers/AccountPage.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
-
-import { links } from '../../config.json';
-import Account from '../components/Account';
-import withAppContext, { IAppContext } from '../context';
-import { IHistoryProps, withHistory } from '../lib/history';
-import accountActions from '../redux/account/actions';
-import { IReduxState, ReduxDispatch } from '../redux/store';
-
-const mapStateToProps = (state: IReduxState) => ({
- isOffline: state.connection.isBlocked,
-});
-const mapDispatchToProps = (dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
- const account = bindActionCreators(accountActions, dispatch);
-
- return {
- prepareLogout: () => account.prepareLogout(),
- cancelLogout: () => account.cancelLogout(),
- onLogout: () => {
- void props.app.logout();
- },
- onClose: () => {
- props.history.pop();
- },
- onBuyMore: () => props.app.openLinkWithAuth(links.purchase),
- updateAccountData: () => props.app.updateAccountData(),
- getDeviceState: () => props.app.getDeviceState(),
- };
-};
-
-export default withAppContext(withHistory(connect(mapStateToProps, mapDispatchToProps)(Account)));