diff options
Diffstat (limited to 'packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx')
-rw-r--r-- | packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx | 409 |
1 files changed, 271 insertions, 138 deletions
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx index c45458eb7..d628b68e8 100644 --- a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx @@ -1,195 +1,328 @@ /* - 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, ProviderPaymentType } from "@gnu-taler/taler-wallet-core"; -import { format, formatDuration, intervalToDuration } from "date-fns"; +import * as utils from "@gnu-taler/taler-util"; +import { + AbsoluteTime, + ProviderInfo, + ProviderPaymentStatus, + ProviderPaymentType, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { ErrorMessage } from "../components/ErrorMessage"; -import { Button, ButtonDestructive, ButtonPrimary, PaymentStatus, WalletBox, SmallLightText } from "../components/styled"; -import { useProviderStatus } from "../hooks/useProviderStatus"; +import { ErrorAlertView } from "../components/CurrentAlerts.js"; +import { ErrorMessage } from "../components/ErrorMessage.js"; +import { Loading } from "../components/Loading.js"; +import { Time } from "../components/Time.js"; +import { PaymentStatus, SmallLightText } 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 { pid: string; - onBack: () => void; + onBack: () => Promise<void>; + onPayProvider: (uri: string) => Promise<void>; + onWithdraw: (amount: string) => Promise<void>; } -export function ProviderDetailPage({ pid, onBack }: Props): VNode { - const status = useProviderStatus(pid) - if (!status) { - return <div><i18n.Translate>Loading...</i18n.Translate></div> +export function ProviderDetailPage({ + pid: providerURL, + onBack, + onPayProvider, + onWithdraw, +}: Props): VNode { + const { i18n } = useTranslationContext(); + const api = useBackendContext(); + async function getProviderInfo(): Promise<ProviderInfo | null> { + //create a first list of backup info by currency + const status = await api.wallet.call(WalletApiOperation.GetBackupInfo, {}); + + const providers = status.providers.filter( + (p) => p.syncProviderBaseUrl === providerURL, + ); + return providers.length ? providers[0] : null; } - if (!status.info) { - onBack() - return <div /> + + const state = useAsyncAsHook(getProviderInfo); + + if (!state) { + return <Loading />; } - return <ProviderView info={status.info} - onSync={status.sync} - onDelete={() => status.remove().then(onBack)} - onBack={onBack} - onExtend={() => { null }} - />; + if (state.hasError) { + return ( + <ErrorAlertView + error={alertFromError( + i18n, + i18n.str`There was an error loading the provider detail for "${providerURL}"`, + state, + )} + /> + ); + } + const info = state.response; + if (info === null) { + return ( + <Fragment> + <section> + <p> + <i18n.Translate> + There is not known provider with url "{providerURL}". + </i18n.Translate> + </p> + </section> + <footer> + <Button variant="contained" color="secondary" onClick={onBack}> + <i18n.Translate>See providers</i18n.Translate> + </Button> + <div /> + </footer> + </Fragment> + ); + } + + return ( + <ProviderView + info={info} + onSync={async () => + api.wallet + .call(WalletApiOperation.RunBackupCycle, { + providers: [providerURL], + }) + .then() + } + onPayProvider={async () => { + if (info.paymentStatus.type !== ProviderPaymentType.Pending) return; + if (!info.paymentStatus.talerUri) return; + onPayProvider(info.paymentStatus.talerUri); + }} + onWithdraw={async () => { + if (info.paymentStatus.type !== ProviderPaymentType.InsufficientBalance) + return; + onWithdraw(info.paymentStatus.amount); + }} + onDelete={() => + api.wallet + .call(WalletApiOperation.RemoveBackupProvider, { + provider: providerURL, + }) + .then(onBack) + } + onBack={onBack} + onExtend={async () => { + null; + }} + /> + ); } export interface ViewProps { info: ProviderInfo; - onDelete: () => void; - onSync: () => void; - onBack: () => void; - onExtend: () => void; + onDelete: () => Promise<void>; + onSync: () => Promise<void>; + onBack: () => Promise<void>; + onExtend: () => Promise<void>; + onPayProvider: () => Promise<void>; + onWithdraw: () => Promise<void>; } -export function ProviderView({ info, onDelete, onSync, onBack, onExtend }: ViewProps): VNode { - const lb = info?.lastSuccessfulBackupTimestamp - const isPaid = info.paymentStatus.type === ProviderPaymentType.Paid || info.paymentStatus.type === ProviderPaymentType.TermsChanged +export function ProviderView({ + info, + onDelete, + onPayProvider, + onWithdraw, + onSync, + onBack, + onExtend, +}: ViewProps): VNode { + const { i18n } = useTranslationContext(); + const lb = info.lastSuccessfulBackupTimestamp + ? AbsoluteTime.fromPreciseTimestamp(info.lastSuccessfulBackupTimestamp) + : undefined; + const isPaid = + info.paymentStatus.type === ProviderPaymentType.Paid || + info.paymentStatus.type === ProviderPaymentType.TermsChanged; return ( - <WalletBox> + <Fragment> <Error info={info} /> <header> - <h3>{info.name} <SmallLightText>{info.syncProviderBaseUrl}</SmallLightText></h3> - <PaymentStatus color={isPaid ? 'rgb(28, 184, 65)' : 'rgb(202, 60, 60)'}>{isPaid ? 'Paid' : 'Unpaid'}</PaymentStatus> + <h3> + {info.name}{" "} + <SmallLightText>{info.syncProviderBaseUrl}</SmallLightText> + </h3> + <PaymentStatus color={isPaid ? "rgb(28, 184, 65)" : "rgb(202, 60, 60)"}> + {isPaid ? "Paid" : "Unpaid"} + </PaymentStatus> </header> <section> - <p><b>Last backup:</b> {lb == null || lb.t_ms == "never" ? "never" : format(lb.t_ms, 'dd MMM yyyy')} </p> - <ButtonPrimary onClick={onSync}><i18n.Translate>Back up</i18n.Translate></ButtonPrimary> - {info.terms && <Fragment> - <p><b>Provider fee:</b> {info.terms && info.terms.annualFee} per year</p> - </Fragment> - } - <p>{descriptionByStatus(info.paymentStatus)}</p> - <ButtonPrimary disabled onClick={onExtend}><i18n.Translate>Extend</i18n.Translate></ButtonPrimary> - - {info.paymentStatus.type === ProviderPaymentType.TermsChanged && <div> - <p><i18n.Translate>terms has changed, extending the service will imply accepting the new terms of service</i18n.Translate></p> - <table> - <thead> - <tr> - <td></td> - <td><i18n.Translate>old</i18n.Translate></td> - <td> -></td> - <td><i18n.Translate>new</i18n.Translate></td> - </tr> - </thead> - <tbody> - - <tr> - <td><i18n.Translate>fee</i18n.Translate></td> - <td>{info.paymentStatus.oldTerms.annualFee}</td> - <td>-></td> - <td>{info.paymentStatus.newTerms.annualFee}</td> - </tr> - <tr> - <td><i18n.Translate>storage</i18n.Translate></td> - <td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td> - <td>-></td> - <td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td> - </tr> - </tbody> - </table> - </div>} + <p> + <b> + <i18n.Translate>Last backup</i18n.Translate>: + </b>{" "} + <Time timestamp={lb} format="dd MMMM yyyy" /> + </p> + <Button variant="contained" onClick={onSync}> + <i18n.Translate>Back up</i18n.Translate> + </Button> + {info.terms && ( + <Fragment> + <p> + <b> + <i18n.Translate>Provider fee</i18n.Translate>: + </b>{" "} + {info.terms && info.terms.annualFee}{" "} + <i18n.Translate>per year</i18n.Translate> + </p> + </Fragment> + )} + <p>{descriptionByStatus(info.paymentStatus, i18n)}</p> + <Button variant="contained" disabled onClick={onExtend}> + <i18n.Translate>Extend</i18n.Translate> + </Button> + {info.paymentStatus.type === ProviderPaymentType.TermsChanged && ( + <div> + <p> + <i18n.Translate> + terms has changed, extending the service will imply accepting + the new terms of service + </i18n.Translate> + </p> + <table> + <thead> + <tr> + <td> </td> + <td> + <i18n.Translate>old</i18n.Translate> + </td> + <td> -></td> + <td> + <i18n.Translate>new</i18n.Translate> + </td> + </tr> + </thead> + <tbody> + <tr> + <td> + <i18n.Translate>fee</i18n.Translate> + </td> + <td>{info.paymentStatus.oldTerms.annualFee}</td> + <td>-></td> + <td>{info.paymentStatus.newTerms.annualFee}</td> + </tr> + <tr> + <td> + <i18n.Translate>storage</i18n.Translate> + </td> + <td>{info.paymentStatus.oldTerms.storageLimitInMegabytes}</td> + <td>-></td> + <td>{info.paymentStatus.newTerms.storageLimitInMegabytes}</td> + </tr> + </tbody> + </table> + </div> + )} </section> <footer> - <Button onClick={onBack}><i18n.Translate> < back</i18n.Translate></Button> + <Button variant="contained" color="secondary" onClick={onBack}> + <i18n.Translate>See providers</i18n.Translate> + </Button> <div> - <ButtonDestructive onClick={onDelete}><i18n.Translate>remove provider</i18n.Translate></ButtonDestructive> + <Button variant="contained" color="error" onClick={onDelete}> + <i18n.Translate>Remove provider</i18n.Translate> + </Button> + {info.paymentStatus.type === ProviderPaymentType.Pending && + info.paymentStatus.talerUri ? ( + <Button variant="contained" color="primary" onClick={onPayProvider}> + <i18n.Translate>Pay</i18n.Translate> + </Button> + ) : undefined} + {info.paymentStatus.type === + ProviderPaymentType.InsufficientBalance ? ( + <Button variant="contained" color="primary" onClick={onWithdraw}> + <i18n.Translate>Withdraw</i18n.Translate> + </Button> + ) : undefined} </div> </footer> - </WalletBox> - ) -} - -function daysSince(d?: Timestamp) { - if (!d || d.t_ms === 'never') return 'never synced' - const duration = intervalToDuration({ - start: d.t_ms, - end: new Date(), - }) - const str = formatDuration(duration, { - delimiter: ', ', - format: [ - duration?.years ? i18n.str`years` : ( - duration?.months ? i18n.str`months` : ( - duration?.days ? i18n.str`days` : ( - duration?.hours ? i18n.str`hours` : ( - duration?.minutes ? i18n.str`minutes` : i18n.str`seconds` - ) - ) - ) - ) - ] - }) - return `synced ${str} ago` + </Fragment> + ); } -function Error({ info }: { info: ProviderInfo }) { +function Error({ info }: { info: ProviderInfo }): VNode { + const { i18n } = useTranslationContext(); if (info.lastError) { - return <ErrorMessage title={info.lastError.hint} /> + return ( + <ErrorMessage + title={i18n.str`This provider has reported an error`} + description={info.lastError.hint} + /> + ); } if (info.backupProblem) { switch (info.backupProblem.type) { case "backup-conflicting-device": - return <ErrorMessage title={<Fragment> - <i18n.Translate>There is conflict with another backup from <b>{info.backupProblem.otherDeviceId}</b></i18n.Translate> - </Fragment>} /> + return ( + <ErrorMessage + title={i18n.str`There is conflict with another backup from "${info.backupProblem.otherDeviceId}"`} + /> + ); case "backup-unreadable": - return <ErrorMessage title="Backup is not readable" /> + return <ErrorMessage title={i18n.str`Backup is not readable`} />; default: - return <ErrorMessage title={<Fragment> - <i18n.Translate>Unknown backup problem: {JSON.stringify(info.backupProblem)}</i18n.Translate> - </Fragment>} /> + return ( + <ErrorMessage + title={i18n.str`Unknown backup problem: ${JSON.stringify( + info.backupProblem, + )}`} + /> + ); } } - return null + return <Fragment />; } -function colorByStatus(status: ProviderPaymentType) { - switch (status) { - case ProviderPaymentType.InsufficientBalance: - return 'rgb(223, 117, 20)' - case ProviderPaymentType.Unpaid: - return 'rgb(202, 60, 60)' - case ProviderPaymentType.Paid: - return 'rgb(28, 184, 65)' - case ProviderPaymentType.Pending: - return 'gray' - case ProviderPaymentType.InsufficientBalance: - return 'rgb(202, 60, 60)' - case ProviderPaymentType.TermsChanged: - return 'rgb(202, 60, 60)' - } -} - -function descriptionByStatus(status: ProviderPaymentStatus) { +function descriptionByStatus( + status: ProviderPaymentStatus, + i18n: typeof utils.i18n, +): VNode { switch (status.type) { - // return i18n.str`no enough balance to make the payment` - // return i18n.str`not paid yet` case ProviderPaymentType.Paid: case ProviderPaymentType.TermsChanged: - if (status.paidUntil.t_ms === 'never') { - return i18n.str`service paid` - } else { - return <Fragment> - <b>Backup valid until:</b> {format(status.paidUntil.t_ms, 'dd MMM yyyy')} - </Fragment> + if (status.paidUntil.t_ms === "never") { + return ( + <span> + <i18n.Translate>service paid</i18n.Translate> + </span> + ); } + return ( + <Fragment> + <b> + <i18n.Translate>Backup valid until</i18n.Translate>: + </b>{" "} + <Time timestamp={status.paidUntil} format="dd MMM yyyy" /> + </Fragment> + ); + case ProviderPaymentType.Unpaid: case ProviderPaymentType.InsufficientBalance: case ProviderPaymentType.Pending: - return '' + return <span />; } } |