summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-02-09 12:52:43 +0100
committerOskar Nyberg <oskar@mullvad.net>2022-02-09 12:52:43 +0100
commit98616b7e7ae6c8722b970895a5756d32872a2b79 (patch)
treea3878604e9a72ed75bf1c4c1989958c8610b9e46 /gui/src
parent5d0be2864ca826669681c0cd3bd818629096225e (diff)
parentf584c3f3bcdbf508ef7490c5586fc83f3d051805 (diff)
downloadmullvadvpn-98616b7e7ae6c8722b970895a5756d32872a2b79.tar.xz
mullvadvpn-98616b7e7ae6c8722b970895a5756d32872a2b79.zip
Merge branch 'fix-time-added-display'
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/index.ts8
-rw-r--r--gui/src/renderer/app.tsx11
-rw-r--r--gui/src/renderer/components/ExpiredAccountAddTime.tsx59
-rw-r--r--gui/src/renderer/components/RedeemVoucher.tsx22
-rw-r--r--gui/src/renderer/lib/history.tsx28
-rw-r--r--gui/src/renderer/lib/routes.ts13
-rw-r--r--gui/src/renderer/redux/account/actions.ts4
-rw-r--r--gui/src/renderer/redux/account/reducers.ts5
-rw-r--r--gui/src/shared/daemon-rpc-types.ts1
9 files changed, 84 insertions, 67 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 5b7504e711..bb84d520fe 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -230,13 +230,7 @@ class ApplicationMain {
return this.daemonRpc.getAccountData(accountToken);
},
(accountData) => {
- this.accountData = accountData && {
- ...accountData,
- previousExpiry:
- accountData.expiry !== this.accountData?.expiry
- ? this.accountData?.expiry
- : this.accountData?.previousExpiry,
- };
+ this.accountData = accountData;
if (this.windowController) {
IpcMainEventChannel.account.notify(this.windowController.webContents, this.accountData);
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index f5134653a7..e9aadedcc4 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -120,7 +120,7 @@ export default class AppRenderer {
});
IpcRendererEventChannel.account.listen((newAccountData?: IAccountData) => {
- this.setAccountExpiry(newAccountData?.expiry, newAccountData?.previousExpiry);
+ this.setAccountExpiry(newAccountData?.expiry);
});
IpcRendererEventChannel.accountHistory.listen((newAccountHistory?: AccountToken) => {
@@ -199,10 +199,7 @@ export default class AppRenderer {
initialState.translations.relayLocations,
);
- this.setAccountExpiry(
- initialState.accountData?.expiry,
- initialState.accountData?.previousExpiry,
- );
+ this.setAccountExpiry(initialState.accountData?.expiry);
this.setSettings(initialState.settings);
this.handleAccountChange(undefined, initialState.settings.accountToken);
this.setAccountHistory(initialState.accountHistory);
@@ -834,8 +831,8 @@ export default class AppRenderer {
this.reduxActions.settings.updateGuiSettings(guiSettings);
}
- private setAccountExpiry(expiry?: string, previousExpiry?: string) {
- this.reduxActions.account.updateAccountExpiry(expiry, previousExpiry);
+ private setAccountExpiry(expiry?: string) {
+ this.reduxActions.account.updateAccountExpiry(expiry);
}
private storeAutoStart(autoStart: boolean) {
diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
index 49e8bf8107..4c6c7989d7 100644
--- a/gui/src/renderer/components/ExpiredAccountAddTime.tsx
+++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
@@ -1,16 +1,16 @@
import { useCallback } from 'react';
-import { useSelector } from 'react-redux';
+import { useSelector } from '../redux/store';
import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
import { links, colors } from '../../config.json';
+import { formatDate } from '../../shared/account-expiry';
import { formatRelativeDate } from '../../shared/date-helper';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
import { transitions, useHistory } from '../lib/history';
-import { RoutePath } from '../lib/routes';
+import { generateRoutePath, RoutePath } from '../lib/routes';
import account from '../redux/account/actions';
-import { IReduxState } from '../redux/store';
import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import { bigText } from './common-styles';
@@ -24,6 +24,7 @@ import {
RedeemVoucherResponse,
RedeemVoucherSubmitButton,
} from './RedeemVoucher';
+import { useParams } from 'react-router';
export const StyledHeader = styled(DefaultHeaderBar)({
flex: 0,
@@ -82,9 +83,13 @@ export const StyledStatusIcon = styled.div({
export function VoucherInput() {
const history = useHistory();
- const onSuccess = useCallback(() => {
- history.push(RoutePath.voucherSuccess);
- }, [history]);
+ const onSuccess = useCallback(
+ (newExpiry: string, secondsAdded: number) => {
+ const path = generateRoutePath(RoutePath.voucherSuccess, { newExpiry, secondsAdded });
+ history.push(path);
+ },
+ [history],
+ );
const navigateBack = useCallback(() => {
history.pop();
@@ -119,23 +124,31 @@ export function VoucherInput() {
}
export function VoucherVerificationSuccess() {
+ const { newExpiry, secondsAdded } = useParams<{ newExpiry: string; secondsAdded: string }>();
+
return (
- <TimeAdded title={messages.pgettext('connect-view', 'Voucher was successfully redeemed')} />
+ <TimeAdded
+ newExpiry={newExpiry}
+ secondsAdded={parseInt(secondsAdded)}
+ title={messages.pgettext('connect-view', 'Voucher was successfully redeemed')}
+ />
);
}
interface ITimeAddedProps {
title?: string;
+ newExpiry?: string;
+ secondsAdded?: number;
}
export function TimeAdded(props: ITimeAddedProps) {
const history = useHistory();
const finish = useFinishedCallback();
- const accountData = useSelector((state: IReduxState) => state.account);
+ const expiry = useSelector((state) => state.account.expiry);
const isNewAccount = useSelector(
- (state: IReduxState) =>
- state.account.status.type === 'ok' && state.account.status.method === 'new_account',
+ (state) => state.account.status.type === 'ok' && state.account.status.method === 'new_account',
);
+ const locale = useSelector((state) => state.userInterface.locale);
const navigateToSetupFinished = useCallback(() => {
if (isNewAccount) {
@@ -146,10 +159,8 @@ export function TimeAdded(props: ITimeAddedProps) {
}, [history, finish]);
const duration =
- (accountData.expiry &&
- accountData.previousExpiry &&
- formatRelativeDate(accountData.expiry, accountData.previousExpiry)) ??
- '';
+ props.secondsAdded !== undefined ? formatRelativeDate(props.secondsAdded * 1000, 0) : undefined;
+ const newExpiry = formatDate(props.newExpiry ?? expiry!, locale);
return (
<Layout>
@@ -164,7 +175,17 @@ export function TimeAdded(props: ITimeAddedProps) {
{props.title ?? messages.pgettext('connect-view', 'Time was successfully added')}
</StyledTitle>
<StyledLabel>
- {sprintf(messages.gettext('%(duration)s was added to your account.'), { duration })}
+ {duration
+ ? sprintf(
+ messages.gettext('%(duration)s was added, account paid until %(expiry)s.'),
+ {
+ duration,
+ expiry: newExpiry,
+ },
+ )
+ : sprintf(messages.gettext('Account paid until %(expiry)s.'), {
+ expiry: newExpiry,
+ })}
</StyledLabel>
</StyledBody>
@@ -238,10 +259,9 @@ export function SetupFinished() {
function HeaderBar() {
const isNewAccount = useSelector(
- (state: IReduxState) =>
- state.account.status.type === 'ok' && state.account.status.method === 'new_account',
+ (state) => state.account.status.type === 'ok' && state.account.status.method === 'new_account',
);
- const tunnelState = useSelector((state: IReduxState) => state.connection.status);
+ const tunnelState = useSelector((state) => state.connection.status);
const headerBarStyle = isNewAccount
? HeaderBarStyle.default
: calculateHeaderBarStyle(tunnelState);
@@ -254,8 +274,7 @@ function useFinishedCallback() {
const history = useHistory();
const isNewAccount = useSelector(
- (state: IReduxState) =>
- state.account.status.type === 'ok' && state.account.status.method === 'new_account',
+ (state) => state.account.status.type === 'ok' && state.account.status.method === 'new_account',
);
const callback = useCallback(() => {
diff --git a/gui/src/renderer/components/RedeemVoucher.tsx b/gui/src/renderer/components/RedeemVoucher.tsx
index c7697f2f6f..7810112082 100644
--- a/gui/src/renderer/components/RedeemVoucher.tsx
+++ b/gui/src/renderer/components/RedeemVoucher.tsx
@@ -1,13 +1,13 @@
import React, { useCallback, useContext, useState } from 'react';
-import { useSelector } from 'react-redux';
+import { useSelector } from '../redux/store';
import { sprintf } from 'sprintf-js';
+import { formatDate } from '../../shared/account-expiry';
import { VoucherResponse } from '../../shared/daemon-rpc-types';
import { formatRelativeDate } from '../../shared/date-helper';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
import accountActions from '../redux/account/actions';
-import { IReduxState } from '../redux/store';
import * as AppButton from './AppButton';
import ImageView from './ImageView';
import { ModalAlert } from './Modal';
@@ -59,7 +59,7 @@ const RedeemVoucherContext = React.createContext<IRedeemVoucherContextValue>({
interface IRedeemVoucherProps {
onSubmit?: () => void;
- onSuccess?: () => void;
+ onSuccess?: (newExpiry: string, secondsAdded: number) => void;
onFailure?: () => void;
children?: React.ReactNode;
}
@@ -95,7 +95,7 @@ export function RedeemVoucherContainer(props: IRedeemVoucherProps) {
setSubmitting(false);
setResponse(response);
if (response.type === 'success') {
- onSuccess?.();
+ onSuccess?.(response.newExpiry, response.secondsAdded);
} else {
onFailure?.();
}
@@ -212,15 +212,12 @@ interface IRedeemVoucherAlertProps {
export function RedeemVoucherAlert(props: IRedeemVoucherAlertProps) {
const { submitting, response } = useContext(RedeemVoucherContext);
- const accountData = useSelector((state: IReduxState) => state.account);
-
- const duration =
- (accountData.expiry &&
- accountData.previousExpiry &&
- formatRelativeDate(accountData.expiry, accountData.previousExpiry)) ??
- '';
+ const locale = useSelector((state) => state.userInterface.locale);
if (response?.type === 'success') {
+ const duration = formatRelativeDate(response.secondsAdded * 1000, 0);
+ const expiry = formatDate(response.newExpiry, locale);
+
return (
<ModalAlert
buttons={[
@@ -236,8 +233,9 @@ export function RedeemVoucherAlert(props: IRedeemVoucherAlertProps) {
{messages.pgettext('redeem-voucher-view', 'Voucher was successfully redeemed.')}
</StyledTitle>
<StyledLabel>
- {sprintf(messages.gettext('%(duration)s was added to your account.'), {
+ {sprintf(messages.gettext('%(duration)s was added, account paid until %(expiry)s.'), {
duration,
+ expiry,
})}
</StyledLabel>
</ModalAlert>
diff --git a/gui/src/renderer/lib/history.tsx b/gui/src/renderer/lib/history.tsx
index 4abdcd5fcb..f5bc213112 100644
--- a/gui/src/renderer/lib/history.tsx
+++ b/gui/src/renderer/lib/history.tsx
@@ -1,7 +1,7 @@
import { Location, Action, LocationDescriptorObject, History as OriginalHistory } from 'history';
import React from 'react';
import { RouteComponentProps, useHistory as useReactRouterHistory, withRouter } from 'react-router';
-import { RoutePath } from './routes';
+import { GeneratedRoutePath, RoutePath } from './routes';
export interface ITransitionSpecification {
name: string;
@@ -38,7 +38,7 @@ export const transitions: ITransitionMap = {
},
};
-type LocationDescriptor<S> = RoutePath | LocationDescriptorObject<S>;
+type LocationDescriptor<S> = RoutePath | GeneratedRoutePath | LocationDescriptorObject<S>;
type LocationListener<S = unknown> = (
location: Location<S>,
@@ -168,7 +168,11 @@ export default class History {
}
private createLocation(location: LocationDescriptor<S>, state?: S): Location<S> {
- if (typeof location === 'object') {
+ if (typeof location === 'string') {
+ return this.createLocationFromString(location, state);
+ } else if ('routePath' in location) {
+ return this.createLocationFromString(location.routePath, state);
+ } else {
return {
pathname: location.pathname ?? this.location.pathname,
search: location.search ?? '',
@@ -176,17 +180,19 @@ export default class History {
state: location.state,
key: location.key ?? this.getRandomKey(),
};
- } else {
- return {
- pathname: location,
- search: '',
- hash: '',
- state,
- key: this.getRandomKey(),
- };
}
}
+ private createLocationFromString(path: string, state?: S): Location<S> {
+ return {
+ pathname: path,
+ search: '',
+ hash: '',
+ state,
+ key: this.getRandomKey(),
+ };
+ }
+
private getRandomKey() {
return Math.random().toString(36).substr(8);
}
diff --git a/gui/src/renderer/lib/routes.ts b/gui/src/renderer/lib/routes.ts
index dee83c2a42..8a63cd6de0 100644
--- a/gui/src/renderer/lib/routes.ts
+++ b/gui/src/renderer/lib/routes.ts
@@ -1,9 +1,13 @@
+import { generatePath } from 'react-router';
+
+export type GeneratedRoutePath = { routePath: string };
+
export enum RoutePath {
launch = '/',
login = '/login',
main = '/main',
redeemVoucher = '/main/voucher/redeem',
- voucherSuccess = '/main/voucher/success',
+ voucherSuccess = '/main/voucher/success/:newExpiry/:secondsAdded',
timeAdded = '/main/time-added',
setupFinished = '/main/setup-finished',
settings = '/settings',
@@ -19,3 +23,10 @@ export enum RoutePath {
selectLocation = '/select-location',
filterByProvider = '/select-location/filter-by-provider',
}
+
+export function generateRoutePath(
+ routePath: RoutePath,
+ parameters: Parameters<typeof generatePath>[1],
+): GeneratedRoutePath {
+ return { routePath: generatePath(routePath, parameters) };
+}
diff --git a/gui/src/renderer/redux/account/actions.ts b/gui/src/renderer/redux/account/actions.ts
index 4a9790df00..b8fbe94d39 100644
--- a/gui/src/renderer/redux/account/actions.ts
+++ b/gui/src/renderer/redux/account/actions.ts
@@ -50,7 +50,6 @@ interface IUpdateAccountHistoryAction {
interface IUpdateAccountExpiryAction {
type: 'UPDATE_ACCOUNT_EXPIRY';
expiry?: string;
- previousExpiry?: string;
}
export type AccountAction =
@@ -133,11 +132,10 @@ function updateAccountHistory(accountHistory?: AccountToken): IUpdateAccountHist
};
}
-function updateAccountExpiry(expiry?: string, previousExpiry?: string): IUpdateAccountExpiryAction {
+function updateAccountExpiry(expiry?: string): IUpdateAccountExpiryAction {
return {
type: 'UPDATE_ACCOUNT_EXPIRY',
expiry,
- previousExpiry,
};
}
diff --git a/gui/src/renderer/redux/account/reducers.ts b/gui/src/renderer/redux/account/reducers.ts
index 6b68fef9cf..53bc55db1b 100644
--- a/gui/src/renderer/redux/account/reducers.ts
+++ b/gui/src/renderer/redux/account/reducers.ts
@@ -10,7 +10,6 @@ export interface IAccountReduxState {
accountToken?: AccountToken;
accountHistory?: AccountToken;
expiry?: string; // ISO8601
- previousExpiry?: string; // ISO8601
status: LoginState;
}
@@ -18,7 +17,6 @@ const initialState: IAccountReduxState = {
accountToken: undefined,
accountHistory: undefined,
expiry: undefined,
- previousExpiry: undefined,
status: { type: 'none' },
};
@@ -50,7 +48,6 @@ export default function (
status: { type: 'none' },
accountToken: undefined,
expiry: undefined,
- previousExpiry: undefined,
};
case 'RESET_LOGIN_ERROR':
return {
@@ -73,7 +70,6 @@ export default function (
status: { type: 'ok', method: 'new_account' },
accountToken: action.token,
expiry: action.expiry,
- previousExpiry: undefined,
};
case 'UPDATE_ACCOUNT_TOKEN':
return {
@@ -89,7 +85,6 @@ export default function (
return {
...state,
expiry: action.expiry,
- previousExpiry: action.previousExpiry,
};
}
diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts
index 68e61fc3ea..3abde2ee64 100644
--- a/gui/src/shared/daemon-rpc-types.ts
+++ b/gui/src/shared/daemon-rpc-types.ts
@@ -1,6 +1,5 @@
export interface IAccountData {
expiry: string;
- previousExpiry?: string;
}
export type AccountToken = string;
export type Ip = string;