diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/BackupPage.tsx')
-rw-r--r-- | packages/taler-wallet-webextension/src/wallet/BackupPage.tsx | 374 |
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; +} |