diff options
| author | Oskar <oskar@mullvad.net> | 2025-09-12 16:19:17 +0200 |
|---|---|---|
| committer | Oskar <oskar@mullvad.net> | 2025-10-01 13:19:43 +0200 |
| commit | 34812fa0e81b275ef271a6027cf854aa124f21c4 (patch) | |
| tree | bab96fb27efee0ac294e6e4562c481ac3fc28abc | |
| parent | ed6f9da36222799ab0f2c9ff61ada979c800c524 (diff) | |
| download | mullvadvpn-34812fa0e81b275ef271a6027cf854aa124f21c4.tar.xz mullvadvpn-34812fa0e81b275ef271a6027cf854aa124f21c4.zip | |
Move resetNavigation into components
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; + } +} |
