summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2021-07-22 14:43:17 +0200
committerOskar Nyberg <oskar@mullvad.net>2021-07-22 14:43:17 +0200
commitf8e47dc57b4821e119104ba1c1223e32085d7174 (patch)
tree365c04dad43d9de5b6f669e2d5bbf22e48a8b0b8 /gui/src
parent43a5f956fbc8fc137367fb6676c7c34d3e8f93cf (diff)
parentbefbb4b3aee6b96eb248cdc883a4f42d3901bacc (diff)
downloadmullvadvpn-f8e47dc57b4821e119104ba1c1223e32085d7174.tar.xz
mullvadvpn-f8e47dc57b4821e119104ba1c1223e32085d7174.zip
Merge branch 'add-routes-enum'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/renderer/app.tsx13
-rw-r--r--gui/src/renderer/components/AppRouter.tsx113
-rw-r--r--gui/src/renderer/components/ExpiredAccountAddTime.tsx7
-rw-r--r--gui/src/renderer/components/HeaderBar.tsx3
-rw-r--r--gui/src/renderer/components/MainView.tsx3
-rw-r--r--gui/src/renderer/containers/AdvancedSettingsPage.tsx5
-rw-r--r--gui/src/renderer/containers/ConnectPage.tsx3
-rw-r--r--gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx3
-rw-r--r--gui/src/renderer/containers/SettingsPage.tsx11
-rw-r--r--gui/src/renderer/lib/history.tsx15
-rw-r--r--gui/src/renderer/lib/routes.ts18
-rw-r--r--gui/src/renderer/routes.tsx120
12 files changed, 172 insertions, 142 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index eb68ed963b..a92ffb2e8a 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -3,9 +3,9 @@ import { Provider } from 'react-redux';
import { Router } from 'react-router';
import { bindActionCreators } from 'redux';
+import AppRouter from './components/AppRouter';
import ErrorBoundary from './components/ErrorBoundary';
import { AppContext } from './context';
-import AppRoutes from './routes';
import accountActions from './redux/account/actions';
import connectionActions from './redux/connection/actions';
@@ -46,6 +46,7 @@ import {
} from '../shared/daemon-rpc-types';
import { LogLevel } from '../shared/logging-types';
import IpcOutput from './lib/logging';
+import { RoutePath } from './lib/routes';
// This function wraps all IPC calls to catch errors and then rethrow them without the
// "Uncaught Error:" prefix that's added by Electron.
@@ -264,9 +265,9 @@ export default class AppRenderer {
return (
<AppContext.Provider value={{ app: this }}>
<Provider store={this.reduxStore}>
- <Router history={this.history}>
+ <Router history={this.history.asHistory}>
<ErrorBoundary>
- <AppRoutes />
+ <AppRouter />
</ErrorBoundary>
</Router>
</Provider>
@@ -686,11 +687,11 @@ export default class AppRenderer {
}
}
- private getNavigationBase(connectedToDaemon: boolean, accountToken?: string): string {
+ private getNavigationBase(connectedToDaemon: boolean, accountToken?: string): RoutePath {
if (connectedToDaemon) {
- return accountToken ? '/main' : '/login';
+ return accountToken ? RoutePath.main : RoutePath.login;
} else {
- return '/';
+ return RoutePath.launch;
}
}
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
new file mode 100644
index 0000000000..1816541b49
--- /dev/null
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -0,0 +1,113 @@
+import { Action } from 'history';
+import * as React from 'react';
+import { Route, Switch } from 'react-router';
+import Launch from './Launch';
+import KeyboardNavigation from './KeyboardNavigation';
+import MainView from './MainView';
+import Focus, { IFocusHandle } from './Focus';
+import SplitTunnelingSettings from './SplitTunnelingSettings';
+import TransitionContainer, { TransitionView } from './TransitionContainer';
+import AccountPage from '../containers/AccountPage';
+import AdvancedSettingsPage from '../containers/AdvancedSettingsPage';
+import LoginPage from '../containers/LoginPage';
+import PlatformWindowContainer from '../containers/PlatformWindowContainer';
+import PreferencesPage from '../containers/PreferencesPage';
+import SelectLanguagePage from '../containers/SelectLanguagePage';
+import SelectLocationPage from '../containers/SelectLocationPage';
+import SettingsPage from '../containers/SettingsPage';
+import SupportPage from '../containers/SupportPage';
+import WireguardKeysPage from '../containers/WireguardKeysPage';
+import { IHistoryProps, ITransitionSpecification, transitions, withHistory } from '../lib/history';
+import {
+ SetupFinished,
+ TimeAdded,
+ VoucherInput,
+ VoucherVerificationSuccess,
+} from './ExpiredAccountAddTime';
+import { RoutePath } from '../lib/routes';
+
+interface IAppRoutesState {
+ currentLocation: IHistoryProps['history']['location'];
+ transition: ITransitionSpecification;
+ action?: Action;
+}
+
+class AppRouter extends React.Component<IHistoryProps, IAppRoutesState> {
+ private unobserveHistory?: () => void;
+
+ private focusRef = React.createRef<IFocusHandle>();
+
+ constructor(props: IHistoryProps) {
+ super(props);
+
+ this.state = {
+ currentLocation: props.history.location,
+ transition: transitions.none,
+ };
+ }
+
+ public componentDidMount() {
+ // React throttles updates, so it's impossible to capture the intermediate navigation without
+ // listening to the history directly.
+ this.unobserveHistory = this.props.history.listen((location, action, transition) => {
+ this.setState({
+ currentLocation: location,
+ transition,
+ action,
+ });
+ });
+ }
+
+ public componentWillUnmount() {
+ if (this.unobserveHistory) {
+ this.unobserveHistory();
+ }
+ }
+
+ public render() {
+ const location = this.state.currentLocation;
+
+ return (
+ <PlatformWindowContainer>
+ <KeyboardNavigation>
+ <Focus ref={this.focusRef}>
+ <TransitionContainer onTransitionEnd={this.onNavigation} {...this.state.transition}>
+ <TransitionView viewId={location.key || ''}>
+ <Switch key={location.key} location={location}>
+ <Route exact path={RoutePath.launch} component={Launch} />
+ <Route exact path={RoutePath.login} component={LoginPage} />
+ <Route exact path={RoutePath.main} component={MainView} />
+ <Route exact path={RoutePath.redeemVoucher} component={VoucherInput} />
+ <Route
+ exact
+ path={RoutePath.voucherSuccess}
+ component={VoucherVerificationSuccess}
+ />
+ <Route exact path={RoutePath.timeAdded} component={TimeAdded} />
+ <Route exact path={RoutePath.setupFinished} component={SetupFinished} />
+ <Route exact path={RoutePath.settings} component={SettingsPage} />
+ <Route exact path={RoutePath.selectLanguage} component={SelectLanguagePage} />
+ <Route exact path={RoutePath.accountSettings} component={AccountPage} />
+ <Route exact path={RoutePath.preferences} component={PreferencesPage} />
+ <Route exact path={RoutePath.advancedSettings} component={AdvancedSettingsPage} />
+ <Route exact path={RoutePath.wireguardKeys} component={WireguardKeysPage} />
+ <Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
+ <Route exact path={RoutePath.support} component={SupportPage} />
+ <Route exact path={RoutePath.selectLocation} component={SelectLocationPage} />
+ </Switch>
+ </TransitionView>
+ </TransitionContainer>
+ </Focus>
+ </KeyboardNavigation>
+ </PlatformWindowContainer>
+ );
+ }
+
+ private onNavigation = () => {
+ this.focusRef.current?.resetFocus();
+ };
+}
+
+const AppRoutesWithRouter = withHistory(AppRouter);
+
+export default AppRoutesWithRouter;
diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
index 24c335a551..43c2a4903f 100644
--- a/gui/src/renderer/components/ExpiredAccountAddTime.tsx
+++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
@@ -8,6 +8,7 @@ import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
import { transitions, useHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
import account from '../redux/account/actions';
import { IReduxState } from '../redux/store';
import * as AppButton from './AppButton';
@@ -82,7 +83,7 @@ export function VoucherInput() {
const history = useHistory();
const onSuccess = useCallback(() => {
- history.push('/main/voucher/success');
+ history.push(RoutePath.voucherSuccess);
}, [history]);
const navigateBack = useCallback(() => {
@@ -138,7 +139,7 @@ export function TimeAdded(props: ITimeAddedProps) {
const navigateToSetupFinished = useCallback(() => {
if (isNewAccount) {
- history.push('/main/setup-finished');
+ history.push(RoutePath.setupFinished);
} else {
finish();
}
@@ -263,7 +264,7 @@ function useFinishedCallback() {
loggedIn();
}
- history.reset('/main', undefined, transitions.push);
+ history.reset(RoutePath.main, undefined, transitions.push);
}, [isNewAccount, loggedIn, history]);
return callback;
diff --git a/gui/src/renderer/components/HeaderBar.tsx b/gui/src/renderer/components/HeaderBar.tsx
index 35d2c2083a..42b074eaa3 100644
--- a/gui/src/renderer/components/HeaderBar.tsx
+++ b/gui/src/renderer/components/HeaderBar.tsx
@@ -9,6 +9,7 @@ import { IReduxState } from '../redux/store';
import { FocusFallback } from './Focus';
import { sourceSansPro } from './common-styles';
import ImageView from './ImageView';
+import { RoutePath } from '../lib/routes';
export enum HeaderBarStyle {
default = 'default',
@@ -103,7 +104,7 @@ export function HeaderBarSettingsButton() {
const history = useHistory();
const openSettings = useCallback(() => {
- history.show('/settings');
+ history.show(RoutePath.settings);
}, [history]);
return (
diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx
index 3891a6e48b..eff5802666 100644
--- a/gui/src/renderer/components/MainView.tsx
+++ b/gui/src/renderer/components/MainView.tsx
@@ -5,6 +5,7 @@ import { IReduxState } from '../redux/store';
import ConnectPage from '../containers/ConnectPage';
import ExpiredAccountErrorViewContainer from '../containers/ExpiredAccountErrorViewContainer';
import { useHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
export default function MainView() {
const history = useHistory();
@@ -20,7 +21,7 @@ export default function MainView() {
if (accountHasExpired) {
setShowAccountExpired(true);
} else if (showAccountExpired && !accountHasExpired) {
- history.push('/main/time-added');
+ history.push(RoutePath.timeAdded);
}
}, [showAccountExpired, accountHasExpired]);
diff --git a/gui/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/src/renderer/containers/AdvancedSettingsPage.tsx
index 36a1a77963..d16f193ce0 100644
--- a/gui/src/renderer/containers/AdvancedSettingsPage.tsx
+++ b/gui/src/renderer/containers/AdvancedSettingsPage.tsx
@@ -11,6 +11,7 @@ import AdvancedSettings from '../components/AdvancedSettings';
import withAppContext, { IAppContext } from '../context';
import { IHistoryProps, withHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
import { RelaySettingsRedux } from '../redux/settings/reducers';
import { IReduxState, ReduxDispatch } from '../redux/store';
@@ -163,8 +164,8 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp
return props.app.setDnsOptions(dns);
},
- onViewWireguardKeys: () => props.history.push('/settings/advanced/wireguard-keys'),
- onViewSplitTunneling: () => props.history.push('/settings/advanced/split-tunneling'),
+ onViewWireguardKeys: () => props.history.push(RoutePath.wireguardKeys),
+ onViewSplitTunneling: () => props.history.push(RoutePath.splitTunneling),
};
};
diff --git a/gui/src/renderer/containers/ConnectPage.tsx b/gui/src/renderer/containers/ConnectPage.tsx
index d7197095f1..2069b4012b 100644
--- a/gui/src/renderer/containers/ConnectPage.tsx
+++ b/gui/src/renderer/containers/ConnectPage.tsx
@@ -5,6 +5,7 @@ import log from '../../shared/logging';
import Connect from '../components/Connect';
import withAppContext, { IAppContext } from '../context';
import { IHistoryProps, withHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
import { IRelayLocationRedux, RelaySettingsRedux } from '../redux/settings/reducers';
import { IReduxState, ReduxDispatch } from '../redux/store';
@@ -74,7 +75,7 @@ const mapStateToProps = (state: IReduxState) => {
const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
return {
onSelectLocation: () => {
- props.history.show('/select-location');
+ props.history.show(RoutePath.selectLocation);
},
onConnect: async () => {
try {
diff --git a/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx b/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx
index 372f9607c9..e5c56e7f28 100644
--- a/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx
+++ b/gui/src/renderer/containers/ExpiredAccountErrorViewContainer.tsx
@@ -5,6 +5,7 @@ import { IHistoryProps, withHistory } from '../lib/history';
import withAppContext, { IAppContext } from '../context';
import { IReduxState, ReduxDispatch } from '../redux/store';
+import { RoutePath } from '../lib/routes';
const mapStateToProps = (state: IReduxState) => ({
accountToken: state.account.accountToken,
@@ -31,7 +32,7 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp
}
},
navigateToRedeemVoucher: () => {
- props.history.push('/main/voucher/redeem');
+ props.history.push(RoutePath.redeemVoucher);
},
};
};
diff --git a/gui/src/renderer/containers/SettingsPage.tsx b/gui/src/renderer/containers/SettingsPage.tsx
index 57ded8cd24..da237abd23 100644
--- a/gui/src/renderer/containers/SettingsPage.tsx
+++ b/gui/src/renderer/containers/SettingsPage.tsx
@@ -2,6 +2,7 @@ import { connect } from 'react-redux';
import Settings from '../components/Settings';
import withAppContext, { IAppContext } from '../context';
import { IHistoryProps, withHistory } from '../lib/history';
+import { RoutePath } from '../lib/routes';
import { IReduxState, ReduxDispatch } from '../redux/store';
const mapStateToProps = (state: IReduxState, props: IAppContext) => ({
@@ -20,11 +21,11 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp
return {
onQuit: () => props.app.quit(),
onClose: () => props.history.dismiss(),
- onViewSelectLanguage: () => props.history.push('/settings/language'),
- onViewAccount: () => props.history.push('/settings/account'),
- onViewSupport: () => props.history.push('/settings/support'),
- onViewPreferences: () => props.history.push('/settings/preferences'),
- onViewAdvancedSettings: () => props.history.push('/settings/advanced'),
+ onViewSelectLanguage: () => props.history.push(RoutePath.selectLanguage),
+ onViewAccount: () => props.history.push(RoutePath.accountSettings),
+ onViewSupport: () => props.history.push(RoutePath.support),
+ onViewPreferences: () => props.history.push(RoutePath.preferences),
+ onViewAdvancedSettings: () => props.history.push(RoutePath.advancedSettings),
onExternalLink: (url: string) => props.app.openUrl(url),
updateAccountData: () => props.app.updateAccountData(),
};
diff --git a/gui/src/renderer/lib/history.tsx b/gui/src/renderer/lib/history.tsx
index 13968e29b9..a391e70fee 100644
--- a/gui/src/renderer/lib/history.tsx
+++ b/gui/src/renderer/lib/history.tsx
@@ -1,6 +1,7 @@
-import { Location, Action, LocationDescriptor } from 'history';
+import { Location, Action, LocationDescriptorObject, History as OriginalHistory } from 'history';
import React from 'react';
import { RouteComponentProps, useHistory as useReactRouterHistory, withRouter } from 'react-router';
+import { RoutePath } from './routes';
export interface ITransitionSpecification {
name: string;
@@ -37,6 +38,8 @@ export const transitions: ITransitionMap = {
},
};
+type LocationDescriptor<S> = RoutePath | LocationDescriptorObject<S>;
+
type LocationListener<S = unknown> = (
location: Location<S>,
action: Action,
@@ -53,7 +56,7 @@ export default class History {
private index = 0;
private lastAction: Action = 'POP';
- public constructor(location: string | Location<S>, state?: S) {
+ public constructor(location: LocationDescriptor<S>, state?: S) {
this.entries = [this.createLocation(location, state)];
}
@@ -114,6 +117,14 @@ export default class History {
return nextIndex >= 0 && nextIndex < this.entries.length;
}
+ // This returns this object casted as History from the History module. The difference between this
+ // one and the one in the history module is that this one has stricter types for the paths.
+ // Instead of accepting any string it's limited to the paths we actually support. But this history
+ // implementation would handle any string as expected.
+ public get asHistory(): OriginalHistory {
+ return this as OriginalHistory;
+ }
+
public block(): never {
throw Error('Not implemented');
}
diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts
new file mode 100644
index 0000000000..bf8e6e1f60
--- /dev/null
+++ b/gui/src/renderer/lib/routes.ts
@@ -0,0 +1,18 @@
+export enum RoutePath {
+ launch = '/',
+ login = '/login',
+ main = '/main',
+ redeemVoucher = '/main/voucher/redeem',
+ voucherSuccess = '/main/voucher/success',
+ timeAdded = '/main/time-added',
+ setupFinished = '/main/setup-finished',
+ settings = '/settings',
+ selectLanguage = '/settings/language',
+ accountSettings = '/settings/account',
+ preferences = '/settings/preferences',
+ advancedSettings = '/settings/advanced',
+ wireguardKeys = '/settings/advanced/wireguard-keys',
+ splitTunneling = '/settings/advanced/split-tunneling',
+ support = '/settings/support',
+ selectLocation = '/select-location',
+}
diff --git a/gui/src/renderer/routes.tsx b/gui/src/renderer/routes.tsx
deleted file mode 100644
index 1f12e7386e..0000000000
--- a/gui/src/renderer/routes.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { Action } from 'history';
-import * as React from 'react';
-import { Route, Switch } from 'react-router';
-import Launch from './components/Launch';
-import KeyboardNavigation from './components/KeyboardNavigation';
-import MainView from './components/MainView';
-import Focus, { IFocusHandle } from './components/Focus';
-import SplitTunnelingSettings from './components/SplitTunnelingSettings';
-import TransitionContainer, { TransitionView } from './components/TransitionContainer';
-import AccountPage from './containers/AccountPage';
-import AdvancedSettingsPage from './containers/AdvancedSettingsPage';
-import LoginPage from './containers/LoginPage';
-import PlatformWindowContainer from './containers/PlatformWindowContainer';
-import PreferencesPage from './containers/PreferencesPage';
-import SelectLanguagePage from './containers/SelectLanguagePage';
-import SelectLocationPage from './containers/SelectLocationPage';
-import SettingsPage from './containers/SettingsPage';
-import SupportPage from './containers/SupportPage';
-import WireguardKeysPage from './containers/WireguardKeysPage';
-import { IHistoryProps, ITransitionSpecification, transitions, withHistory } from './lib/history';
-import {
- SetupFinished,
- TimeAdded,
- VoucherInput,
- VoucherVerificationSuccess,
-} from './components/ExpiredAccountAddTime';
-
-interface IAppRoutesState {
- currentLocation: IHistoryProps['history']['location'];
- transition: ITransitionSpecification;
- action?: Action;
-}
-
-class AppRoutes extends React.Component<IHistoryProps, IAppRoutesState> {
- private unobserveHistory?: () => void;
-
- private focusRef = React.createRef<IFocusHandle>();
-
- constructor(props: IHistoryProps) {
- super(props);
-
- this.state = {
- currentLocation: props.history.location,
- transition: transitions.none,
- };
- }
-
- public componentDidMount() {
- // React throttles updates, so it's impossible to capture the intermediate navigation without
- // listening to the history directly.
- this.unobserveHistory = this.props.history.listen((location, action, transition) => {
- this.setState({
- currentLocation: location,
- transition,
- action,
- });
- });
- }
-
- public componentWillUnmount() {
- if (this.unobserveHistory) {
- this.unobserveHistory();
- }
- }
-
- public render() {
- const location = this.state.currentLocation;
-
- return (
- <PlatformWindowContainer>
- <KeyboardNavigation>
- <Focus ref={this.focusRef}>
- <TransitionContainer onTransitionEnd={this.onNavigation} {...this.state.transition}>
- <TransitionView viewId={location.key || ''}>
- <Switch key={location.key} location={location}>
- <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} />
- <Route exact={true} path="/settings/preferences" component={PreferencesPage} />
- <Route exact={true} path="/settings/advanced" component={AdvancedSettingsPage} />
- <Route
- exact={true}
- path="/settings/advanced/wireguard-keys"
- component={WireguardKeysPage}
- />
- <Route
- exact={true}
- path="/settings/advanced/split-tunneling"
- component={SplitTunnelingSettings}
- />
- <Route exact={true} path="/settings/support" component={SupportPage} />
- <Route exact={true} path="/select-location" component={SelectLocationPage} />
- </Switch>
- </TransitionView>
- </TransitionContainer>
- </Focus>
- </KeyboardNavigation>
- </PlatformWindowContainer>
- );
- }
-
- private onNavigation = () => {
- this.focusRef.current?.resetFocus();
- };
-}
-
-const AppRoutesWithRouter = withHistory(AppRoutes);
-
-export default AppRoutesWithRouter;