diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 78c11504f259..1a735e1ec553 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -3,7 +3,7 @@ import { Router } from 'react-router'; import { bindActionCreators } from 'redux'; import { StyleSheetManager } from 'styled-components'; -import { hasExpired } from '../shared/account-expiry'; +import { closeToExpiry, hasExpired } from '../shared/account-expiry'; import { ILinuxSplitTunnelingApplication, IWindowsApplication } from '../shared/application-types'; import { AccessMethodSetting, @@ -103,9 +103,11 @@ export default class AppRenderer { private deviceState?: DeviceState; private loginState: LoginState = 'none'; private previousLoginState: LoginState = 'none'; - private loginScheduler = new Scheduler(); private connectedToDaemon = false; + private loginScheduler = new Scheduler(); + private expiryScheduler = new Scheduler(); + constructor() { log.addOutput(new ConsoleOutput(LogLevel.debug)); log.addOutput(new IpcOutput(LogLevel.debug)); @@ -669,14 +671,18 @@ export default class AppRenderer { this.resetNavigation(); } - private 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); - this.history.reset(nextPath, { transition }); + if (replaceRoot) { + this.history.replaceRoot(nextPath, { transition }); + } else { + this.history.reset(nextPath, { transition }); + } } } } @@ -927,29 +933,41 @@ export default class AppRenderer { } private setAccountExpiry(expiry?: string) { - if (window.env.e2e && expiry) { - log.verbose('Expiry of account:', expiry); + const state = this.reduxStore.getState(); + const previousExpiry = state.account.expiry; + + this.expiryScheduler.cancel(); + + if (expiry !== undefined) { + const expired = hasExpired(expiry); + + // 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); + } + + if (expiry !== previousExpiry) { + this.handleExpiry(expiry, expired); + } + } else { + this.handleExpiry(expiry); } + } + private handleExpiry(expiry?: string, expired?: boolean) { 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.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 ) { - 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 }); + this.resetNavigation(true); } } diff --git a/gui/test/e2e/installed/state-dependent/login.spec.ts b/gui/test/e2e/installed/state-dependent/login.spec.ts index 45444715bb66..ba72a5b4666d 100644 --- a/gui/test/e2e/installed/state-dependent/login.spec.ts +++ b/gui/test/e2e/installed/state-dependent/login.spec.ts @@ -49,12 +49,12 @@ test('App should create account', async () => { const title = page.locator('h1') const subtitle = page.getByTestId('subtitle'); - await page.getByText('Create account').click(); + expect(await util.waitForNavigation(async () => { + await page.getByText('Create account').click(); - await expect(title).toHaveText('Account created'); - await expect(subtitle).toHaveText('Logged in'); - - expect(await util.waitForNavigation()).toEqual(RoutePath.expired); + await expect(title).toHaveText('Account created'); + await expect(subtitle).toHaveText('Logged in'); + })).toEqual(RoutePath.expired); const outOfTimeTitle = page.getByTestId('title'); await expect(outOfTimeTitle).toHaveText('Congrats!'); @@ -80,13 +80,14 @@ test('App should log in', async () => { await expect(title).toHaveText('Login'); await expect(subtitle).toHaveText('Enter your account number'); - await loginInput.type(process.env.ACCOUNT_NUMBER!); - await loginInput.press('Enter'); + await loginInput.fill(process.env.ACCOUNT_NUMBER!); - await expect(title).toHaveText('Logged in'); - await expect(subtitle).toHaveText('Valid account number'); + expect(await util.waitForNavigation(async () => { + await loginInput.press('Enter'); - expect(await util.waitForNavigation()).toEqual(RoutePath.main); + await expect(title).toHaveText('Logged in'); + await expect(subtitle).toHaveText('Valid account number'); + })).toEqual(RoutePath.main); await expectDisconnected(page); }); @@ -115,13 +116,11 @@ test('App should log in to expired account', async () => { await expect(title).toHaveText('Login'); await expect(subtitle).toHaveText('Enter your account number'); - await loginInput.type(accountNumber); - await loginInput.press('Enter'); - - await expect(title).toHaveText('Logged in'); - await expect(subtitle).toHaveText('Valid account number'); + await loginInput.fill(accountNumber); - expect(await util.waitForNavigation()).toEqual(RoutePath.expired); + expect(await util.waitForNavigation(async () => { + await loginInput.press('Enter'); + })).toEqual(RoutePath.expired); const outOfTimeTitle = page.getByTestId('title'); await expect(outOfTimeTitle).toHaveText('Out of time'); diff --git a/gui/test/e2e/mocked/expired-account-error-view.spec.ts b/gui/test/e2e/mocked/expired-account-error-view.spec.ts index 0d6bacd74770..5c906a134b35 100644 --- a/gui/test/e2e/mocked/expired-account-error-view.spec.ts +++ b/gui/test/e2e/mocked/expired-account-error-view.spec.ts @@ -4,15 +4,16 @@ import { expect, test } from '@playwright/test'; import { IAccountData } from '../../../src/shared/daemon-rpc-types'; import { getBackgroundColor } from '../utils'; import { colors } from '../../../src/config.json'; +import { RoutePath } from '../../../src/renderer/lib/routes'; let page: Page; let util: MockedTestUtils; -test.beforeAll(async () => { +test.beforeEach(async () => { ({ page, util } = await startMockedApp()); }); -test.afterAll(async () => { +test.afterEach(async () => { await page.close(); }); @@ -31,3 +32,15 @@ test('App should show Expired Account Error View', async () => { await expect(redeemVoucherButton).toBeVisible(); expect(await getBackgroundColor(redeemVoucherButton)).toBe(colors.green); }); + +test('App should show out of time view after running out of time', async () => { + const expiryDate = new Date(); + expiryDate.setSeconds(expiryDate.getSeconds() + 2); + + expect(await util.waitForNavigation(async () => { + await util.sendMockIpcResponse({ + channel: 'account-', + response: { expiry: expiryDate.toISOString() }, + }); + })).toEqual(RoutePath.expired); +});