diff options
| -rw-r--r-- | gui/src/renderer/app.tsx | 106 | ||||
| -rw-r--r-- | gui/src/renderer/components/AppRouter.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/MainView.tsx | 56 | ||||
| -rw-r--r-- | gui/src/renderer/lib/history.tsx | 13 | ||||
| -rw-r--r-- | gui/src/renderer/lib/routes.ts | 1 | ||||
| -rw-r--r-- | gui/src/renderer/redux/account/actions.ts | 3 | ||||
| -rw-r--r-- | gui/src/renderer/redux/account/reducers.ts | 26 |
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, |
