summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--gui/src/renderer/app.tsx106
-rw-r--r--gui/src/renderer/components/AppRouter.tsx6
-rw-r--r--gui/src/renderer/components/MainView.tsx56
-rw-r--r--gui/src/renderer/lib/history.tsx13
-rw-r--r--gui/src/renderer/lib/routes.ts1
-rw-r--r--gui/src/renderer/redux/account/actions.ts3
-rw-r--r--gui/src/renderer/redux/account/reducers.ts26
7 files changed, 117 insertions, 94 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 6efcd81887..0dfda5d688 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -2,6 +2,7 @@ import { batch, Provider } from 'react-redux';
import { Router } from 'react-router';
import { bindActionCreators } from 'redux';
+import { hasExpired } from '../shared/account-expiry';
import { ILinuxSplitTunnelingApplication, IWindowsApplication } from '../shared/application-types';
import {
AccountToken,
@@ -204,7 +205,6 @@ export default class AppRenderer {
initialState.translations.relayLocations,
);
- this.setAccountExpiry(initialState.accountData?.expiry);
this.setSettings(initialState.settings);
this.setIsPerformingPostUpgrade(initialState.isPerformingPostUpgrade);
@@ -219,6 +219,8 @@ export default class AppRenderer {
initialState.navigationHistory !== undefined,
);
}
+ // Login state and account needs to be set before expiry.
+ this.setAccountExpiry(initialState.accountData?.expiry);
this.setAccountHistory(initialState.accountHistory);
this.setTunnelState(initialState.tunnelState);
@@ -648,40 +650,54 @@ export default class AppRenderer {
const nextPath = this.getNavigationBase() as RoutePath;
if (pathname !== nextPath) {
- // First level contains the possible next locations and the second level contains the
- // possible current locations.
- const navigationTransitions: Partial<
- Record<RoutePath, Partial<Record<RoutePath | '*', ITransitionSpecification>>>
- > = {
- [RoutePath.launch]: {
- [RoutePath.login]: transitions.pop,
- [RoutePath.main]: transitions.pop,
- '*': transitions.dismiss,
- },
- [RoutePath.login]: {
- [RoutePath.launch]: transitions.push,
- [RoutePath.main]: transitions.pop,
- [RoutePath.deviceRevoked]: transitions.pop,
- '*': transitions.dismiss,
- },
- [RoutePath.main]: {
- [RoutePath.launch]: transitions.push,
- [RoutePath.login]: transitions.push,
- [RoutePath.tooManyDevices]: transitions.push,
- '*': transitions.dismiss,
- },
- [RoutePath.deviceRevoked]: {
- '*': transitions.pop,
- },
- };
-
- const transition =
- navigationTransitions[nextPath]?.[pathname] ?? navigationTransitions[nextPath]?.['*'];
+ const transition = this.getNavigationTransition(pathname, nextPath);
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 | '*', ITransitionSpecification>>>
+ > = {
+ [RoutePath.launch]: {
+ [RoutePath.login]: transitions.pop,
+ [RoutePath.main]: transitions.pop,
+ '*': transitions.dismiss,
+ },
+ [RoutePath.login]: {
+ [RoutePath.launch]: transitions.push,
+ [RoutePath.main]: transitions.pop,
+ [RoutePath.deviceRevoked]: transitions.pop,
+ '*': transitions.dismiss,
+ },
+ [RoutePath.main]: {
+ [RoutePath.launch]: transitions.push,
+ [RoutePath.login]: transitions.push,
+ [RoutePath.tooManyDevices]: transitions.push,
+ '*': transitions.dismiss,
+ },
+ [RoutePath.expired]: {
+ [RoutePath.launch]: transitions.push,
+ [RoutePath.login]: transitions.push,
+ [RoutePath.tooManyDevices]: transitions.push,
+ '*': transitions.dismiss,
+ },
+ [RoutePath.timeAdded]: {
+ [RoutePath.expired]: transitions.push,
+ [RoutePath.redeemVoucher]: transitions.push,
+ '*': transitions.dismiss,
+ },
+ [RoutePath.deviceRevoked]: {
+ '*': transitions.pop,
+ },
+ };
+
+ return navigationTransitions[nextPath]?.[prevPath] ?? navigationTransitions[nextPath]?.['*'];
+ }
+
private getNavigationBase(): RoutePath {
if (this.connectedToDaemon && this.deviceState !== undefined) {
const loginState = this.reduxStore.getState().account.status;
@@ -689,10 +705,17 @@ export default class AppRenderer {
if (deviceRevoked) {
return RoutePath.deviceRevoked;
- } else if (this.isLoggedIn()) {
- return RoutePath.main;
- } else {
+ } else if (!this.isLoggedIn()) {
return RoutePath.login;
+ } else if (
+ loginState.type === 'ok' &&
+ (loginState.expiredState === 'expired' || loginState.method === 'new_account')
+ ) {
+ return RoutePath.expired;
+ } else if (loginState.type === 'ok' && loginState.expiredState === 'time_added') {
+ return RoutePath.timeAdded;
+ } else {
+ return RoutePath.main;
}
} else {
return RoutePath.launch;
@@ -884,7 +907,24 @@ export default class AppRenderer {
}
private setAccountExpiry(expiry?: string) {
+ const state = this.reduxStore.getState();
+ const previousExpiry = state.account.expiry;
this.reduxActions.account.updateAccountExpiry(expiry);
+
+ const expired = expiry !== undefined && hasExpired(expiry);
+ if (
+ this.history &&
+ state.account.status.type === 'ok' &&
+ expiry !== undefined &&
+ expiry !== previousExpiry &&
+ ((state.account.status.expiredState === undefined && expired) ||
+ (state.account.status.expiredState === 'expired' && !expired))
+ ) {
+ const prevPath = this.history.location.pathname as RoutePath;
+ const nextPath = expired ? RoutePath.expired : RoutePath.timeAdded;
+ const transition = this.getNavigationTransition(prevPath, nextPath);
+ this.history.replaceRoot(nextPath, { transition });
+ }
}
private storeAutoStart(autoStart: boolean) {
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index 59cace598d..aa474e30d5 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -7,6 +7,7 @@ import { useAppContext } from '../context';
import { ITransitionSpecification, transitions, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import Account from './Account';
+import Connect from './Connect';
import Debug from './Debug';
import { DeviceRevokedView } from './DeviceRevokedView';
import {
@@ -15,10 +16,10 @@ import {
VoucherInput,
VoucherVerificationSuccess,
} from './ExpiredAccountAddTime';
+import ExpiredAccountErrorView from './ExpiredAccountErrorView';
import Filter from './Filter';
import Focus, { IFocusHandle } from './Focus';
import Launch from './Launch';
-import MainView from './MainView';
import OpenVpnSettings from './OpenVpnSettings';
import ProblemReport from './ProblemReport';
import SelectLanguage from './SelectLanguage';
@@ -65,7 +66,8 @@ export default function AppRouter() {
<Route exact path={RoutePath.login} component={LoginPage} />
<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.main} component={Connect} />
+ <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} />
diff --git a/gui/src/renderer/components/MainView.tsx b/gui/src/renderer/components/MainView.tsx
deleted file mode 100644
index 9269013732..0000000000
--- a/gui/src/renderer/components/MainView.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useEffect, useState } from 'react';
-
-import { hasExpired } from '../../shared/account-expiry';
-import { AuthFailedError, ErrorStateCause } from '../../shared/daemon-rpc-types';
-import Connect from '../components/Connect';
-import { useAppContext } from '../context';
-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 };
-
-export default function MainView() {
- const { updateAccountData } = useAppContext();
- const history = useHistory();
- const accountExpiry = useSelector((state) => state.account.expiry);
- const accountHasExpired = accountExpiry !== undefined && hasExpired(accountExpiry);
- const isNewAccount = useSelector(
- (state) => state.account.status.type === 'ok' && state.account.status.method === 'new_account',
- );
- const tunnelState = useSelector((state) => state.connection.status);
-
- const [showAccountExpired, setShowAccountExpired] = useState<ExpiryData>(() =>
- isNewAccount || accountHasExpired ? { show: true, expiry: accountExpiry } : { show: false },
- );
-
- useEffect(() => {
- updateAccountData();
- }, []);
-
- useEffect(() => {
- if (
- (!showAccountExpired.show || showAccountExpired.expiry !== accountExpiry) &&
- (accountHasExpired ||
- (tunnelState.state === 'error' &&
- tunnelState.details.cause === ErrorStateCause.authFailed &&
- tunnelState.details.authFailedError === AuthFailedError.expiredAccount))
- ) {
- setShowAccountExpired({ show: true, expiry: accountExpiry });
- } else if (
- showAccountExpired.show &&
- accountExpiry &&
- accountExpiry !== showAccountExpired.expiry &&
- !accountHasExpired
- ) {
- history.push(RoutePath.timeAdded);
- }
- }, [showAccountExpired, accountHasExpired, tunnelState.state]);
-
- if (showAccountExpired.show) {
- return <ExpiredAccountErrorView />;
- } else {
- return <Connect />;
- }
-}
diff --git a/gui/src/renderer/lib/history.tsx b/gui/src/renderer/lib/history.tsx
index 159e597cce..af851a01d4 100644
--- a/gui/src/renderer/lib/history.tsx
+++ b/gui/src/renderer/lib/history.tsx
@@ -116,6 +116,19 @@ export default class History {
this.notify(nextState?.transition ?? transitions.none);
};
+ public replaceRoot = (
+ replacementLocation: LocationDescriptor,
+ replacementState?: Partial<LocationState>,
+ ) => {
+ const location = this.createLocation(replacementLocation, replacementState);
+ this.lastAction = 'REPLACE';
+ this.entries.splice(0, 1, location);
+
+ if (this.index === 0) {
+ this.notify(replacementState?.transition ?? transitions.none);
+ }
+ };
+
public listen(callback: LocationListener) {
this.listeners.push(callback);
return () => (this.listeners = this.listeners.filter((listener) => listener !== callback));
diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts
index 0be9983769..df8193b6d8 100644
--- a/gui/src/renderer/lib/routes.ts
+++ b/gui/src/renderer/lib/routes.ts
@@ -6,6 +6,7 @@ export enum RoutePath {
main = '/main',
redeemVoucher = '/main/voucher/redeem',
voucherSuccess = '/main/voucher/success/:newExpiry/:secondsAdded',
+ expired = '/main/expired',
timeAdded = '/main/time-added',
setupFinished = '/main/setup-finished',
settings = '/settings',
diff --git a/gui/src/renderer/redux/account/actions.ts b/gui/src/renderer/redux/account/actions.ts
index ece9719952..dded1c9513 100644
--- a/gui/src/renderer/redux/account/actions.ts
+++ b/gui/src/renderer/redux/account/actions.ts
@@ -1,3 +1,4 @@
+import { hasExpired } from '../../../shared/account-expiry';
import { AccountDataError, AccountToken, IDevice } from '../../../shared/daemon-rpc-types';
interface IStartLoginAction {
@@ -77,6 +78,7 @@ interface IUpdateAccountHistoryAction {
interface IUpdateAccountExpiryAction {
type: 'UPDATE_ACCOUNT_EXPIRY';
expiry?: string;
+ expired: boolean;
}
interface IUpdateDevicesAction {
@@ -214,6 +216,7 @@ function updateAccountExpiry(expiry?: string): IUpdateAccountExpiryAction {
return {
type: 'UPDATE_ACCOUNT_EXPIRY',
expiry,
+ expired: expiry !== undefined && hasExpired(expiry),
};
}
diff --git a/gui/src/renderer/redux/account/reducers.ts b/gui/src/renderer/redux/account/reducers.ts
index 6f9e558b03..93a07a2a3b 100644
--- a/gui/src/renderer/redux/account/reducers.ts
+++ b/gui/src/renderer/redux/account/reducers.ts
@@ -2,10 +2,12 @@ import { AccountDataError, AccountToken, IDevice } from '../../../shared/daemon-
import { ReduxAction } from '../store';
type LoginMethod = 'existing_account' | 'new_account';
+type ExpiredState = 'expired' | 'time_added';
+
export type LoginState =
| { type: 'none'; deviceRevoked: boolean }
| { type: 'logging in'; method: LoginMethod }
- | { type: 'ok'; method: LoginMethod; newDeviceBanner: boolean }
+ | { type: 'ok'; method: LoginMethod; newDeviceBanner: boolean; expiredState?: ExpiredState }
| { type: 'too many devices'; method: LoginMethod }
| { type: 'failed'; method: 'existing_account'; error: AccountDataError['error'] }
| { type: 'failed'; method: 'new_account'; error: Error };
@@ -98,7 +100,12 @@ export default function (
case 'ACCOUNT_CREATED':
return {
...state,
- status: { type: 'ok', method: 'new_account', newDeviceBanner: true },
+ status: {
+ type: 'ok',
+ method: 'new_account',
+ newDeviceBanner: true,
+ expiredState: 'expired',
+ },
accountToken: action.accountToken,
deviceName: action.deviceName,
expiry: action.expiry,
@@ -127,11 +134,24 @@ export default function (
...state,
accountHistory: action.accountHistory,
};
- case 'UPDATE_ACCOUNT_EXPIRY':
+ case 'UPDATE_ACCOUNT_EXPIRY': {
+ const status = { ...state.status };
+ if (status.type === 'ok') {
+ if (action.expired) {
+ status.expiredState = 'expired';
+ } else if (status.expiredState === 'expired' && !action.expired) {
+ status.expiredState = 'time_added';
+ } else {
+ status.expiredState = undefined;
+ }
+ }
+
return {
...state,
expiry: action.expiry,
+ status,
};
+ }
case 'UPDATE_DEVICES':
return {
...state,