summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2025-09-12 16:19:17 +0200
committerOskar <oskar@mullvad.net>2025-10-01 13:19:43 +0200
commit34812fa0e81b275ef271a6027cf854aa124f21c4 (patch)
treebab96fb27efee0ac294e6e4562c481ac3fc28abc
parented6f9da36222799ab0f2c9ff61ada979c800c524 (diff)
downloadmullvadvpn-34812fa0e81b275ef271a6027cf854aa124f21c4.tar.xz
mullvadvpn-34812fa0e81b275ef271a6027cf854aa124f21c4.zip
Move resetNavigation into components
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/app.tsx141
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx82
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/StateTriggeredNavigation.tsx102
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/lib/functions/navigation-base.ts20
4 files changed, 173 insertions, 172 deletions
diff --git a/desktop/packages/mullvad-vpn/src/renderer/app.tsx b/desktop/packages/mullvad-vpn/src/renderer/app.tsx
index 5b7f625019..9252aa7023 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/app.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/app.tsx
@@ -17,7 +17,6 @@ import {
BridgeState,
CustomProxy,
DeviceEvent,
- DeviceState,
IAccountData,
IAppVersionInfo,
ICustomList,
@@ -54,6 +53,7 @@ import MacOsScrollbarDetection from './components/MacOsScrollbarDetection';
import { ModalContainer } from './components/Modal';
import { AppContext } from './context';
import { Theme } from './lib/components';
+import { getNavigationBase } from './lib/functions/navigation-base';
import History, { TransitionType } from './lib/history';
import { loadTranslations } from './lib/load-translations';
import IpcOutput from './lib/logging';
@@ -113,9 +113,7 @@ export default class AppRenderer {
private relayList?: IRelayListWithEndpointData;
private tunnelState!: TunnelState;
private settings!: ISettings;
- private deviceState?: DeviceState;
private loginState: LoginState = 'none';
- private previousLoginState: LoginState = 'none';
private connectedToDaemon = false;
private loginScheduler = new Scheduler();
@@ -282,10 +280,7 @@ export default class AppRenderer {
if (initialState.deviceState) {
const deviceState = initialState.deviceState;
- this.handleDeviceEvent(
- { type: deviceState.type, deviceState } as DeviceEvent,
- initialState.navigationHistory !== undefined,
- );
+ this.handleDeviceEvent({ type: deviceState.type, deviceState } as DeviceEvent);
}
// Login state and account needs to be set before expiry.
this.setAccountExpiry(initialState.accountData?.expiry);
@@ -331,7 +326,8 @@ export default class AppRenderer {
initialState.navigationHistory.lastAction = 'POP';
this.history = History.fromSavedHistory(initialState.navigationHistory);
} else {
- const navigationBase = this.getNavigationBase();
+ const loginState = this.reduxStore.getState().account.status;
+ const navigationBase = getNavigationBase(this.connectedToDaemon, loginState);
this.history = new History(navigationBase);
}
@@ -493,7 +489,6 @@ export default class AppRenderer {
log.info('Logging in');
- this.previousLoginState = this.loginState;
this.loginState = 'logging in';
const response = await IpcRendererEventChannel.account.login(accountNumber);
@@ -546,7 +541,6 @@ export default class AppRenderer {
try {
await IpcRendererEventChannel.account.create();
- this.redirectToConnect();
} catch (e) {
const error = e as Error;
actions.account.createAccountFailed(error);
@@ -727,10 +721,6 @@ export default class AppRenderer {
}
}
- private isLoggedIn(): boolean {
- return this.deviceState?.type === 'logged in';
- }
-
// Make sure that the content height is correct and log if it isn't. This is mostly for debugging
// purposes since there's a bug in Electron that causes the app height to be another value than
// the one we have set.
@@ -746,11 +736,6 @@ export default class AppRenderer {
}
}
- private redirectToConnect() {
- // Redirect the user after some time to allow for the 'Logged in' screen to be visible
- this.loginScheduler.schedule(() => this.resetNavigation(), 1000);
- }
-
private setLocale(locale: string) {
this.reduxActions.userInterface.updateLocale(locale);
}
@@ -823,93 +808,12 @@ export default class AppRenderer {
this.reduxActions.userInterface.setConnectedToDaemon(true);
this.reduxActions.userInterface.setDaemonAllowed(true);
this.reduxActions.userInterface.setDaemonStatus('running');
- this.resetNavigation();
}
private onDaemonDisconnected() {
this.connectedToDaemon = false;
this.reduxActions.userInterface.setConnectedToDaemon(false);
this.reduxActions.userInterface.setDaemonStatus('stopped');
- this.resetNavigation();
- }
-
- private resetNavigation(replaceRoot?: boolean) {
- if (this.history) {
- const pathname = this.history.location.pathname as RoutePath;
- const nextPath = this.getNavigationBase() as RoutePath;
-
- if (pathname !== nextPath) {
- const transition = this.getNavigationTransition(pathname, nextPath);
- if (replaceRoot) {
- this.history.replaceRoot(nextPath, { transition });
- } else {
- this.history.reset(nextPath, { transition });
- }
- }
- }
- }
-
- private getNavigationTransition(prevPath: RoutePath, nextPath: RoutePath) {
- // First level contains the possible next locations and the second level contains the
- // possible current locations.
- const navigationTransitions: Partial<
- Record<RoutePath, Partial<Record<RoutePath | '*', TransitionType>>>
- > = {
- [RoutePath.launch]: {
- [RoutePath.login]: TransitionType.pop,
- [RoutePath.main]: TransitionType.pop,
- '*': TransitionType.dismiss,
- },
- [RoutePath.login]: {
- [RoutePath.launch]: TransitionType.push,
- [RoutePath.main]: TransitionType.pop,
- [RoutePath.deviceRevoked]: TransitionType.pop,
- '*': TransitionType.dismiss,
- },
- [RoutePath.main]: {
- [RoutePath.launch]: TransitionType.push,
- [RoutePath.login]: TransitionType.push,
- [RoutePath.tooManyDevices]: TransitionType.push,
- '*': TransitionType.dismiss,
- },
- [RoutePath.expired]: {
- [RoutePath.launch]: TransitionType.push,
- [RoutePath.login]: TransitionType.push,
- [RoutePath.tooManyDevices]: TransitionType.push,
- '*': TransitionType.dismiss,
- },
- [RoutePath.timeAdded]: {
- [RoutePath.expired]: TransitionType.push,
- [RoutePath.redeemVoucher]: TransitionType.push,
- '*': TransitionType.dismiss,
- },
- [RoutePath.deviceRevoked]: {
- '*': TransitionType.pop,
- },
- };
-
- return navigationTransitions[nextPath]?.[prevPath] ?? navigationTransitions[nextPath]?.['*'];
- }
-
- private getNavigationBase(): RoutePath {
- if (this.connectedToDaemon && this.deviceState !== undefined) {
- const loginState = this.reduxStore.getState().account.status;
- const deviceRevoked = loginState.type === 'none' && loginState.deviceRevoked;
-
- if (deviceRevoked) {
- return RoutePath.deviceRevoked;
- } else if (!this.isLoggedIn()) {
- return RoutePath.login;
- } else if (loginState.type === 'ok' && loginState.expiredState === 'expired') {
- return RoutePath.expired;
- } else if (loginState.type === 'ok' && loginState.expiredState === 'time_added') {
- return RoutePath.timeAdded;
- } else {
- return RoutePath.main;
- }
- } else {
- return RoutePath.launch;
- }
}
private setAccountHistory(accountHistory?: AccountNumber) {
@@ -1005,11 +909,9 @@ export default class AppRenderer {
}
}
- private handleDeviceEvent(deviceEvent: DeviceEvent, preventRedirectToConnect?: boolean) {
+ private handleDeviceEvent(deviceEvent: DeviceEvent) {
const reduxAccount = this.reduxActions.account;
- this.deviceState = deviceEvent.deviceState;
-
switch (deviceEvent.type) {
case 'logged in': {
const accountNumber = deviceEvent.deviceState.accountAndDevice.accountNumber;
@@ -1018,16 +920,9 @@ export default class AppRenderer {
switch (this.loginState) {
case 'none':
reduxAccount.loggedIn(accountNumber, device);
- this.resetNavigation();
break;
case 'logging in':
reduxAccount.loggedIn(accountNumber, device);
-
- if (this.previousLoginState === 'too many devices') {
- this.resetNavigation();
- } else if (!preventRedirectToConnect) {
- this.redirectToConnect();
- }
break;
case 'creating account':
reduxAccount.accountCreated(accountNumber, device, new Date().toISOString());
@@ -1038,17 +933,14 @@ export default class AppRenderer {
case 'logged out':
this.loginScheduler.cancel();
reduxAccount.loggedOut();
- this.resetNavigation();
break;
case 'revoked': {
this.loginScheduler.cancel();
reduxAccount.deviceRevoked();
- this.resetNavigation();
break;
}
}
- this.previousLoginState = this.loginState;
this.loginState = 'none';
}
@@ -1092,9 +984,6 @@ export default class AppRenderer {
}
private setAccountExpiry(expiry?: string) {
- const state = this.reduxStore.getState();
- const previousExpiry = state.account.expiry;
-
this.expiryScheduler.cancel();
if (expiry !== undefined) {
@@ -1103,31 +992,17 @@ export default class AppRenderer {
// Set state to expired when expiry date passes.
if (!expired && closeToExpiry(expiry)) {
const delay = new Date(expiry).getTime() - Date.now() + 1;
- this.expiryScheduler.schedule(() => this.handleExpiry(expiry, true), delay);
+ this.expiryScheduler.schedule(() => this.handleExpiry(expiry), delay);
}
- if (expiry !== previousExpiry) {
- this.handleExpiry(expiry, expired);
- }
+ this.handleExpiry(expiry);
} else {
this.handleExpiry(expiry);
}
}
- private handleExpiry(expiry?: string, expired?: boolean) {
- const state = this.reduxStore.getState();
+ private handleExpiry(expiry?: string) {
this.reduxActions.account.updateAccountExpiry(expiry);
-
- if (
- expiry !== undefined &&
- state.account.status.type === 'ok' &&
- ((state.account.status.expiredState === undefined && expired) ||
- (state.account.status.expiredState === 'expired' && !expired)) &&
- // If the login navigation is already scheduled no navigation is needed
- !this.loginScheduler.isRunning
- ) {
- this.resetNavigation(true);
- }
}
private storeAutoStart(autoStart: boolean) {
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx
index 5f5f2c4082..5f0bf016d2 100644
--- a/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx
@@ -23,6 +23,7 @@ import ProblemReport from './ProblemReport';
import SelectLanguage from './SelectLanguage';
import SettingsImport from './SettingsImport';
import SettingsTextImport from './SettingsTextImport';
+import StateTriggeredNavigation from './StateTriggeredNavigation';
import Support from './Support';
import TooManyDevices from './TooManyDevices';
import UserInterfaceSettings from './UserInterfaceSettings';
@@ -53,44 +54,47 @@ export default function AppRouter() {
const currentLocation = useViewTransitions(onNavigation);
return (
- <Focus ref={focusRef}>
- <Switch key={currentLocation.key} location={currentLocation}>
- <Route exact path={RoutePath.launch} component={LaunchView} />
- <Route exact path={RoutePath.login} component={LoginView} />
- <Route exact path={RoutePath.tooManyDevices} component={TooManyDevices} />
- <Route exact path={RoutePath.deviceRevoked} component={DeviceRevokedView} />
- <Route exact path={RoutePath.main} component={MainView} />
- <Route exact path={RoutePath.expired} component={ExpiredAccountErrorView} />
- <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.account} component={Account} />
- <Route exact path={RoutePath.settings} component={SettingsView} />
- <Route exact path={RoutePath.selectLanguage} component={SelectLanguage} />
- <Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
- <Route exact path={RoutePath.multihopSettings} component={MultihopSettingsView} />
- <Route exact path={RoutePath.vpnSettings} component={VpnSettingsView} />
- <Route exact path={RoutePath.wireguardSettings} component={WireguardSettingsView} />
- <Route exact path={RoutePath.daitaSettings} component={DaitaSettingsView} />
- <Route exact path={RoutePath.udpOverTcp} component={UdpOverTcpSettingsView} />
- <Route exact path={RoutePath.shadowsocks} component={ShadowsocksSettingsView} />
- <Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettingsView} />
- <Route exact path={RoutePath.splitTunneling} component={SplitTunnelingView} />
- <Route exact path={RoutePath.apiAccessMethods} component={ApiAccessMethods} />
- <Route exact path={RoutePath.settingsImport} component={SettingsImport} />
- <Route exact path={RoutePath.settingsTextImport} component={SettingsTextImport} />
- <Route exact path={RoutePath.editApiAccessMethods} component={EditApiAccessMethod} />
- <Route exact path={RoutePath.support} component={Support} />
- <Route exact path={RoutePath.problemReport} component={ProblemReport} />
- <Route exact path={RoutePath.debug} component={Debug} />
- <Route exact path={RoutePath.selectLocation} component={SelectLocation} />
- <Route exact path={RoutePath.editCustomBridge} component={EditCustomBridge} />
- <Route exact path={RoutePath.filter} component={Filter} />
- <Route exact path={RoutePath.appInfo} component={AppInfoView} />
- <Route exact path={RoutePath.changelog} component={ChangelogView} />
- <Route exact path={RoutePath.appUpgrade} component={AppUpgradeView} />
- </Switch>
- </Focus>
+ <>
+ <StateTriggeredNavigation />
+ <Focus ref={focusRef}>
+ <Switch key={currentLocation.key} location={currentLocation}>
+ <Route exact path={RoutePath.launch} component={LaunchView} />
+ <Route exact path={RoutePath.login} component={LoginView} />
+ <Route exact path={RoutePath.tooManyDevices} component={TooManyDevices} />
+ <Route exact path={RoutePath.deviceRevoked} component={DeviceRevokedView} />
+ <Route exact path={RoutePath.main} component={MainView} />
+ <Route exact path={RoutePath.expired} component={ExpiredAccountErrorView} />
+ <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.account} component={Account} />
+ <Route exact path={RoutePath.settings} component={SettingsView} />
+ <Route exact path={RoutePath.selectLanguage} component={SelectLanguage} />
+ <Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
+ <Route exact path={RoutePath.multihopSettings} component={MultihopSettingsView} />
+ <Route exact path={RoutePath.vpnSettings} component={VpnSettingsView} />
+ <Route exact path={RoutePath.wireguardSettings} component={WireguardSettingsView} />
+ <Route exact path={RoutePath.daitaSettings} component={DaitaSettingsView} />
+ <Route exact path={RoutePath.udpOverTcp} component={UdpOverTcpSettingsView} />
+ <Route exact path={RoutePath.shadowsocks} component={ShadowsocksSettingsView} />
+ <Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettingsView} />
+ <Route exact path={RoutePath.splitTunneling} component={SplitTunnelingView} />
+ <Route exact path={RoutePath.apiAccessMethods} component={ApiAccessMethods} />
+ <Route exact path={RoutePath.settingsImport} component={SettingsImport} />
+ <Route exact path={RoutePath.settingsTextImport} component={SettingsTextImport} />
+ <Route exact path={RoutePath.editApiAccessMethods} component={EditApiAccessMethod} />
+ <Route exact path={RoutePath.support} component={Support} />
+ <Route exact path={RoutePath.problemReport} component={ProblemReport} />
+ <Route exact path={RoutePath.debug} component={Debug} />
+ <Route exact path={RoutePath.selectLocation} component={SelectLocation} />
+ <Route exact path={RoutePath.editCustomBridge} component={EditCustomBridge} />
+ <Route exact path={RoutePath.filter} component={Filter} />
+ <Route exact path={RoutePath.appInfo} component={AppInfoView} />
+ <Route exact path={RoutePath.changelog} component={ChangelogView} />
+ <Route exact path={RoutePath.appUpgrade} component={AppUpgradeView} />
+ </Switch>
+ </Focus>
+ </>
);
}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/StateTriggeredNavigation.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/StateTriggeredNavigation.tsx
new file mode 100644
index 0000000000..0187bda865
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/components/StateTriggeredNavigation.tsx
@@ -0,0 +1,102 @@
+import { useEffect, useMemo } from 'react';
+
+import { RoutePath } from '../../shared/routes';
+import { useScheduler } from '../../shared/scheduler';
+import { getNavigationBase } from '../lib/functions/navigation-base';
+import { TransitionType, useHistory } from '../lib/history';
+import { useEffectEvent } from '../lib/utility-hooks';
+import { useSelector } from '../redux/store';
+
+export default function StateTriggeredNavigation() {
+ const { location, reset } = useHistory();
+
+ const connectedToDaemon = useSelector((state) => state.userInterface.connectedToDaemon);
+ const loginState = useSelector((state) => state.account.status);
+
+ const delayScheduler = useScheduler();
+
+ const nextPath = useMemo(
+ () => getNavigationBase(connectedToDaemon, loginState),
+ [connectedToDaemon, loginState],
+ );
+
+ const updatePath = useEffectEvent((nextPath: RoutePath) => {
+ const currentPath = location.pathname as RoutePath;
+
+ if (currentPath !== nextPath) {
+ delayScheduler.cancel();
+
+ const transition = getNavigationTransition(currentPath, nextPath);
+ const delay = getNavigationDelay(currentPath, nextPath);
+
+ const navigate = () => {
+ reset(nextPath, { transition });
+ };
+
+ if (delay) {
+ delayScheduler.schedule(navigate, delay);
+ } else {
+ navigate();
+ }
+ }
+ });
+
+ useEffect(() => {
+ updatePath(nextPath);
+ // eslint-disable-next-line react-compiler/react-compiler
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [nextPath]);
+
+ return null;
+}
+
+function getNavigationDelay(currentPath: RoutePath, nextPath: RoutePath): number | void {
+ if (
+ currentPath === RoutePath.login &&
+ (nextPath === RoutePath.main || nextPath === RoutePath.expired)
+ ) {
+ return 1000;
+ }
+}
+
+function getNavigationTransition(currentPath: RoutePath, nextPath: RoutePath) {
+ // First level contains the possible next locations and the second level contains the
+ // possible current locations.
+ const navigationTransitions: Partial<
+ Record<RoutePath, Partial<Record<RoutePath | '*', TransitionType>>>
+ > = {
+ [RoutePath.launch]: {
+ [RoutePath.login]: TransitionType.pop,
+ [RoutePath.main]: TransitionType.pop,
+ '*': TransitionType.dismiss,
+ },
+ [RoutePath.login]: {
+ [RoutePath.launch]: TransitionType.push,
+ [RoutePath.main]: TransitionType.pop,
+ [RoutePath.deviceRevoked]: TransitionType.pop,
+ '*': TransitionType.dismiss,
+ },
+ [RoutePath.main]: {
+ [RoutePath.launch]: TransitionType.push,
+ [RoutePath.login]: TransitionType.push,
+ [RoutePath.tooManyDevices]: TransitionType.push,
+ '*': TransitionType.dismiss,
+ },
+ [RoutePath.expired]: {
+ [RoutePath.launch]: TransitionType.push,
+ [RoutePath.login]: TransitionType.push,
+ [RoutePath.tooManyDevices]: TransitionType.push,
+ '*': TransitionType.dismiss,
+ },
+ [RoutePath.timeAdded]: {
+ [RoutePath.expired]: TransitionType.push,
+ [RoutePath.redeemVoucher]: TransitionType.push,
+ '*': TransitionType.dismiss,
+ },
+ [RoutePath.deviceRevoked]: {
+ '*': TransitionType.pop,
+ },
+ };
+
+ return navigationTransitions[nextPath]?.[currentPath] ?? navigationTransitions[nextPath]?.['*'];
+}
diff --git a/desktop/packages/mullvad-vpn/src/renderer/lib/functions/navigation-base.ts b/desktop/packages/mullvad-vpn/src/renderer/lib/functions/navigation-base.ts
new file mode 100644
index 0000000000..96f92d483e
--- /dev/null
+++ b/desktop/packages/mullvad-vpn/src/renderer/lib/functions/navigation-base.ts
@@ -0,0 +1,20 @@
+import { RoutePath } from '../../../shared/routes';
+import { LoginState } from '../../redux/account/reducers';
+
+export function getNavigationBase(connectedToDaemon: boolean, loginState: LoginState): RoutePath {
+ if (connectedToDaemon) {
+ if (loginState.type === 'none' && loginState.deviceRevoked) {
+ return RoutePath.deviceRevoked;
+ } else if (loginState.type === 'none' || loginState.type === 'logging in') {
+ return RoutePath.login;
+ } else if (loginState.type === 'ok' && loginState.expiredState === 'expired') {
+ return RoutePath.expired;
+ } else if (loginState.type === 'ok' && loginState.expiredState === 'time_added') {
+ return RoutePath.timeAdded;
+ } else {
+ return RoutePath.main;
+ }
+ } else {
+ return RoutePath.launch;
+ }
+}