summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/BackupPage.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/wallet/BackupPage.tsx374
1 files changed, 292 insertions, 82 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
index 712329bf8..8a3710f69 100644
--- a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx
@@ -1,146 +1,356 @@
/*
- This file is part of TALER
- (C) 2016 GNUnet e.V.
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
- TALER is free software; you can redistribute it and/or modify it under the
+ GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-*/
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
-
-import { i18n, Timestamp } from "@gnu-taler/taler-util";
-import { ProviderInfo, ProviderPaymentStatus } from "@gnu-taler/taler-wallet-core";
-import { differenceInMonths, formatDuration, intervalToDuration } from "date-fns";
-import { Fragment, JSX, VNode, h } from "preact";
import {
- BoldLight, ButtonPrimary, ButtonSuccess, Centered,
- CenteredText, CenteredBoldText, PopupBox, RowBorderGray,
- SmallText, SmallLightText, WalletBox
-} from "../components/styled";
-import { useBackupStatus } from "../hooks/useBackupStatus";
-import { Pages } from "../NavigationBar";
+ AbsoluteTime,
+ ProviderInfo,
+ ProviderPaymentPaid,
+ ProviderPaymentStatus,
+ ProviderPaymentType,
+ stringifyRestoreUri,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ differenceInMonths,
+ formatDuration,
+ intervalToDuration,
+} from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { Pages } from "../NavigationBar.js";
+import { ErrorAlertView } from "../components/CurrentAlerts.js";
+import { Loading } from "../components/Loading.js";
+import { QR } from "../components/QR.js";
+import {
+ BoldLight,
+ Centered,
+ CenteredBoldText,
+ CenteredText,
+ RowBorderGray,
+ SmallLightText,
+ SmallText,
+ WarningBox,
+} from "../components/styled/index.js";
+import { alertFromError } from "../context/alert.js";
+import { useBackendContext } from "../context/backend.js";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { Button } from "../mui/Button.js";
interface Props {
- onAddProvider: () => void;
+ onAddProvider: () => Promise<void>;
+}
+
+export function ShowRecoveryInfo({
+ info,
+ onClose,
+}: {
+ info: string;
+ onClose: () => Promise<void>;
+}): VNode {
+ const [display, setDisplay] = useState(false);
+ const [copied, setCopied] = useState(false);
+ async function copyText(): Promise<void> {
+ navigator.clipboard.writeText(info);
+ setCopied(true);
+ }
+ useEffect(() => {
+ if (copied) {
+ setTimeout(() => {
+ setCopied(false);
+ }, 1000);
+ }
+ }, [copied]);
+ return (
+ <Fragment>
+ <h2>Wallet Recovery</h2>
+ <WarningBox>Do not share this QR or URI with anyone</WarningBox>
+ <section>
+ <p>
+ The qr code can be scanned by another wallet to keep synchronized with
+ this wallet.
+ </p>
+ <Button variant="contained" onClick={async () => setDisplay((d) => !d)}>
+ {display ? "Hide" : "Show"} QR code
+ </Button>
+ {display && <QR text={JSON.stringify(info)} />}
+ </section>
+
+ <section>
+ <p>You can also use the string version</p>
+ <Button variant="contained" disabled={copied} onClick={copyText}>
+ Copy recovery URI
+ </Button>
+ </section>
+ <footer>
+ <div></div>
+ <div>
+ <Button variant="contained" onClick={onClose}>
+ Close
+ </Button>
+ </div>
+ </footer>
+ </Fragment>
+ );
}
export function BackupPage({ onAddProvider }: Props): VNode {
- const status = useBackupStatus()
+ const { i18n } = useTranslationContext();
+ const api = useBackendContext();
+ const status = useAsyncAsHook(() =>
+ api.wallet.call(WalletApiOperation.GetBackupInfo, {}),
+ );
+ const [recoveryInfo, setRecoveryInfo] = useState<string>("");
if (!status) {
- return <div>Loading...</div>
+ return <Loading />;
+ }
+ if (status.hasError) {
+ return (
+ <ErrorAlertView
+ error={alertFromError(
+ i18n,
+ i18n.str`Could not load backup providers`,
+ status,
+ )}
+ />
+ );
+ }
+
+ async function getRecoveryInfo(): Promise<void> {
+ const r = await api.wallet.call(
+ WalletApiOperation.ExportBackupRecovery,
+ {},
+ );
+ const str = stringifyRestoreUri({
+ walletRootPriv: r.walletRootPriv,
+ providers: r.providers.map((p) => p.url),
+ });
+ setRecoveryInfo(str);
}
- return <BackupView providers={status.providers} onAddProvider={onAddProvider} onSyncAll={status.sync} />;
+
+ const providers = status.response.providers.sort((a, b) => {
+ if (
+ a.paymentStatus.type === ProviderPaymentType.Paid &&
+ b.paymentStatus.type === ProviderPaymentType.Paid
+ ) {
+ return getStatusPaidOrder(a.paymentStatus, b.paymentStatus);
+ }
+ return (
+ getStatusTypeOrder(a.paymentStatus) - getStatusTypeOrder(b.paymentStatus)
+ );
+ });
+
+ if (recoveryInfo) {
+ return (
+ <ShowRecoveryInfo
+ info={recoveryInfo}
+ onClose={async () => setRecoveryInfo("")}
+ />
+ );
+ }
+
+ return (
+ <BackupView
+ providers={providers}
+ onAddProvider={onAddProvider}
+ onSyncAll={async () =>
+ api.wallet.call(WalletApiOperation.RunBackupCycle, {}).then()
+ }
+ onShowInfo={getRecoveryInfo}
+ />
+ );
}
export interface ViewProps {
- providers: ProviderInfo[],
- onAddProvider: () => void;
+ providers: ProviderInfo[];
+ onAddProvider: () => Promise<void>;
onSyncAll: () => Promise<void>;
+ onShowInfo: () => Promise<void>;
}
-export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps): VNode {
+export function BackupView({
+ providers,
+ onAddProvider,
+ onSyncAll,
+ onShowInfo,
+}: ViewProps): VNode {
+ const { i18n } = useTranslationContext();
return (
- <WalletBox>
+ <Fragment>
<section>
- {providers.map((provider) => <BackupLayout
- status={provider.paymentStatus}
- timestamp={provider.lastSuccessfulBackupTimestamp}
- id={provider.syncProviderBaseUrl}
- active={provider.active}
- title={provider.name}
- />
+ {providers.map((provider, idx) => (
+ <BackupLayout
+ key={idx}
+ status={provider.paymentStatus}
+ timestamp={
+ provider.lastSuccessfulBackupTimestamp
+ ? AbsoluteTime.fromPreciseTimestamp(
+ provider.lastSuccessfulBackupTimestamp,
+ )
+ : undefined
+ }
+ id={provider.syncProviderBaseUrl}
+ active={provider.active}
+ title={provider.name}
+ />
+ ))}
+ {!providers.length && (
+ <Centered style={{ marginTop: 100 }}>
+ <BoldLight>
+ <i18n.Translate>No backup providers configured</i18n.Translate>
+ </BoldLight>
+ <Button variant="contained" color="success" onClick={onAddProvider}>
+ <i18n.Translate>Add provider</i18n.Translate>
+ </Button>
+ </Centered>
)}
- {!providers.length && <Centered style={{ marginTop: 100 }}>
- <BoldLight>No backup providers configured</BoldLight>
- <ButtonSuccess onClick={onAddProvider}><i18n.Translate>Add provider</i18n.Translate></ButtonSuccess>
- </Centered>}
</section>
- {!!providers.length && <footer>
- <div />
- <div>
- <ButtonPrimary onClick={onSyncAll}>{
- providers.length > 1 ?
- <i18n.Translate>Sync all backups</i18n.Translate> :
- <i18n.Translate>Sync now</i18n.Translate>
- }</ButtonPrimary>
- <ButtonSuccess onClick={onAddProvider}>Add provider</ButtonSuccess>
- </div>
- </footer>}
- </WalletBox>
- )
+ {!!providers.length && (
+ <footer>
+ <div>
+ <Button variant="contained" onClick={onShowInfo}>
+ Show recovery
+ </Button>
+ </div>
+ <div>
+ <Button variant="contained" onClick={onSyncAll}>
+ {providers.length > 1
+ ? i18n.str`Sync all backups`
+ : i18n.str`Sync now`}
+ </Button>
+ <Button variant="contained" color="success" onClick={onAddProvider}>
+ <i18n.Translate>Add provider</i18n.Translate>
+ </Button>
+ </div>
+ </footer>
+ )}
+ </Fragment>
+ );
}
interface TransactionLayoutProps {
status: ProviderPaymentStatus;
- timestamp?: Timestamp;
+ timestamp?: AbsoluteTime;
title: string;
id: string;
active: boolean;
}
-function BackupLayout(props: TransactionLayoutProps): JSX.Element {
+function BackupLayout(props: TransactionLayoutProps): VNode {
+ const { i18n } = useTranslationContext();
const date = !props.timestamp ? undefined : new Date(props.timestamp.t_ms);
const dateStr = date?.toLocaleString([], {
dateStyle: "medium",
timeStyle: "short",
} as any);
-
return (
<RowBorderGray>
<div style={{ color: !props.active ? "grey" : undefined }}>
- <a href={Pages.provider_detail.replace(':pid', encodeURIComponent(props.id))}><span>{props.title}</span></a>
+ <a
+ href={Pages.backupProviderDetail({
+ pid: encodeURIComponent(props.id),
+ })}
+ >
+ <span>{props.title}</span>
+ </a>
- {dateStr && <SmallText style={{ marginTop: 5 }}>Last synced: {dateStr}</SmallText>}
- {!dateStr && <SmallLightText style={{ marginTop: 5 }}>Not synced</SmallLightText>}
+ {dateStr && (
+ <SmallText style={{ marginTop: 5 }}>
+ <i18n.Translate>Last synced</i18n.Translate>: {dateStr}
+ </SmallText>
+ )}
+ {!dateStr && (
+ <SmallLightText style={{ marginTop: 5 }}>
+ <i18n.Translate>Not synced</i18n.Translate>
+ </SmallLightText>
+ )}
</div>
<div>
- {props.status?.type === 'paid' ?
- <ExpirationText until={props.status.paidUntil} /> :
+ {props.status?.type === "paid" ? (
+ <ExpirationText until={props.status.paidUntil} />
+ ) : (
<div>{props.status.type}</div>
- }
+ )}
</div>
</RowBorderGray>
);
}
-function ExpirationText({ until }: { until: Timestamp }) {
- return <Fragment>
- <CenteredText> Expires in </CenteredText>
- <CenteredBoldText {...({ color: colorByTimeToExpire(until) })}> {daysUntil(until)} </CenteredBoldText>
- </Fragment>
+function ExpirationText({ until }: { until: AbsoluteTime }): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <Fragment>
+ <CenteredText>
+ <i18n.Translate>Expires in</i18n.Translate>
+ </CenteredText>
+ <CenteredBoldText {...{ color: colorByTimeToExpire(until) }}>
+ {" "}
+ {daysUntil(until)}{" "}
+ </CenteredBoldText>
+ </Fragment>
+ );
}
-function colorByTimeToExpire(d: Timestamp) {
- if (d.t_ms === 'never') return 'rgb(28, 184, 65)'
- const months = differenceInMonths(d.t_ms, new Date())
- return months > 1 ? 'rgb(28, 184, 65)' : 'rgb(223, 117, 20)';
+function colorByTimeToExpire(d: AbsoluteTime): string {
+ if (d.t_ms === "never") return "rgb(28, 184, 65)";
+ const months = differenceInMonths(d.t_ms, new Date());
+ return months > 1 ? "rgb(28, 184, 65)" : "rgb(223, 117, 20)";
}
-function daysUntil(d: Timestamp) {
- if (d.t_ms === 'never') return undefined
+function daysUntil(d: AbsoluteTime): string {
+ if (d.t_ms === "never") return "";
const duration = intervalToDuration({
start: d.t_ms,
end: new Date(),
- })
+ });
const str = formatDuration(duration, {
- delimiter: ', ',
+ delimiter: ", ",
format: [
- duration?.years ? 'years' : (
- duration?.months ? 'months' : (
- duration?.days ? 'days' : (
- duration.hours ? 'hours' : 'minutes'
- )
- )
- )
- ]
- })
- return `${str}`
-} \ No newline at end of file
+ duration?.years
+ ? "years"
+ : duration?.months
+ ? "months"
+ : duration?.days
+ ? "days"
+ : duration.hours
+ ? "hours"
+ : "minutes",
+ ],
+ });
+ return `${str}`;
+}
+
+function getStatusTypeOrder(t: ProviderPaymentStatus): number {
+ return [
+ ProviderPaymentType.InsufficientBalance,
+ ProviderPaymentType.TermsChanged,
+ ProviderPaymentType.Unpaid,
+ ProviderPaymentType.Paid,
+ ProviderPaymentType.Pending,
+ ].indexOf(t.type);
+}
+
+function getStatusPaidOrder(
+ a: ProviderPaymentPaid,
+ b: ProviderPaymentPaid,
+): number {
+ return a.paidUntil.t_ms === "never"
+ ? -1
+ : b.paidUntil.t_ms === "never"
+ ? 1
+ : a.paidUntil.t_ms - b.paidUntil.t_ms;
+}