taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 550905f0e7eed38fa1ef598b4015faf10648cf1b
parent 26a12809605ac8099acf7931676bfbad0298a3f2
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu,  1 Jul 2021 15:42:32 -0300

add provider examples

Diffstat:
Mpackages/taler-wallet-webextension/src/popup/BackupPage.tsx | 2+-
Dpackages/taler-wallet-webextension/src/popup/Provider.stories.tsx | 215-------------------------------------------------------------------------------
Apackages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx | 46++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx | 218+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/taler-wallet-webextension/src/popup/ProviderPage.tsx | 174-------------------------------------------------------------------------------
Mpackages/taler-wallet-webextension/src/popup/Transaction.tsx | 14+++++++-------
Mpackages/taler-wallet-webextension/src/popup/popup.tsx | 5+++--
Mpackages/taler-wallet-webextension/src/popupEntryPoint.tsx | 27++++++++++++++-------------
11 files changed, 619 insertions(+), 412 deletions(-)

diff --git a/packages/taler-wallet-webextension/src/popup/BackupPage.tsx b/packages/taler-wallet-webextension/src/popup/BackupPage.tsx @@ -99,7 +99,7 @@ function BackupLayout(props: TransactionLayoutProps): JSX.Element { {dateStr && <div style={{ fontSize: "small", color: "gray" }}>{dateStr}</div>} {!dateStr && <div style={{ fontSize: "small", color: "red" }}>never synced</div>} <div style={{ fontVariant: "small-caps", fontSize: "x-large" }}> - <a href={Pages.provider.replace(':currency', props.id)}><span>{props.title}</span></a> + <a href={Pages.provider_detail.replace(':currency', props.id)}><span>{props.title}</span></a> </div> <div>{props.subtitle}</div> diff --git a/packages/taler-wallet-webextension/src/popup/Provider.stories.tsx b/packages/taler-wallet-webextension/src/popup/Provider.stories.tsx @@ -1,215 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - 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. - - 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 - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core'; -import { FunctionalComponent } from 'preact'; -import { ProviderView as TestedComponent } from './ProviderPage'; - -export default { - title: 'popup/backup/details', - component: TestedComponent, - argTypes: { - onRetry: { action: 'onRetry' }, - onDelete: { action: 'onDelete' }, - onBack: { action: 'onBack' }, - } -}; - - -function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { - const r = (args: any) => <Component {...args} /> - r.args = props - return r -} - -export const NotDefined = createExample(TestedComponent, { - currency: 'ARS', -}); - -export const Active = createExample(TestedComponent, { - currency: 'ARS', - info: { - "active": true, - "syncProviderBaseUrl": "http://sync.taler:9967/", - "lastSuccessfulBackupTimestamp": { - "t_ms": 1625063925078 - }, - "paymentProposalIds": [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" - ], - "paymentStatus": { - "type": ProviderPaymentType.Paid, - "paidUntil": { - "t_ms": 1656599921000 - } - }, - "terms": { - "annualFee": "ARS:1", - "storageLimitInMegabytes": 16, - "supportedProtocolVersion": "0.0" - } - } -}); - -export const ActiveErrorSync = createExample(TestedComponent, { - currency: 'ARS', - info: { - "active": true, - "syncProviderBaseUrl": "http://sync.taler:9967/", - "lastSuccessfulBackupTimestamp": { - "t_ms": 1625063925078 - }, - "paymentProposalIds": [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" - ], - "paymentStatus": { - "type": ProviderPaymentType.Paid, - "paidUntil": { - "t_ms": 1656599921000 - } - }, - lastError: { - code: 2002, - details: 'details', - hint: 'error hint from the server', - message: 'message' - }, - "terms": { - "annualFee": "ARS:1", - "storageLimitInMegabytes": 16, - "supportedProtocolVersion": "0.0" - } - } -}); - -export const ActiveBackupProblemUnreadable = createExample(TestedComponent, { - currency: 'ARS', - info: { - "active": true, - "syncProviderBaseUrl": "http://sync.taler:9967/", - "lastSuccessfulBackupTimestamp": { - "t_ms": 1625063925078 - }, - "paymentProposalIds": [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" - ], - "paymentStatus": { - "type": ProviderPaymentType.Paid, - "paidUntil": { - "t_ms": 1656599921000 - } - }, - backupProblem: { - type: 'backup-unreadable' - }, - "terms": { - "annualFee": "ARS:1", - "storageLimitInMegabytes": 16, - "supportedProtocolVersion": "0.0" - } - } -}); - -export const ActiveBackupProblemDevice = createExample(TestedComponent, { - currency: 'ARS', - info: { - "active": true, - "syncProviderBaseUrl": "http://sync.taler:9967/", - "lastSuccessfulBackupTimestamp": { - "t_ms": 1625063925078 - }, - "paymentProposalIds": [ - "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" - ], - "paymentStatus": { - "type": ProviderPaymentType.Paid, - "paidUntil": { - "t_ms": 1656599921000 - } - }, - backupProblem: { - type: 'backup-conflicting-device', - myDeviceId: 'my-device-id', - otherDeviceId: 'other-device-id', - backupTimestamp: { - "t_ms": 1656599921000 - } - }, - "terms": { - "annualFee": "ARS:1", - "storageLimitInMegabytes": 16, - "supportedProtocolVersion": "0.0" - } - } -}); - -export const InactiveUnpaid = createExample(TestedComponent, { - currency: 'ARS', - info: { - "active": false, - "syncProviderBaseUrl": "http://sync.demo.taler.net/", - "paymentProposalIds": [], - "paymentStatus": { - "type": ProviderPaymentType.Unpaid, - }, - "terms": { - "annualFee": "ARS:0.1", - "storageLimitInMegabytes": 16, - "supportedProtocolVersion": "0.0" - } - } -}); - -export const InactiveInsufficientBalance = createExample(TestedComponent, { - currency: 'ARS', - info: { - "active": false, - "syncProviderBaseUrl": "http://sync.demo.taler.net/", - "paymentProposalIds": [], - "paymentStatus": { - "type": ProviderPaymentType.InsufficientBalance, - }, - "terms": { - "annualFee": "ARS:0.1", - "storageLimitInMegabytes": 16, - "supportedProtocolVersion": "0.0" - } - } -}); - -export const InactivePending = createExample(TestedComponent, { - currency: 'ARS', - info: { - "active": false, - "syncProviderBaseUrl": "http://sync.demo.taler.net/", - "paymentProposalIds": [], - "paymentStatus": { - "type": ProviderPaymentType.Pending, - }, - "terms": { - "annualFee": "ARS:0.1", - "storageLimitInMegabytes": 16, - "supportedProtocolVersion": "0.0" - } - } -}); - - diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddConfirmProvider.stories.tsx @@ -0,0 +1,60 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + 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. + + 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 + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { FunctionalComponent } from 'preact'; +import { ConfirmProviderView as TestedComponent } from './ProviderAddPage'; + +export default { + title: 'popup/backup/confirm', + component: TestedComponent, + argTypes: { + onRetry: { action: 'onRetry' }, + onDelete: { action: 'onDelete' }, + onBack: { action: 'onBack' }, + } +}; + + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +export const DemoService = createExample(TestedComponent, { + currency: 'KUDOS', + url: 'https://sync.demo.taler.net/', + provider: { + annual_fee: 'KUDOS:0.1', + storage_limit_in_megabytes: 20, + supported_protocol_version: '1' + } +}); + +export const FreeService = createExample(TestedComponent, { + currency: 'ARS', + url: 'https://sync.taler:9667/', + provider: { + annual_fee: 'ARS:0', + storage_limit_in_megabytes: 20, + supported_protocol_version: '1' + } +}); diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddPage.tsx @@ -0,0 +1,107 @@ +import { Amounts, BackupBackupProviderTerms, i18n } from "@gnu-taler/taler-util"; +import { privateDecrypt } from "crypto"; +import { add, addYears } from "date-fns"; +import { VNode } from "preact"; +import { useState } from "preact/hooks"; +import * as wxApi from "../wxApi"; +import ProviderAddConfirmProviderStories from "./ProviderAddConfirmProvider.stories"; + +interface Props { + currency: string; +} + +export function ProviderAddPage({ currency }: Props): VNode { + const [verifying, setVerifying] = useState<{ url: string, provider: BackupBackupProviderTerms } | undefined>(undefined) + if (!verifying) { + return <SetUrlView + currency={currency} + onCancel={() => { + setVerifying(undefined); + }} + onVerify={(url) => { + return fetch(url).then(r => r.json()) + .then((provider) => setVerifying({ url, provider })) + .catch((e) => e.message) + }} + /> + } + return <ConfirmProviderView + provider={verifying.provider} + currency={currency} + url={verifying.url} + onCancel={() => { + setVerifying(undefined); + }} + onConfirm={() => { + wxApi.addBackupProvider(verifying.url).then(_ => history.go(-1)) + }} + + /> +} + +export interface SetUrlViewProps { + currency: string, + onCancel: () => void; + onVerify: (s: string) => Promise<string | undefined>; +} + +export function SetUrlView({ currency, onCancel, onVerify }: SetUrlViewProps) { + const [value, setValue] = useState<string>("") + const [error, setError] = useState<string | undefined>(undefined) + return <div style={{ display: 'flex', flexDirection: 'column' }}> + <section style={{ height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> + <div> + Add backup provider for storing <b>{currency}</b> + </div> + {error && <div class="errorbox" style={{ marginTop: 10 }} > + <p>{error}</p> + </div>} + <h3>Backup provider URL</h3> + <input style={{ width: 'calc(100% - 8px)' }} value={value} onChange={(e) => setValue(e.currentTarget.value)} /> + <p> + Backup providers may charge for their service + </p> + </section> + <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}> + <button class="pure-button" onClick={onCancel}><i18n.Translate>cancel</i18n.Translate></button> + <div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}> + <button class="pure-button button-secondary" style={{ marginLeft: 5 }} onClick={() => onVerify(value).then(r => r ? setError(r) : undefined)}><i18n.Translate>verify service terms</i18n.Translate></button> + </div> + </footer> + </div> +} + +export interface ConfirmProviderViewProps { + provider: BackupBackupProviderTerms, + currency: string, + url: string, + onCancel: () => void; + onConfirm: () => void +} +export function ConfirmProviderView({ url, provider, currency, onCancel, onConfirm }: ConfirmProviderViewProps) { + return <div style={{ display: 'flex', flexDirection: 'column' }}> + + <section style={{ height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> + <div> + Verify provider service terms for storing <b>{currency}</b> + </div> + <h3>{url}</h3> + <p> + {Amounts.isZero(provider.annual_fee) ? 'free of charge' : provider.annual_fee} for a year of backup service + </p> + <p> + {provider.storage_limit_in_megabytes} megabytes of storage + </p> + </section> + <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}> + <button class="pure-button" onClick={onCancel}> + <i18n.Translate>cancel</i18n.Translate> + </button> + <div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}> + <button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onConfirm}> + <i18n.Translate>confirm</i18n.Translate> + </button> + </div> + </footer> + </div> +} diff --git a/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderAddSetUrl.stories.tsx @@ -0,0 +1,46 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + 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. + + 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 + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core'; +import { FunctionalComponent } from 'preact'; +import { SetUrlView as TestedComponent } from './ProviderAddPage'; + +export default { + title: 'popup/backup/add', + component: TestedComponent, + argTypes: { + onRetry: { action: 'onRetry' }, + onDelete: { action: 'onDelete' }, + onBack: { action: 'onBack' }, + } +}; + + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +export const SetUrl = createExample(TestedComponent, { + currency: 'ARS', +}); + diff --git a/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetail.stories.tsx @@ -0,0 +1,218 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + 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. + + 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 + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core'; +import { FunctionalComponent } from 'preact'; +import { ProviderView as TestedComponent } from './ProviderDetailPage'; + +export default { + title: 'popup/backup/details', + component: TestedComponent, + argTypes: { + onRetry: { action: 'onRetry' }, + onDelete: { action: 'onDelete' }, + onBack: { action: 'onBack' }, + } +}; + + +function createExample<Props>(Component: FunctionalComponent<Props>, props: Partial<Props>) { + const r = (args: any) => <Component {...args} /> + r.args = props + return r +} + +export const NotDefined = createExample(TestedComponent, { + currency: 'ARS', +}); + +export const Active = createExample(TestedComponent, { + currency: 'ARS', + info: { + "active": true, + "syncProviderBaseUrl": "http://sync.taler:9967/", + "lastSuccessfulBackupTimestamp": { + "t_ms": 1625063925078 + }, + "paymentProposalIds": [ + "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" + ], + "paymentStatus": { + "type": ProviderPaymentType.Paid, + "paidUntil": { + "t_ms": 1656599921000 + } + }, + "terms": { + "annualFee": "ARS:1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const ActiveErrorSync = createExample(TestedComponent, { + currency: 'ARS', + info: { + "active": true, + "syncProviderBaseUrl": "http://sync.taler:9967/", + "lastSuccessfulBackupTimestamp": { + "t_ms": 1625063925078 + }, + lastAttemptedBackupTimestamp: { + "t_ms": 1625063925078 + }, + "paymentProposalIds": [ + "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" + ], + "paymentStatus": { + "type": ProviderPaymentType.Paid, + "paidUntil": { + "t_ms": 1656599921000 + } + }, + lastError: { + code: 2002, + details: 'details', + hint: 'error hint from the server', + message: 'message' + }, + "terms": { + "annualFee": "ARS:1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const ActiveBackupProblemUnreadable = createExample(TestedComponent, { + currency: 'ARS', + info: { + "active": true, + "syncProviderBaseUrl": "http://sync.taler:9967/", + "lastSuccessfulBackupTimestamp": { + "t_ms": 1625063925078 + }, + "paymentProposalIds": [ + "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" + ], + "paymentStatus": { + "type": ProviderPaymentType.Paid, + "paidUntil": { + "t_ms": 1656599921000 + } + }, + backupProblem: { + type: 'backup-unreadable' + }, + "terms": { + "annualFee": "ARS:1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const ActiveBackupProblemDevice = createExample(TestedComponent, { + currency: 'ARS', + info: { + "active": true, + "syncProviderBaseUrl": "http://sync.taler:9967/", + "lastSuccessfulBackupTimestamp": { + "t_ms": 1625063925078 + }, + "paymentProposalIds": [ + "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" + ], + "paymentStatus": { + "type": ProviderPaymentType.Paid, + "paidUntil": { + "t_ms": 1656599921000 + } + }, + backupProblem: { + type: 'backup-conflicting-device', + myDeviceId: 'my-device-id', + otherDeviceId: 'other-device-id', + backupTimestamp: { + "t_ms": 1656599921000 + } + }, + "terms": { + "annualFee": "ARS:1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const InactiveUnpaid = createExample(TestedComponent, { + currency: 'ARS', + info: { + "active": false, + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.Unpaid, + }, + "terms": { + "annualFee": "ARS:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const InactiveInsufficientBalance = createExample(TestedComponent, { + currency: 'ARS', + info: { + "active": false, + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.InsufficientBalance, + }, + "terms": { + "annualFee": "ARS:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const InactivePending = createExample(TestedComponent, { + currency: 'ARS', + info: { + "active": false, + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.Pending, + }, + "terms": { + "annualFee": "ARS:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + + diff --git a/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderDetailPage.tsx @@ -0,0 +1,163 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + 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 + 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/> +*/ + + +import { BackupBackupProviderTerms, i18n, Timestamp } from "@gnu-taler/taler-util"; +import { ProviderInfo, ProviderPaymentType } from "@gnu-taler/taler-wallet-core"; +import { formatDuration, intervalToDuration, format } from "date-fns"; +import { Fragment, VNode } from "preact"; +import { useRef, useState } from "preact/hooks"; +import { useBackupStatus } from "../hooks/useProvidersByCurrency"; +import * as wxApi from "../wxApi"; + +interface Props { + currency: string; + onAddProvider: (c: string) => void; + onBack: () => void; +} + +export function ProviderDetailPage({ currency, onAddProvider, onBack }: Props): VNode { + const status = useBackupStatus() + if (!status) { + return <div>Loading...</div> + } + const info = status.providers[currency]; + return <ProviderView currency={currency} info={info} + onSync={() => { null }} + onDelete={() => { null }} + onBack={onBack} + onAddProvider={() => onAddProvider(currency)} + />; +} + +export interface ViewProps { + currency: string; + info?: ProviderInfo; + onDelete: () => void; + onSync: () => void; + onBack: () => void; + onAddProvider: () => void; +} + +export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddProvider }: ViewProps): VNode { + function Footer() { + return <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}> + <button class="pure-button" onClick={onBack}><i18n.Translate>back</i18n.Translate></button> + <div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}> + {info && <button class="pure-button button-destructive" onClick={onDelete}><i18n.Translate>remove</i18n.Translate></button>} + {info && <button class="pure-button button-secondary" style={{ marginLeft: 5 }} onClick={onSync}><i18n.Translate>sync now</i18n.Translate></button>} + {!info && <button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onAddProvider}><i18n.Translate>add provider</i18n.Translate></button>} + </div> + </footer> + } + function Error() { + if (info?.lastError) { + return <Fragment> + <div class="errorbox" style={{ marginTop: 10 }} > + <div style={{ height: 0, textAlign: 'right', color: 'gray', fontSize: 'small' }}>{!info.lastAttemptedBackupTimestamp || info.lastAttemptedBackupTimestamp.t_ms === 'never' ? 'never' : format(new Date(info.lastAttemptedBackupTimestamp.t_ms), 'dd/MM/yyyy HH:mm:ss')}</div> + <p>{info.lastError.hint}</p> + </div> + </Fragment> + } + if (info?.backupProblem) { + switch (info.backupProblem.type) { + case "backup-conflicting-device": + return <div class="errorbox" style={{ marginTop: 10 }}> + <p>There is another backup from <b>{info.backupProblem.otherDeviceId}</b></p> + </div> + case "backup-unreadable": + return <div class="errorbox" style={{ marginTop: 10 }}> + <p>Backup is not readable</p> + </div> + default: + return <div class="errorbox" style={{ marginTop: 10 }}> + <p>Unkown backup problem: {JSON.stringify(info.backupProblem)}</p> + </div> + } + } + return null + } + function colorByStatus(status: ProviderPaymentType | undefined) { + 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)' + default: + break; + } + return undefined + } + + return ( + <div style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}> + <style>{` + table td { + padding: 5px 10px; + } + `}</style> + <div style={{ display: 'flex', flexDirection: 'column' }}> + <section style={{ flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> + <span style={{ padding: 5, display: 'inline-block', backgroundColor: colorByStatus(info?.paymentStatus.type), borderRadius: 5, color: 'white' }}>{info?.paymentStatus.type}</span> + {info && <span style={{ float: "right", fontSize: "small", color: "gray", padding: 5 }}> + From <b>{info.syncProviderBaseUrl}</b> + </span>} + + <Error /> + + <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", }}> + <h1>{currency}</h1> + {info && <div style={{ marginTop: 'auto', marginBottom: 'auto' }}>{info.terms?.annualFee} / year</div>} + </div> + + <div>{daysSince(info?.lastSuccessfulBackupTimestamp)} </div> + </section> + <Footer /> + </div> + </div> + ) +} + +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 ? 'years' : ( + duration?.months ? 'months' : ( + duration?.days ? 'days' : ( + duration?.hours ? 'hours' : ( + duration?.minutes ? 'minutes' : 'seconds' + ) + ) + ) + ) + ] + }) + return `synced ${str} ago` +} diff --git a/packages/taler-wallet-webextension/src/popup/ProviderPage.tsx b/packages/taler-wallet-webextension/src/popup/ProviderPage.tsx @@ -1,174 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - 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 - 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/> -*/ - - -import { i18n, Timestamp } from "@gnu-taler/taler-util"; -import { ProviderInfo, ProviderPaymentType } from "@gnu-taler/taler-wallet-core"; -import { formatDuration, intervalToDuration } from "date-fns"; -import { VNode } from "preact"; -import { useRef, useState } from "preact/hooks"; -import { useBackupStatus } from "../hooks/useProvidersByCurrency"; -import * as wxApi from "../wxApi"; - -interface Props { - currency: string; -} - -export function ProviderPage({ currency }: Props): VNode { - const status = useBackupStatus() - const [adding, setAdding] = useState<boolean>(false) - if (!status) { - return <div>Loading...</div> - } - if (adding) { - return <AddProviderView onConfirm={(value) => { - console.log(value) - wxApi.addBackupProvider(value).then(_ => history.go(-1)) - setAdding(false) - }} /> - } - const info = status.providers[currency]; - return <ProviderView currency={currency} info={info} - onSync={() => { null }} - onDelete={() => { null }} - onBack={() => { history.go(-1); }} - onAddProvider={() => { setAdding(true) }} - />; -} - -function AddProviderView({ onConfirm }: { onConfirm: (s: string) => void }) { - const textInput = useRef<HTMLInputElement>(null) - return <div> - <input ref={textInput} /> - <button onClick={() => onConfirm(textInput?.current.value)}>confirm</button> - </div> -} - -export interface ViewProps { - currency: string; - info?: ProviderInfo; - onDelete: () => void; - onSync: () => void; - onBack: () => void; - onAddProvider: () => void; -} - -export function ProviderView({ currency, info, onDelete, onSync, onBack, onAddProvider }: ViewProps): VNode { - function Footer() { - return <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}> - <button onClick={onBack}><i18n.Translate>back</i18n.Translate></button> - <div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}> - {info && <button class="pure-button button-destructive" onClick={onDelete}><i18n.Translate>remove</i18n.Translate></button>} - {info && <button class="pure-button button-secondary" style={{ marginLeft: 5 }} onClick={onSync}><i18n.Translate>sync now</i18n.Translate></button>} - {!info && <button class="pure-button button-success" style={{ marginLeft: 5 }} onClick={onAddProvider}><i18n.Translate>add provider</i18n.Translate></button>} - </div> - </footer> - } - function Error() { - if (info?.lastError) { - return <div class="errorbox" style={{ marginTop: 10 }} > - <p>{info.lastError.hint}</p> - </div> - } - if (info?.backupProblem) { - switch (info.backupProblem.type) { - case "backup-conflicting-device": - return <div class="errorbox" style={{ marginTop: 10 }}> - <p>There is another backup from <b>{info.backupProblem.otherDeviceId}</b></p> - </div> - case "backup-unreadable": - return <div class="errorbox" style={{ marginTop: 10 }}> - <p>Backup is not readable</p> - </div> - default: - return <div class="errorbox" style={{ marginTop: 10 }}> - <p>Unkown backup problem: {JSON.stringify(info.backupProblem)}</p> - </div> - } - } - return null - } - function colorByStatus(status: ProviderPaymentType | undefined) { - 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)' - default: - break; - } - return undefined - } - - return ( - <div style={{ height: 'calc(320px - 34px - 16px)', overflow: 'auto' }}> - <style>{` - table td { - padding: 5px 10px; - } - `}</style> - <div style={{ display: 'flex', flexDirection: 'column' }}> - <section style={{ flex: '1 0 auto', height: 'calc(320px - 34px - 45px - 16px)', overflow: 'auto' }}> - <span style={{ padding: 5, display: 'inline-block', backgroundColor: colorByStatus(info?.paymentStatus.type), borderRadius: 5, color: 'white' }}>{info?.paymentStatus.type}</span> - {info && <span style={{ float: "right", fontSize: "small", color: "gray", padding: 5 }}> - From <b>{info.syncProviderBaseUrl}</b> - </span>} - - <Error /> - - <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", }}> - <h1>{currency}</h1> - {info && <div style={{ marginTop: 'auto', marginBottom: 'auto' }}>{info.terms?.annualFee} / year</div>} - </div> - - <div>{daysSince(info?.lastSuccessfulBackupTimestamp)} </div> - </section> - <Footer /> - </div> - </div> - ) -} - -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 ? 'years' : ( - duration?.months ? 'months' : ( - duration?.days ? 'days' : ( - duration?.hours ? 'hours' : ( - duration?.minutes ? 'minutes' : 'seconds' - ) - ) - ) - ) - ] - }) - return `synced ${str} ago` -} diff --git a/packages/taler-wallet-webextension/src/popup/Transaction.tsx b/packages/taler-wallet-webextension/src/popup/Transaction.tsx @@ -62,7 +62,7 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall function Footer() { return <footer style={{ marginTop: 'auto', display: 'flex', flexShrink: 0 }}> - <button onClick={onBack}><i18n.Translate>back</i18n.Translate></button> + <button class="pure-button" onClick={onBack}><i18n.Translate>back</i18n.Translate></button> <div style={{ width: '100%', flexDirection: 'row', justifyContent: 'flex-end', display: 'flex' }}> {transaction?.error ? <button class="pure-button button-secondary" style={{marginRight: 5}} onClick={onRetry}><i18n.Translate>retry</i18n.Translate></button> : null } <button class="pure-button button-destructive" onClick={onDelete}><i18n.Translate>delete</i18n.Translate></button> @@ -96,7 +96,7 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall ).amount return ( <div style={{ display: 'flex', flexDirection: 'column' }} > - <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 45px - 16px)', overflow: 'auto' }}> + <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> <span style="font-size:small; color:gray">{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</span> <span style="float: right; font-size:small; color:gray"> From <b>{transaction.exchangeBaseUrl}</b> @@ -122,7 +122,7 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall return ( <div style={{ display: 'flex', flexDirection: 'column' }} > - <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 45px - 16px)', overflow: 'auto' }}> + <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> <span style="flat: left; font-size:small; color:gray">{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</span> <span style="float: right; font-size:small; color:gray"> To <b>{transaction.info.merchant.name}</b> @@ -161,7 +161,7 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall ).amount return ( <div style={{ display: 'flex', flexDirection: 'column' }} > - <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 45px - 16px)', overflow: 'auto' }}> + <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> <span style="flat: left; font-size:small; color:gray">{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</span> <span style="float: right; font-size:small; color:gray"> To <b>{transaction.targetPaytoUri}</b> @@ -182,7 +182,7 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall ).amount return ( <div style={{ display: 'flex', flexDirection: 'column' }} > - <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 45px - 16px)', overflow: 'auto' }}> + <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> <span style="flat: left; font-size:small; color:gray">{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</span> <span style="float: right; font-size:small; color:gray"> From <b>{transaction.exchangeBaseUrl}</b> @@ -203,7 +203,7 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall ).amount return ( <div style={{ display: 'flex', flexDirection: 'column' }} > - <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 45px - 16px)', overflow: 'auto' }}> + <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> <span style="flat: left; font-size:small; color:gray">{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</span> <span style="float: right; font-size:small; color:gray"> From <b>{transaction.merchantBaseUrl}</b> @@ -224,7 +224,7 @@ export function TransactionView({ transaction, onDelete, onRetry, onBack }: Wall ).amount return ( <div style={{ display: 'flex', flexDirection: 'column' }} > - <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 45px - 16px)', overflow: 'auto' }}> + <section style={{ color: transaction.pending ? 'gray' : '', flex: '1 0 auto', height: 'calc(320px - 34px - 34px - 16px)', overflow: 'auto' }}> <span style="flat: left; font-size:small; color:gray">{transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')}</span> <span style="float: right; font-size:small; color:gray"> From <b>{transaction.info.merchant.name}</b> diff --git a/packages/taler-wallet-webextension/src/popup/popup.tsx b/packages/taler-wallet-webextension/src/popup/popup.tsx @@ -36,7 +36,8 @@ export enum Pages { backup = '/backup', history = '/history', transaction = '/transaction/:tid', - provider = '/provider/:currency', + provider_detail = '/provider/:currency', + provider_add = '/provider/:currency/add', } interface TabProps { @@ -47,7 +48,7 @@ interface TabProps { function Tab(props: TabProps): JSX.Element { let cssClass = ""; - if (props.current === props.target) { + if (props.current?.startsWith(props.target)) { cssClass = "active"; } return ( diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -20,25 +20,25 @@ * @author Florian Dold <dold@taler.net> */ -import { Fragment, render } from "preact"; import { setupI18n } from "@gnu-taler/taler-util"; -import { strings } from "./i18n/strings"; +import { createHashHistory } from "history"; +import { render } from "preact"; +import Router, { route, Route } from "preact-router"; import { useEffect } from "preact/hooks"; +import { DevContextProvider } from "./context/useDevContext"; +import { useTalerActionURL } from "./hooks/useTalerActionURL"; +import { strings } from "./i18n/strings"; +import { BackupPage } from "./popup/BackupPage"; +import { BalancePage } from "./popup/Balance"; +import { DeveloperPage as DeveloperPage } from "./popup/Debug"; +import { HistoryPage } from "./popup/History"; import { Pages, WalletNavBar } from "./popup/popup"; -import { HistoryPage } from "./popup/History"; -import { DeveloperPage as DeveloperPage } from "./popup/Debug"; +import { ProviderAddPage } from "./popup/ProviderAddPage"; +import { ProviderDetailPage } from "./popup/ProviderDetailPage"; import { SettingsPage } from "./popup/Settings"; import { TransactionPage } from "./popup/Transaction"; -import { BalancePage } from "./popup/Balance"; -import Match from "preact-router/match"; -import Router, { getCurrentUrl, route, Route } from "preact-router"; -import { useTalerActionURL } from "./hooks/useTalerActionURL"; -import { createHashHistory } from "history"; -import { DevContextProvider } from "./context/useDevContext"; -import { BackupPage } from "./popup/BackupPage"; -import { ProviderPage } from "./popup/ProviderPage.js"; function main(): void { try { @@ -100,7 +100,8 @@ function Application() { <Route path={Pages.dev} component={DeveloperPage} /> <Route path={Pages.history} component={HistoryPage} /> <Route path={Pages.backup} component={BackupPage} /> - <Route path={Pages.provider} component={ProviderPage} /> + <Route path={Pages.provider_detail} component={ProviderDetailPage} /> + <Route path={Pages.provider_add} component={ProviderAddPage} /> <Route path={Pages.transaction} component={TransactionPage} /> <Route default component={Redirect} to={Pages.balance} /> </Router>