summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2023-06-28 09:13:52 +0200
committerOskar Nyberg <oskar@mullvad.net>2023-06-28 09:13:52 +0200
commit77f4e40ee83b6d66544099e0ab6253780a8c358d (patch)
tree91c28c3aab4dcd0a7f499979066c05be9871bebf /gui
parent425a7830379f4d2c583f076d4b1d04669afc02c7 (diff)
parentf6c2af4c5787f826b479eb0e3d8ab7d539e538ff (diff)
downloadmullvadvpn-77f4e40ee83b6d66544099e0ab6253780a8c358d.tar.xz
mullvadvpn-77f4e40ee83b6d66544099e0ab6253780a8c358d.zip
Merge branch 'add-device-and-account-to-header'
Diffstat (limited to 'gui')
-rw-r--r--gui/locales/messages.pot70
-rw-r--r--gui/src/renderer/components/AppRouter.tsx2
-rw-r--r--gui/src/renderer/components/ExpiredAccountAddTime.tsx4
-rw-r--r--gui/src/renderer/components/HeaderBar.tsx116
-rw-r--r--gui/src/renderer/components/RedeemVoucher.tsx5
-rw-r--r--gui/src/renderer/components/Settings.tsx27
-rw-r--r--gui/src/renderer/components/SettingsStyles.tsx5
-rw-r--r--gui/src/renderer/lib/routes.ts2
-rw-r--r--gui/src/shared/account-expiry.ts17
-rw-r--r--gui/src/shared/date-helper.ts86
-rw-r--r--gui/src/shared/notifications/close-to-account-expiry.ts10
-rw-r--r--gui/test/e2e/installed/state-dependent/login.spec.ts6
-rw-r--r--gui/test/e2e/mocked/settings.spec.ts36
-rw-r--r--gui/test/unit/date-helper.spec.ts97
14 files changed, 276 insertions, 207 deletions
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index cfa73b0d27..db4ff05229 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -35,33 +35,11 @@ msgid_plural "%d years left"
msgstr[0] ""
msgstr[1] ""
-msgid "a day ago"
-msgid_plural "%d days ago"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "a minute ago"
-msgid_plural "%d minutes ago"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "a month ago"
-msgid_plural "%d months ago"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "a year ago"
-msgid_plural "%d years ago"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Account paid until %(expiry)s."
msgstr ""
-msgid "an hour ago"
-msgid_plural "%d hours ago"
-msgstr[0] ""
-msgstr[1] ""
+msgid "Account settings"
+msgstr ""
msgid "Any"
msgstr ""
@@ -167,9 +145,6 @@ msgstr ""
msgid "less than a day left"
msgstr ""
-msgid "less than a minute ago"
-msgstr ""
-
msgid "Next"
msgstr ""
@@ -535,6 +510,10 @@ msgctxt "device-management"
msgid "This will delete all forwarded ports. Local settings will be saved."
msgstr ""
+msgctxt "device-management"
+msgid "Time left: %(timeLeft)s"
+msgstr ""
+
#. Page title informing user that the login failed due to too many registered
#. devices on account.
msgctxt "device-management"
@@ -1069,11 +1048,6 @@ msgctxt "select-location-view"
msgid "While connected, your traffic will be routed through two secure locations, the entry point and the exit point (needs to be two different VPN servers)."
msgstr ""
-#. Navigation button to the 'Account' view
-msgctxt "settings-view"
-msgid "Account"
-msgstr ""
-
msgctxt "settings-view"
msgid "App is out of sync. Please quit and restart."
msgstr ""
@@ -1082,10 +1056,6 @@ msgctxt "settings-view"
msgid "App version"
msgstr ""
-msgctxt "settings-view"
-msgid "OUT OF TIME"
-msgstr ""
-
#. Navigation button to the 'Split tunneling' view
msgctxt "settings-view"
msgid "Split tunneling"
@@ -1818,6 +1788,9 @@ msgstr ""
msgid "You are running an unsupported app version."
msgstr ""
+msgid "less than a minute ago"
+msgstr ""
+
msgid "Account credit expires in a day"
msgid_plural "Account credit expires in %d days"
msgstr[0] ""
@@ -1827,3 +1800,28 @@ msgid "Account credit expires in an hour"
msgid_plural "Account credit expires in %d hours"
msgstr[0] ""
msgstr[1] ""
+
+msgid "a day ago"
+msgid_plural "%d days ago"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "a minute ago"
+msgid_plural "%d minutes ago"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "a month ago"
+msgid_plural "%d months ago"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "a year ago"
+msgid_plural "%d years ago"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "an hour ago"
+msgid_plural "%d hours ago"
+msgstr[0] ""
+msgstr[1] ""
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index cd6e07fbbd..59cace598d 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -70,9 +70,9 @@ export default function AppRouter() {
<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={Settings} />
<Route exact path={RoutePath.selectLanguage} component={SelectLanguage} />
- <Route exact path={RoutePath.accountSettings} component={Account} />
<Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
index 2c593217b8..3b54642978 100644
--- a/gui/src/renderer/components/ExpiredAccountAddTime.tsx
+++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
@@ -151,7 +151,9 @@ export function TimeAdded(props: ITimeAddedProps) {
}, [history, finish]);
const duration =
- props.secondsAdded !== undefined ? formatRelativeDate(props.secondsAdded * 1000, 0) : undefined;
+ props.secondsAdded !== undefined
+ ? formatRelativeDate(0, props.secondsAdded * 1000, { capitalize: true, displayMonths: true })
+ : undefined;
let newExpiry = '';
if (props.newExpiry !== undefined) {
diff --git a/gui/src/renderer/components/HeaderBar.tsx b/gui/src/renderer/components/HeaderBar.tsx
index 7c436e3c16..7ebf448e5c 100644
--- a/gui/src/renderer/components/HeaderBar.tsx
+++ b/gui/src/renderer/components/HeaderBar.tsx
@@ -1,13 +1,16 @@
import React, { useCallback } from 'react';
-import { useSelector } from 'react-redux';
+import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
import { colors } from '../../config.json';
+import { closeToExpiry, formatRemainingTime, hasExpired } from '../../shared/account-expiry';
import { TunnelState } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
+import { capitalizeEveryWord } from '../../shared/string-helpers';
import { transitions, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
-import { IReduxState } from '../redux/store';
+import { useSelector } from '../redux/store';
+import { tinyText } from './common-styles';
import { FocusFallback } from './Focus';
import ImageView from './ImageView';
@@ -27,39 +30,45 @@ const headerBarStyleColorMap = {
interface IHeaderBarContainerProps {
barStyle?: HeaderBarStyle;
+ accountInfoVisible: boolean;
unpinnedWindow: boolean;
}
const HeaderBarContainer = styled.header({}, (props: IHeaderBarContainerProps) => ({
- padding: '12px 16px',
+ padding: '15px 16px 0px',
+ minHeight: props.accountInfoVisible ? '80px' : '68px',
+ height: props.accountInfoVisible ? '80px' : '68px',
backgroundColor: headerBarStyleColorMap[props.barStyle ?? HeaderBarStyle.default],
+ transitionProperty: 'height, min-height',
+ transitionDuration: '250ms',
+ transitionTimingFunction: 'ease-in-out',
}));
const HeaderBarContent = styled.div({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
- // In views without the brand components we still want the Header to have the same height.
- minHeight: '51px',
+ height: '38px',
});
interface IHeaderBarProps {
barStyle?: HeaderBarStyle;
className?: string;
children?: React.ReactNode;
+ showAccountInfo?: boolean;
}
export default function HeaderBar(props: IHeaderBarProps) {
- const unpinnedWindow = useSelector(
- (state: IReduxState) => state.settings.guiSettings.unpinnedWindow,
- );
+ const unpinnedWindow = useSelector((state) => state.settings.guiSettings.unpinnedWindow);
return (
<HeaderBarContainer
barStyle={props.barStyle}
className={props.className}
+ accountInfoVisible={props.showAccountInfo ?? false}
unpinnedWindow={unpinnedWindow}>
<HeaderBarContent>{props.children}</HeaderBarContent>
+ {props.showAccountInfo && <HeaderBarDeviceInfo />}
</HeaderBarContainer>
);
}
@@ -78,12 +87,68 @@ const Title = styled(ImageView)({
export function Brand(props: React.HTMLAttributes<HTMLDivElement>) {
return (
<BrandContainer {...props}>
- <ImageView width={44} height={44} source="logo-icon" />
- <Title height={18} source="logo-text" />
+ <ImageView width={38} height={38} source="logo-icon" />
+ <Title height={15.4} source="logo-text" />
</BrandContainer>
);
}
+const StyledAccountInfo = styled.div({
+ display: 'flex',
+ marginTop: '2px',
+ maxWidth: '100%',
+});
+
+const StyledDeviceLabel = styled.div(tinyText, {
+ fontSize: '10px',
+ color: colors.white80,
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+});
+
+const StyledTimeLeftLabel = styled.div(tinyText, {
+ fontSize: '10px',
+ color: colors.white80,
+ marginLeft: '16px',
+ whiteSpace: 'nowrap',
+});
+
+function HeaderBarDeviceInfo() {
+ const deviceName = useSelector((state) => state.account.deviceName);
+ const accountExpiry = useSelector((state) => state.account.expiry);
+ const isOutOfTime = accountExpiry ? hasExpired(accountExpiry) : false;
+ const formattedExpiry = isOutOfTime
+ ? sprintf(messages.ngettext('1 day', '%d days', 0), 0)
+ : accountExpiry
+ ? formatRemainingTime(accountExpiry)
+ : '';
+
+ return (
+ <StyledAccountInfo>
+ <StyledDeviceLabel>
+ {sprintf(
+ // TRANSLATORS: A label that will display the newly created device name to inform the user
+ // TRANSLATORS: about it.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(deviceName)s - The name of the current device
+ messages.pgettext('device-management', 'Device name: %(deviceName)s'),
+ {
+ deviceName: capitalizeEveryWord(deviceName ?? ''),
+ },
+ )}
+ </StyledDeviceLabel>
+ {accountExpiry && !closeToExpiry(accountExpiry) && !isOutOfTime && (
+ <StyledTimeLeftLabel>
+ {sprintf(messages.pgettext('device-management', 'Time left: %(timeLeft)s'), {
+ timeLeft: formattedExpiry,
+ })}
+ </StyledTimeLeftLabel>
+ )}
+ </StyledAccountInfo>
+ );
+}
+
const HeaderBarSettingsButtonContainer = styled.button({
cursor: 'default',
padding: 0,
@@ -92,6 +157,10 @@ const HeaderBarSettingsButtonContainer = styled.button({
border: 'none',
});
+const HeaderBarAccountButtonContainer = styled(HeaderBarSettingsButtonContainer)({
+ marginRight: '16px',
+});
+
interface IHeaderBarSettingsButtonProps {
disabled?: boolean;
}
@@ -120,12 +189,37 @@ export function HeaderBarSettingsButton(props: IHeaderBarSettingsButtonProps) {
);
}
+export function HeaderBarAccountButton() {
+ const history = useHistory();
+ const openAccount = useCallback(
+ () => history.push(RoutePath.account, { transition: transitions.show }),
+ [history],
+ );
+
+ return (
+ <HeaderBarAccountButtonContainer
+ onClick={openAccount}
+ aria-label={messages.gettext('Account settings')}>
+ <ImageView
+ height={24}
+ width={24}
+ source="icon-account"
+ tintColor={colors.white60}
+ tintHoverColor={colors.white80}
+ />
+ </HeaderBarAccountButtonContainer>
+ );
+}
+
export function DefaultHeaderBar(props: IHeaderBarProps) {
+ const loggedIn = useSelector((state) => state.account.status.type === 'ok');
+
return (
- <HeaderBar {...props}>
+ <HeaderBar showAccountInfo={loggedIn} {...props}>
<FocusFallback>
<Brand />
</FocusFallback>
+ {loggedIn && <HeaderBarAccountButton />}
<HeaderBarSettingsButton />
</HeaderBar>
);
diff --git a/gui/src/renderer/components/RedeemVoucher.tsx b/gui/src/renderer/components/RedeemVoucher.tsx
index 34cc89c4b0..7f637ab125 100644
--- a/gui/src/renderer/components/RedeemVoucher.tsx
+++ b/gui/src/renderer/components/RedeemVoucher.tsx
@@ -217,7 +217,10 @@ export function RedeemVoucherAlert(props: IRedeemVoucherAlertProps) {
const locale = useSelector((state) => state.userInterface.locale);
if (response?.type === 'success') {
- const duration = formatRelativeDate(response.secondsAdded * 1000, 0);
+ const duration = formatRelativeDate(0, response.secondsAdded * 1000, {
+ capitalize: true,
+ displayMonths: true,
+ });
const expiry = formatDate(response.newExpiry, locale);
return (
diff --git a/gui/src/renderer/components/Settings.tsx b/gui/src/renderer/components/Settings.tsx
index 1a316f81bd..ce84ae1661 100644
--- a/gui/src/renderer/components/Settings.tsx
+++ b/gui/src/renderer/components/Settings.tsx
@@ -1,7 +1,6 @@
import { useCallback, useEffect } from 'react';
import { colors, links } from '../../config.json';
-import { formatRemainingTime, hasExpired } from '../../shared/account-expiry';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { useHistory } from '../lib/history';
@@ -17,7 +16,6 @@ import {
StyledCellIcon,
StyledContent,
StyledNavigationScrollbars,
- StyledOutOfTimeSubText,
StyledQuitButton,
StyledSettingsContent,
} from './SettingsStyles';
@@ -63,7 +61,6 @@ export default function Support() {
{showSubSettings ? (
<>
<Cell.Group>
- <AccountButton />
<UserInterfaceSettingsButton />
<VpnSettingsButton />
</Cell.Group>
@@ -102,30 +99,6 @@ export default function Support() {
);
}
-function AccountButton() {
- const history = useHistory();
- const navigate = useCallback(() => history.push(RoutePath.accountSettings), [history]);
-
- const accountExpiry = useSelector((state) => state.account.expiry);
- const isOutOfTime = accountExpiry ? hasExpired(accountExpiry) : false;
- const formattedExpiry = accountExpiry ? formatRemainingTime(accountExpiry).toUpperCase() : '';
- const outOfTimeMessage = messages.pgettext('settings-view', 'OUT OF TIME');
-
- return (
- <Cell.CellNavigationButton onClick={navigate}>
- <Cell.Label>
- {
- // TRANSLATORS: Navigation button to the 'Account' view
- messages.pgettext('settings-view', 'Account')
- }
- </Cell.Label>
- <StyledOutOfTimeSubText isOutOfTime={isOutOfTime}>
- {isOutOfTime ? outOfTimeMessage : formattedExpiry}
- </StyledOutOfTimeSubText>
- </Cell.CellNavigationButton>
- );
-}
-
function UserInterfaceSettingsButton() {
const history = useHistory();
const navigate = useCallback(() => history.push(RoutePath.userInterfaceSettings), [history]);
diff --git a/gui/src/renderer/components/SettingsStyles.tsx b/gui/src/renderer/components/SettingsStyles.tsx
index 57e7ecc7ad..7a3d2c04ae 100644
--- a/gui/src/renderer/components/SettingsStyles.tsx
+++ b/gui/src/renderer/components/SettingsStyles.tsx
@@ -1,15 +1,10 @@
import styled from 'styled-components';
-import { colors } from '../../config.json';
import * as AppButton from './AppButton';
import * as Cell from './cell';
import { measurements } from './common-styles';
import { NavigationScrollbars } from './NavigationBar';
-export const StyledOutOfTimeSubText = styled(Cell.SubText)((props: { isOutOfTime: boolean }) => ({
- color: props.isOutOfTime ? colors.red : undefined,
-}));
-
export const StyledCellIcon = styled(Cell.UntintedIcon)({
marginRight: '8px',
});
diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts
index a0248d940e..0be9983769 100644
--- a/gui/src/renderer/lib/routes.ts
+++ b/gui/src/renderer/lib/routes.ts
@@ -10,7 +10,7 @@ export enum RoutePath {
setupFinished = '/main/setup-finished',
settings = '/settings',
selectLanguage = '/settings/language',
- accountSettings = '/settings/account',
+ account = '/account',
userInterfaceSettings = '/settings/interface',
vpnSettings = '/settings/vpn',
wireguardSettings = '/settings/advanced/wireguard',
diff --git a/gui/src/shared/account-expiry.ts b/gui/src/shared/account-expiry.ts
index 85d0d78315..1b40848220 100644
--- a/gui/src/shared/account-expiry.ts
+++ b/gui/src/shared/account-expiry.ts
@@ -1,5 +1,10 @@
-import { dateByAddingComponent, DateComponent, DateType, formatTimeLeft } from './date-helper';
-import { capitalize } from './string-helpers';
+import {
+ dateByAddingComponent,
+ DateComponent,
+ DateType,
+ FormatDateOptions,
+ formatRelativeDate,
+} from './date-helper';
export function hasExpired(expiry: DateType): boolean {
return new Date(expiry).getTime() < Date.now();
@@ -18,10 +23,6 @@ export function formatDate(date: DateType, locale: string): string {
);
}
-export function formatRemainingTime(
- expiry: DateType,
- shouldCapitalizeFirstLetter?: boolean,
-): string {
- const remaining = formatTimeLeft(new Date(), expiry);
- return shouldCapitalizeFirstLetter ? capitalize(remaining) : remaining;
+export function formatRemainingTime(expiry: DateType, options?: FormatDateOptions): string {
+ return formatRelativeDate(new Date(), expiry, options);
}
diff --git a/gui/src/shared/date-helper.ts b/gui/src/shared/date-helper.ts
index 017b2048f5..be83473cfa 100644
--- a/gui/src/shared/date-helper.ts
+++ b/gui/src/shared/date-helper.ts
@@ -1,6 +1,7 @@
import { sprintf } from 'sprintf-js';
import { messages } from './gettext';
+import { capitalize } from './string-helpers';
export type DateType = Date | string | number;
@@ -72,74 +73,53 @@ export class DateDiff {
}
}
+export interface FormatDateOptions {
+ suffix?: boolean;
+ displayMonths?: boolean;
+ capitalize?: boolean;
+}
+
+// If withSuffix is true then "left" will be added at the end of the remaining time.
+// If noMonths is true then the following applies:
+// If a user has more than 2 years (730 days) left of time it should be displayed in whole years
+// rounded down If a user has less than 2 years left (e.g. 729 days) then this should be displayed
+// in days.
export function formatRelativeDate(
fromDate: DateType,
toDate: DateType,
- withSuffix = false,
+ options?: FormatDateOptions,
): string {
const diff = new DateDiff(fromDate, toDate);
const years = Math.abs(diff.years);
const months = Math.abs(diff.months);
const days = Math.abs(diff.days);
- const hours = Math.abs(diff.hours);
- const minutes = Math.abs(diff.minutes);
- if (!withSuffix) {
- if (years > 0) {
- return sprintf(messages.ngettext('1 year', '%d years', years), years);
- } else if (months >= 3) {
- return sprintf(messages.ngettext('1 month', '%d months', months), months);
+ if (isNaN(years) || isNaN(months) || isNaN(days)) {
+ return '';
+ }
+
+ let result = '';
+ if (!options?.suffix) {
+ if (options?.displayMonths ? years > 0 : days >= 730) {
+ result = sprintf(messages.ngettext('1 year', '%d years', years), years);
+ } else if (options?.displayMonths && months >= 3) {
+ result = sprintf(messages.ngettext('1 month', '%d months', months), months);
} else if (days > 0) {
- return sprintf(messages.ngettext('1 day', '%d days', days), days);
+ result = sprintf(messages.ngettext('1 day', '%d days', days), days);
} else {
- return messages.gettext('less than a day');
+ result = messages.gettext('less than a day');
}
} else if (diff.milliseconds > 0) {
- if (years > 0) {
- return sprintf(messages.ngettext('1 year left', '%d years left', years), years);
- } else if (months >= 3) {
- return sprintf(messages.ngettext('1 month left', '%d months left', months), months);
+ if (options?.displayMonths ? years > 0 : days >= 730) {
+ result = sprintf(messages.ngettext('1 year left', '%d years left', years), years);
+ } else if (options?.displayMonths && months >= 3) {
+ result = sprintf(messages.ngettext('1 month left', '%d months left', months), months);
} else if (days > 0) {
- return sprintf(messages.ngettext('1 day left', '%d days left', days), days);
+ result = sprintf(messages.ngettext('1 day left', '%d days left', days), days);
} else {
- return messages.gettext('less than a day left');
- }
- } else {
- if (years > 0) {
- return sprintf(messages.ngettext('a year ago', '%d years ago', years), years);
- } else if (months > 0) {
- return sprintf(messages.ngettext('a month ago', '%d months ago', months), months);
- } else if (days > 0) {
- return sprintf(messages.ngettext('a day ago', '%d days ago', days), days);
- } else if (hours > 0) {
- return sprintf(messages.ngettext('an hour ago', '%d hours ago', hours), hours);
- } else if (minutes > 0) {
- return sprintf(messages.ngettext('a minute ago', '%d minutes ago', minutes), minutes);
- } else {
- return messages.gettext('less than a minute ago');
+ result = messages.gettext('less than a day left');
}
}
-}
-/**
- * If a user has more than 2 years (730 days) left of time it should be displayed in whole years rounded down
- * If a user has less than 2 years left (e.g. 729 days) then this should be displayed in days.
- *
- * @param fromDate
- * @param toDate
- */
-export const formatTimeLeft = (fromDate: DateType, toDate: DateType): string => {
- const diff = new DateDiff(fromDate, toDate);
- const years = Math.abs(diff.years);
- const days = Math.abs(diff.days);
-
- if (days < 1) {
- return messages.gettext('less than a day left');
- }
-
- if (days < 730) {
- return sprintf(messages.ngettext('1 day left', '%d days left', days), days);
- }
-
- return sprintf(messages.ngettext('1 year left', '%d years left', years), years);
-};
+ return options?.capitalize ? capitalize(result) : result;
+}
diff --git a/gui/src/shared/notifications/close-to-account-expiry.ts b/gui/src/shared/notifications/close-to-account-expiry.ts
index a3f5e749ad..60feb7ec4d 100644
--- a/gui/src/shared/notifications/close-to-account-expiry.ts
+++ b/gui/src/shared/notifications/close-to-account-expiry.ts
@@ -3,7 +3,6 @@ import { sprintf } from 'sprintf-js';
import { links } from '../../config.json';
import { messages } from '../../shared/gettext';
import { closeToExpiry, formatRemainingTime } from '../account-expiry';
-import { formatRelativeDate } from '../date-helper';
import {
InAppNotification,
InAppNotificationProvider,
@@ -34,7 +33,7 @@ export class CloseToAccountExpiryNotificationProvider
'Account credit expires in %(duration)s. Buy more credit.',
),
{
- duration: formatRelativeDate(new Date(), this.context.accountExpiry),
+ duration: formatRemainingTime(this.context.accountExpiry),
},
);
@@ -54,7 +53,12 @@ export class CloseToAccountExpiryNotificationProvider
public getInAppNotification(): InAppNotification {
const subtitle = sprintf(
messages.pgettext('in-app-notifications', '%(duration)s. Buy more credit.'),
- { duration: formatRemainingTime(this.context.accountExpiry, true) },
+ {
+ duration: formatRemainingTime(this.context.accountExpiry, {
+ capitalize: true,
+ suffix: true,
+ }),
+ },
);
return {
diff --git a/gui/test/e2e/installed/state-dependent/login.spec.ts b/gui/test/e2e/installed/state-dependent/login.spec.ts
index 6701cfbd1b..b860de010c 100644
--- a/gui/test/e2e/installed/state-dependent/login.spec.ts
+++ b/gui/test/e2e/installed/state-dependent/login.spec.ts
@@ -92,12 +92,8 @@ test('App should log in', async () => {
test('App should log out', async () => {
expect(await util.waitForNavigation(() => {
- void page.getByLabel('Settings').click();
- })).toEqual(RoutePath.settings);
-
- expect(await util.waitForNavigation(() => {
void page.getByText('Account').click();
- })).toEqual(RoutePath.accountSettings);
+ })).toEqual(RoutePath.account);
expect(await util.waitForNavigation(() => {
void page.getByText('Log out').click();
diff --git a/gui/test/e2e/mocked/settings.spec.ts b/gui/test/e2e/mocked/settings.spec.ts
index e495c68b25..c6022678f5 100644
--- a/gui/test/e2e/mocked/settings.spec.ts
+++ b/gui/test/e2e/mocked/settings.spec.ts
@@ -9,27 +9,20 @@ let util: MockedTestUtils;
test.beforeAll(async () => {
({ page, util } = await startMockedApp());
- await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
});
test.afterAll(async () => {
await page.close();
});
-test('Settings Page', async () => {
- const title = page.locator('h1');
- await expect(title).toContainText('Settings');
-
- const closeButton = page.locator('button[aria-label="Close"]');
- await expect(closeButton).toBeVisible();
-});
-
test('Account button should be displayed correctly', async () => {
- const accountButton = page.locator('button:has-text("Account")');
+ const accountButton = page.getByLabel('Account settings');
await expect(accountButton).toBeVisible();
+});
- let expiryText = accountButton.locator('span');
- await expect(expiryText).toContainText(/29 days left/i);
+test('Headerbar account info should be displayed correctly', async () => {
+ let expiryText = page.getByText(/^Time left:/);
+ await expect(expiryText).toContainText(/Time left: 29 days/i);
/**
* 729 days left
@@ -39,8 +32,7 @@ test('Account button should be displayed correctly', async () => {
channel: 'account-',
response: { expiry: new Date(Date.now() + 730 * 24 * 60 * 60 * 1000 - 1000).toISOString() },
});
- expiryText = accountButton.locator('span');
- await expect(expiryText).toContainText(/729 days left/i);
+ await expect(expiryText).toContainText(/Time left: 729 days/i);
/**
* 2 years left
@@ -49,8 +41,7 @@ test('Account button should be displayed correctly', async () => {
channel: 'account-',
response: { expiry: new Date(Date.now() + 731 * 24 * 60 * 60 * 1000).toISOString() },
});
- expiryText = accountButton.locator('span');
- await expect(expiryText).toContainText(/2 years left/i);
+ await expect(expiryText).toContainText(/Time left: 2 years/i);
/**
* Expiry 1 day ago should show 'out of time'
@@ -59,6 +50,15 @@ test('Account button should be displayed correctly', async () => {
channel: 'account-',
response: { expiry: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString() },
});
- expiryText = accountButton.locator('span');
- await expect(expiryText).toContainText(/out of time/i);
+ await expect(expiryText).not.toBeVisible();
+});
+
+test('Settings Page', async () => {
+ await util.waitForNavigation(() => page.click('button[aria-label="Settings"]'));
+
+ const title = page.locator('h1');
+ await expect(title).toContainText('Settings');
+
+ const closeButton = page.locator('button[aria-label="Close"]');
+ await expect(closeButton).toBeVisible();
});
diff --git a/gui/test/unit/date-helper.spec.ts b/gui/test/unit/date-helper.spec.ts
index 5f44c53a2d..74500ff960 100644
--- a/gui/test/unit/date-helper.spec.ts
+++ b/gui/test/unit/date-helper.spec.ts
@@ -100,66 +100,89 @@ describe('Date helper', () => {
});
it('should format positive difference as string', () => {
- const diff1 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-01-01 13:37:20');
+ const diff1 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2021-01-01 13:37:20',
+ { displayMonths: true },
+ );
expect(diff1).to.equal('less than a day');
- const diff2 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-01-02 13:37:20');
+ const diff2 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2021-01-02 13:37:20',
+ { displayMonths: true },
+ );
expect(diff2).to.equal('1 day');
- const diff3 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-02-25 13:37:20');
+ const diff3 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2021-02-25 13:37:20',
+ { displayMonths: true },
+ );
expect(diff3).to.equal('55 days');
- const diff4 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-04-25 13:37:20');
+ const diff4 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2021-04-25 13:37:20',
+ { displayMonths: true },
+ );
expect(diff4).to.equal('3 months');
- const diff5 = date.formatRelativeDate('2021-01-01 13:37:10', '2031-04-25 13:37:20');
+ const diff5 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2031-04-25 13:37:20',
+ { displayMonths: true },
+ );
expect(diff5).to.equal('10 years');
});
it('should format positive difference as string suffixed with "left"', () => {
- const diff1 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-01-01 13:37:20', true);
+ const diff1 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2021-01-01 13:37:20',
+ { suffix: true, displayMonths: true },
+ );
expect(diff1).to.equal('less than a day left');
- const diff2 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-01-02 13:37:20', true);
+ const diff2 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2021-01-02 13:37:20',
+ { suffix: true, displayMonths: true },
+ );
expect(diff2).to.equal('1 day left');
- const diff3 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-02-25 13:37:20', true);
+ const diff3 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2021-02-25 13:37:20',
+ { suffix: true, displayMonths: true },
+ );
expect(diff3).to.equal('55 days left');
- const diff4 = date.formatRelativeDate('2021-01-01 13:37:10', '2021-04-25 13:37:20', true);
+ const diff4 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2021-04-25 13:37:20',
+ { suffix: true, displayMonths: true },
+ );
expect(diff4).to.equal('3 months left');
- const diff5 = date.formatRelativeDate('2021-01-01 13:37:10', '2031-04-25 13:37:20', true);
+ const diff5 = date.formatRelativeDate(
+ '2021-01-01 13:37:10',
+ '2031-04-25 13:37:20',
+ { suffix: true, displayMonths: true },
+ );
expect(diff5).to.equal('10 years left');
});
- it('should format negative difference as string', () => {
- const diff1 = date.formatRelativeDate('2021-01-01 13:37:20', '2021-01-01 13:37:10', true);
- expect(diff1).to.equal('less than a minute ago');
-
- const diff2 = date.formatRelativeDate('2021-01-02 13:37:20', '2021-01-01 13:37:10', true);
- expect(diff2).to.equal('a day ago');
-
- const diff3 = date.formatRelativeDate('2021-02-25 13:37:20', '2021-01-01 13:37:10', true);
- expect(diff3).to.equal('a month ago');
-
- const diff4 = date.formatRelativeDate('2021-04-25 13:37:20', '2021-01-01 13:37:10', true);
- expect(diff4).to.equal('3 months ago');
-
- const diff5 = date.formatRelativeDate('2031-04-25 13:37:20', '2021-01-01 13:37:10', true);
- expect(diff5).to.equal('10 years ago');
- });
-
it('should format time left correctly', () => {
- expect(date.formatTimeLeft('2022-09-01', '2022-09-01')).to.equal('less than a day left');
- expect(date.formatTimeLeft('2022-09-01', '2022-09-02')).to.equal('1 day left');
- expect(date.formatTimeLeft('2022-09-01', '2022-09-05')).to.equal('4 days left');
- expect(date.formatTimeLeft('2022-09-01', '2022-09-30')).to.equal('29 days left');
- expect(date.formatTimeLeft('2022-09-01', '2023-09-01')).to.equal('365 days left');
- expect(date.formatTimeLeft('2022-09-01', '2024-08-30')).to.equal('729 days left');
- expect(date.formatTimeLeft('2022-09-01', '2024-08-31')).to.equal('2 years left');
- expect(date.formatTimeLeft('2022-09-01', '2024-09-05')).to.equal('2 years left');
- expect(date.formatTimeLeft('2022-09-01', '2025-08-31')).to.equal('2 years left');
- expect(date.formatTimeLeft('2022-09-01', '2025-09-01')).to.equal('3 years left');
+ expect(date.formatRelativeDate('2022-09-01', '2022-09-01')).to.equal('less than a day');
+ expect(date.formatRelativeDate('2022-09-01', '2022-09-02')).to.equal('1 day');
+ expect(date.formatRelativeDate('2022-09-01', '2022-09-05')).to.equal('4 days');
+ expect(date.formatRelativeDate('2022-09-01', '2022-09-30')).to.equal('29 days');
+ expect(date.formatRelativeDate('2022-09-01', '2023-09-01')).to.equal('365 days');
+ expect(date.formatRelativeDate('2022-09-01', '2024-08-30')).to.equal('729 days');
+ expect(date.formatRelativeDate('2022-09-01', '2024-08-31')).to.equal('2 years');
+ expect(date.formatRelativeDate('2022-09-01', '2024-09-05')).to.equal('2 years');
+ expect(date.formatRelativeDate('2022-09-01', '2025-08-31')).to.equal('2 years');
+ expect(date.formatRelativeDate('2022-09-01', '2025-09-01')).to.equal('3 years');
});
});