From 0bc235c64b6936aa092a2df40e0c4909e4ac05d5 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 24 Aug 2021 13:29:37 -0300 Subject: copy from popup to wallet --- .../src/popup/Balance.stories.tsx | 2 +- .../src/popup/Transaction.stories.tsx | 276 --------------------- .../src/popup/Transaction.tsx | 217 ---------------- .../src/popupEntryPoint.tsx | 2 - .../src/wallet/Backup.stories.tsx | 193 ++++++++++++++ .../src/wallet/BackupPage.tsx | 146 +++++++++++ .../src/wallet/Balance.stories.tsx | 105 ++++++++ .../src/wallet/BalancePage.tsx | 117 +++++++++ .../wallet/ProviderAddConfirmProvider.stories.tsx | 52 ++++ .../src/wallet/ProviderAddPage.tsx | 150 +++++++++++ .../src/wallet/ProviderAddSetUrl.stories.tsx | 53 ++++ .../src/wallet/ProviderDetail.stories.tsx | 238 ++++++++++++++++++ .../src/wallet/ProviderDetailPage.tsx | 197 +++++++++++++++ .../src/wallet/Settings.stories.tsx | 43 ++++ .../src/wallet/Settings.tsx | 103 ++++++++ .../src/wallet/Transaction.stories.tsx | 276 +++++++++++++++++++++ .../src/wallet/Transaction.tsx | 217 ++++++++++++++++ .../src/walletEntryPoint.tsx | 29 ++- 18 files changed, 1909 insertions(+), 507 deletions(-) delete mode 100644 packages/taler-wallet-webextension/src/popup/Transaction.stories.tsx delete mode 100644 packages/taler-wallet-webextension/src/popup/Transaction.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/BackupPage.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/BalancePage.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/Settings.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx create mode 100644 packages/taler-wallet-webextension/src/wallet/Transaction.tsx (limited to 'packages/taler-wallet-webextension') diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx index 4a2e1045b..a0655d379 100644 --- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx @@ -23,7 +23,7 @@ import { createExample, NullLink } from '../test-utils'; import { BalanceView as TestedComponent } from './BalancePage'; export default { - title: 'popup/balance/detail', + title: 'popup/balance', component: TestedComponent, argTypes: { } diff --git a/packages/taler-wallet-webextension/src/popup/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/popup/Transaction.stories.tsx deleted file mode 100644 index 65302babb..000000000 --- a/packages/taler-wallet-webextension/src/popup/Transaction.stories.tsx +++ /dev/null @@ -1,276 +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 - */ - -/** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { - PaymentStatus, - TransactionCommon, TransactionDeposit, TransactionPayment, - TransactionRefresh, TransactionRefund, TransactionTip, TransactionType, - TransactionWithdrawal, - WithdrawalType -} from '@gnu-taler/taler-util'; -import { createExample } from '../test-utils'; -import { TransactionView as TestedComponent } from './Transaction'; - -export default { - title: 'popup/history/details', - component: TestedComponent, - argTypes: { - onRetry: { action: 'onRetry' }, - onDelete: { action: 'onDelete' }, - onBack: { action: 'onBack' }, - } -}; - -const commonTransaction = { - amountRaw: 'USD:10', - amountEffective: 'USD:9', - pending: false, - timestamp: { - t_ms: new Date().getTime() - }, - transactionId: '12', -} as TransactionCommon - -const exampleData = { - withdraw: { - ...commonTransaction, - type: TransactionType.Withdrawal, - exchangeBaseUrl: 'http://exchange.taler', - withdrawalDetails: { - confirmed: false, - exchangePaytoUris: ['payto://x-taler-bank/bank/account'], - type: WithdrawalType.ManualTransfer, - } - } as TransactionWithdrawal, - payment: { - ...commonTransaction, - amountEffective: 'USD:11', - type: TransactionType.Payment, - info: { - contractTermsHash: 'ASDZXCASD', - merchant: { - name: 'the merchant', - }, - orderId: '2021.167-03NPY6MCYMVGT', - products: [], - summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth", - fulfillmentMessage: '', - }, - proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', - status: PaymentStatus.Accepted, - } as TransactionPayment, - deposit: { - ...commonTransaction, - type: TransactionType.Deposit, - depositGroupId: '#groupId', - targetPaytoUri: 'payto://x-taler-bank/bank/account', - } as TransactionDeposit, - refresh: { - ...commonTransaction, - type: TransactionType.Refresh, - exchangeBaseUrl: 'http://exchange.taler', - } as TransactionRefresh, - tip: { - ...commonTransaction, - type: TransactionType.Tip, - merchantBaseUrl: 'http://merchant.taler', - } as TransactionTip, - refund: { - ...commonTransaction, - type: TransactionType.Refund, - refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', - info: { - contractTermsHash: 'ASDZXCASD', - merchant: { - name: 'the merchant', - }, - orderId: '2021.167-03NPY6MCYMVGT', - products: [], - summary: 'the summary', - fulfillmentMessage: '', - }, - } as TransactionRefund, -} - -const transactionError = { - code: 2000, - details: "details", - hint: "this is a hint for the error", - message: 'message' -} - -export const Withdraw = createExample(TestedComponent, { - transaction: exampleData.withdraw -}); - -export const WithdrawError = createExample(TestedComponent, { - transaction: { - ...exampleData.withdraw, - error: transactionError, - }, -}); - -export const WithdrawPending = createExample(TestedComponent, { - transaction: { ...exampleData.withdraw, pending: true }, -}); - - -export const Payment = createExample(TestedComponent, { - transaction: exampleData.payment -}); - -export const PaymentError = createExample(TestedComponent, { - transaction: { - ...exampleData.payment, - error: transactionError - }, -}); - -export const PaymentWithoutFee = createExample(TestedComponent, { - transaction: { - ...exampleData.payment, - amountRaw: 'USD:11', - - } -}); - -export const PaymentPending = createExample(TestedComponent, { - transaction: { ...exampleData.payment, pending: true }, -}); - -export const PaymentWithProducts = createExample(TestedComponent, { - transaction: { - ...exampleData.payment, - info: { - ...exampleData.payment.info, - summary: 'this order has 5 products', - products: [{ - description: 't-shirt', - unit: 'shirts', - quantity: 1, - }, { - description: 't-shirt', - unit: 'shirts', - quantity: 1, - }, { - description: 'e-book', - }, { - description: 'beer', - unit: 'pint', - quantity: 15, - }, { - description: 'beer', - unit: 'pint', - quantity: 15, - }] - } - } as TransactionPayment, -}); - -export const PaymentWithLongSummary = createExample(TestedComponent, { - transaction: { - ...exampleData.payment, - info: { - ...exampleData.payment.info, - summary: 'this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, ', - products: [{ - description: 'an xl sized t-shirt with some drawings on it, color pink', - unit: 'shirts', - quantity: 1, - }, { - description: 'beer', - unit: 'pint', - quantity: 15, - }] - } - } as TransactionPayment, -}); - - -export const Deposit = createExample(TestedComponent, { - transaction: exampleData.deposit -}); - -export const DepositError = createExample(TestedComponent, { - transaction: { - ...exampleData.deposit, - error: transactionError - }, -}); - -export const DepositPending = createExample(TestedComponent, { - transaction: { ...exampleData.deposit, pending: true } -}); - -export const Refresh = createExample(TestedComponent, { - transaction: exampleData.refresh -}); - -export const RefreshError = createExample(TestedComponent, { - transaction: { - ...exampleData.refresh, - error: transactionError - }, -}); - -export const Tip = createExample(TestedComponent, { - transaction: exampleData.tip -}); - -export const TipError = createExample(TestedComponent, { - transaction: { - ...exampleData.tip, - error: transactionError - }, -}); - -export const TipPending = createExample(TestedComponent, { - transaction: { ...exampleData.tip, pending: true } -}); - -export const Refund = createExample(TestedComponent, { - transaction: exampleData.refund -}); - -export const RefundError = createExample(TestedComponent, { - transaction: { - ...exampleData.refund, - error: transactionError - }, -}); - -export const RefundPending = createExample(TestedComponent, { - transaction: { ...exampleData.refund, pending: true } -}); - -export const RefundWithProducts = createExample(TestedComponent, { - transaction: { - ...exampleData.refund, - info: { - ...exampleData.refund.info, - products: [{ - description: 't-shirt', - }, { - description: 'beer', - }] - } - } as TransactionRefund, -}); diff --git a/packages/taler-wallet-webextension/src/popup/Transaction.tsx b/packages/taler-wallet-webextension/src/popup/Transaction.tsx deleted file mode 100644 index 117a098bc..000000000 --- a/packages/taler-wallet-webextension/src/popup/Transaction.tsx +++ /dev/null @@ -1,217 +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 - */ - -import { AmountJson, Amounts, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util"; -import { format } from "date-fns"; -import { Fragment, JSX, VNode, h } from "preact"; -import { route } from 'preact-router'; -import { useEffect, useState } from "preact/hooks"; -import * as wxApi from "../wxApi"; -import { Pages } from "../NavigationBar"; -import emptyImg from "../../static/img/empty.png" -import { Button, ButtonDestructive, ButtonPrimary, ListOfProducts, PopupBox, Row, RowBorderGray, SmallTextLight } from "../components/styled"; -import { ErrorMessage } from "../components/ErrorMessage"; - -export function TransactionPage({ tid }: { tid: string; }): JSX.Element { - const [transaction, setTransaction] = useState< - Transaction | undefined - >(undefined); - - useEffect(() => { - const fetchData = async (): Promise => { - const res = await wxApi.getTransactions(); - const ts = res.transactions.filter(t => t.transactionId === tid); - if (ts.length === 1) { - setTransaction(ts[0]); - } else { - route(Pages.history); - } - }; - fetchData(); - }, []); - - if (!transaction) { - return
Loading ...
; - } - return wxApi.deleteTransaction(tid).then(_ => history.go(-1))} - onRetry={() => wxApi.retryTransaction(tid).then(_ => history.go(-1))} - onBack={() => { history.go(-1); }} />; -} - -export interface WalletTransactionProps { - transaction: Transaction, - onDelete: () => void, - onRetry: () => void, - onBack: () => void, -} - - -export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) { - - function Status() { - if (transaction.error) { - return (failed) - } - if (transaction.pending) { - return (pending...) - } - return null - } - - function Fee({ value }: { value: AmountJson }) { - if (Amounts.isZero(value)) return null - return (fee {Amounts.stringify(value)}) - } - - function TransactionTemplate({ upperRight, children }: { upperRight: VNode, children: VNode[] }) { - return -
- - {transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')} - - - {upperRight} - -
-
- - {children} -
-
- -
- {transaction?.error ? retry : null} - delete -
-
-
- } - - if (transaction.type === TransactionType.Withdrawal) { - const fee = Amounts.sub( - Amounts.parseOrThrow(transaction.amountRaw), - Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return From {transaction.exchangeBaseUrl}}> -

Withdraw

-

{transaction.amountEffective}

-
- } - - const showLargePic = () => { - - } - - if (transaction.type === TransactionType.Payment) { - const fee = Amounts.sub( - Amounts.parseOrThrow(transaction.amountEffective), - Amounts.parseOrThrow(transaction.amountRaw), - ).amount - - return To {transaction.info.merchant.name}}> -

Payment

-

{transaction.amountEffective}

- #{transaction.info.orderId} -

- {transaction.info.summary} -

-
- {transaction.info.products && transaction.info.products.length > 0 && - - {transaction.info.products.map(p => - - - -
- {p.quantity && p.quantity > 0 && x {p.quantity} {p.unit}} -
{p.description}
-
-
)} -
- } -
-
- } - - if (transaction.type === TransactionType.Deposit) { - const fee = Amounts.sub( - Amounts.parseOrThrow(transaction.amountRaw), - Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return To {transaction.targetPaytoUri}}> -

Deposit

-

{transaction.amountEffective}

-
- } - - if (transaction.type === TransactionType.Refresh) { - const fee = Amounts.sub( - Amounts.parseOrThrow(transaction.amountRaw), - Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return From {transaction.exchangeBaseUrl}}> -

Refresh

-

{transaction.amountEffective}

-
- } - - if (transaction.type === TransactionType.Tip) { - const fee = Amounts.sub( - Amounts.parseOrThrow(transaction.amountRaw), - Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return From {transaction.merchantBaseUrl}}> -

Tip

-

{transaction.amountEffective}

-
- } - - if (transaction.type === TransactionType.Refund) { - const fee = Amounts.sub( - Amounts.parseOrThrow(transaction.amountRaw), - Amounts.parseOrThrow(transaction.amountEffective), - ).amount - return From {transaction.info.merchant.name}}> -

Refund

-

{transaction.amountEffective}

- - #{transaction.info.orderId} -

- {transaction.info.summary} -

-
- {transaction.info.products && transaction.info.products.length > 0 && - - {transaction.info.products.map(p => - - - -
- {p.quantity && p.quantity > 0 && x {p.quantity} {p.unit}} -
{p.description}
-
-
)} -
- } -
-
- } - - - return
-} diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx index 77c19c150..c72ea85c4 100644 --- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx @@ -38,7 +38,6 @@ import { import { ProviderAddPage } from "./popup/ProviderAddPage"; import { ProviderDetailPage } from "./popup/ProviderDetailPage"; import { SettingsPage } from "./popup/Settings"; -import { TransactionPage } from "./popup/Transaction"; function main(): void { try { @@ -114,7 +113,6 @@ function Application() { route(Pages.backup) }} /> - diff --git a/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx new file mode 100644 index 000000000..9a53fefe2 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Backup.stories.tsx @@ -0,0 +1,193 @@ +/* + 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core'; +import { addDays } from 'date-fns'; +import { BackupView as TestedComponent } from './BackupPage'; +import { createExample } from '../test-utils'; + +export default { + title: 'wallet/backup/list', + component: TestedComponent, + argTypes: { + onRetry: { action: 'onRetry' }, + onDelete: { action: 'onDelete' }, + onBack: { action: 'onBack' }, + } +}; + + +export const LotOfProviders = createExample(TestedComponent, { + providers: [{ + "active": true, + name:'sync.demo', + "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" + } + }, { + "active": true, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.taler:9967/", + "lastSuccessfulBackupTimestamp": { + "t_ms": 1625063925078 + }, + "paymentProposalIds": [ + "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" + ], + "paymentStatus": { + "type": ProviderPaymentType.Paid, + "paidUntil": { + "t_ms": addDays(new Date(), 13).getTime() + } + }, + "terms": { + "annualFee": "ARS:1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + }, { + "active": false, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.Pending, + }, + "terms": { + "annualFee": "KUDOS:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + }, { + "active": false, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.InsufficientBalance, + }, + "terms": { + "annualFee": "KUDOS:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + }, { + "active": false, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.TermsChanged, + newTerms: { + annualFee: 'USD:2', + storageLimitInMegabytes: 8, + supportedProtocolVersion: '2', + }, + oldTerms: { + annualFee: 'USD:1', + storageLimitInMegabytes: 16, + supportedProtocolVersion: '1', + + }, + paidUntil: { + t_ms: 'never' + } + }, + "terms": { + "annualFee": "KUDOS:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + }, { + "active": false, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.Unpaid, + }, + "terms": { + "annualFee": "KUDOS:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + }, { + "active": false, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.Unpaid, + }, + "terms": { + "annualFee": "KUDOS:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + }] +}); + + +export const OneProvider = createExample(TestedComponent, { + providers: [{ + "active": true, + name:'sync.demo', + "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 Empty = createExample(TestedComponent, { + providers: [] +}); + diff --git a/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx new file mode 100644 index 000000000..8b88432e0 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx @@ -0,0 +1,146 @@ +/* + 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 +*/ + + +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, CenteredTextBold, PopupBox, RowBorderGray, + SmallText, SmallTextLight, WalletBox +} from "../components/styled"; +import { useBackupStatus } from "../hooks/useBackupStatus"; +import { Pages } from "../NavigationBar"; + +interface Props { + onAddProvider: () => void; +} + +export function BackupPage({ onAddProvider }: Props): VNode { + const status = useBackupStatus() + if (!status) { + return
Loading...
+ } + return ; +} + +export interface ViewProps { + providers: ProviderInfo[], + onAddProvider: () => void; + onSyncAll: () => Promise; +} + +export function BackupView({ providers, onAddProvider, onSyncAll }: ViewProps): VNode { + return ( + +
+ {providers.map((provider) => + )} + {!providers.length && + No backup providers configured + Add provider + } +
+ {!!providers.length &&
+
+
+ { + providers.length > 1 ? + Sync all backups : + Sync now + } + Add provider +
+
} +
+ ) +} + +interface TransactionLayoutProps { + status: ProviderPaymentStatus; + timestamp?: Timestamp; + title: string; + id: string; + active: boolean; +} + +function BackupLayout(props: TransactionLayoutProps): JSX.Element { + const date = !props.timestamp ? undefined : new Date(props.timestamp.t_ms); + const dateStr = date?.toLocaleString([], { + dateStyle: "medium", + timeStyle: "short", + } as any); + + + return ( + +
+ {props.title} + + {dateStr && Last synced: {dateStr}} + {!dateStr && Not synced} +
+
+ {props.status?.type === 'paid' ? + : +
{props.status.type}
+ } +
+
+ ); +} + +function ExpirationText({ until }: { until: Timestamp }) { + return + Expires in + {daysUntil(until)} + +} + +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 daysUntil(d: Timestamp) { + if (d.t_ms === 'never') return undefined + 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' : 'minutes' + ) + ) + ) + ] + }) + return `${str}` +} \ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx new file mode 100644 index 000000000..1b145345f --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx @@ -0,0 +1,105 @@ +/* + 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { createExample, NullLink } from '../test-utils'; +import { BalanceView as TestedComponent } from './BalancePage'; + +export default { + title: 'wallet/balance', + component: TestedComponent, + argTypes: { + } +}; + + +export const NotYetLoaded = createExample(TestedComponent, { +}); + +export const GotError = createExample(TestedComponent, { + balance: { + error: true + }, + Linker: NullLink, +}); + +export const EmptyBalance = createExample(TestedComponent, { + balance: { + error: false, + response: { + balances: [] + }, + }, + Linker: NullLink, +}); + +export const SomeCoins = createExample(TestedComponent, { + balance: { + error: false, + response: { + balances: [{ + available: 'USD:10.5', + hasPendingTransactions: false, + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + requiresUserInput: false + }] + }, + }, + Linker: NullLink, +}); + +export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, { + balance: { + error: false, + response: { + balances: [{ + available: 'USD:2.23', + hasPendingTransactions: false, + pendingIncoming: 'USD:5.11', + pendingOutgoing: 'USD:0', + requiresUserInput: false + }] + }, + }, + Linker: NullLink, +}); + +export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, { + balance: { + error: false, + response: { + balances: [{ + available: 'USD:2', + hasPendingTransactions: false, + pendingIncoming: 'USD:5', + pendingOutgoing: 'USD:0', + requiresUserInput: false + },{ + available: 'EUR:4', + hasPendingTransactions: false, + pendingIncoming: 'EUR:5', + pendingOutgoing: 'EUR:0', + requiresUserInput: false + }] + }, + }, + Linker: NullLink, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx new file mode 100644 index 000000000..4846d47f7 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx @@ -0,0 +1,117 @@ +/* + 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 + */ + +import { + amountFractionalBase, Amounts, + Balance, BalancesResponse, + i18n +} from "@gnu-taler/taler-util"; +import { JSX, h } from "preact"; +import { WalletBox, Centered } from "../components/styled/index"; +import { BalancesHook, useBalances } from "../hooks/useBalances"; +import { PageLink, renderAmount } from "../renderHtml"; + + +export function BalancePage() { + const balance = useBalances() + return +} +export interface BalanceViewProps { + balance: BalancesHook, + Linker: typeof PageLink, +} +export function BalanceView({ balance, Linker }: BalanceViewProps) { + if (!balance) { + return + } + + if (balance.error) { + return ( +
+

{i18n.str`Error: could not retrieve balance information.`}

+

+ Click here for help and + diagnostics. +

+
+ ) + } + if (balance.response.balances.length === 0) { + return ( +

+ You have no balance to show. Need some{" "} + help getting started? +

+ ) + } + return +} + +function formatPending(entry: Balance): JSX.Element { + let incoming: JSX.Element | undefined; + let payment: JSX.Element | undefined; + + const available = Amounts.parseOrThrow(entry.available); + const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming); + const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing); + + if (!Amounts.isZero(pendingIncoming)) { + incoming = ( + + + {"+"} + {renderAmount(entry.pendingIncoming)} + {" "} + incoming + + ); + } + + const l = [incoming, payment].filter((x) => x !== undefined); + if (l.length === 0) { + return ; + } + + if (l.length === 1) { + return ({l}); + } + return ( + + ({l[0]}, {l[1]}) + + ); +} + + +function ShowBalances({ wallet }: { wallet: BalancesResponse }) { + return +
+ {wallet.balances.map((entry) => { + const av = Amounts.parseOrThrow(entry.available); + const v = av.value + av.fraction / amountFractionalBase; + return ( +

+ + {v}{" "} + {av.currency} + + {formatPending(entry)} +

+ ); + })}
+
+
+} diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx new file mode 100644 index 000000000..d1e76c053 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddConfirmProvider.stories.tsx @@ -0,0 +1,52 @@ +/* + 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { createExample } from '../test-utils'; +import { ConfirmProviderView as TestedComponent } from './ProviderAddPage'; + +export default { + title: 'wallet/backup/confirm', + component: TestedComponent, + argTypes: { + onRetry: { action: 'onRetry' }, + onDelete: { action: 'onDelete' }, + onBack: { action: 'onBack' }, + } +}; + + +export const DemoService = createExample(TestedComponent, { + 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, { + 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/wallet/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx new file mode 100644 index 000000000..2b205ebee --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx @@ -0,0 +1,150 @@ +import { Amounts, BackupBackupProviderTerms, canonicalizeBaseUrl, i18n } from "@gnu-taler/taler-util"; +import { verify } from "@gnu-taler/taler-wallet-core/src/crypto/primitives/nacl-fast"; +import { VNode, h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { Checkbox } from "../components/Checkbox"; +import { ErrorMessage } from "../components/ErrorMessage"; +import { Button, ButtonPrimary, Input, LightText, WalletBox, SmallTextLight } from "../components/styled/index"; +import * as wxApi from "../wxApi"; + +interface Props { + currency: string; + onBack: () => void; +} + +function getJsonIfOk(r: Response) { + if (r.ok) { + return r.json() + } else { + if (r.status >= 400 && r.status < 500) { + throw new Error(`URL may not be right: (${r.status}) ${r.statusText}`) + } else { + throw new Error(`Try another server: (${r.status}) ${r.statusText || 'internal server error'}`) + } + } +} + + +export function ProviderAddPage({ onBack }: Props): VNode { + const [verifying, setVerifying] = useState<{ url: string, name: string, provider: BackupBackupProviderTerms } | undefined>(undefined) + + async function getProviderInfo(url: string): Promise { + return fetch(`${url}config`) + .catch(e => { throw new Error(`Network error`) }) + .then(getJsonIfOk) + } + + if (!verifying) { + return getProviderInfo(url)} + onConfirm={(url, name) => getProviderInfo(url) + .then((provider) => { + setVerifying({ url, name, provider }); + }) + .catch(e => e.message) + } + /> + } + return { + setVerifying(undefined); + }} + onConfirm={() => { + wxApi.addBackupProvider(verifying.url, verifying.name).then(onBack) + }} + + /> +} + + +export interface SetUrlViewProps { + initialValue?: string; + onCancel: () => void; + onVerify: (s: string) => Promise; + onConfirm: (url: string, name: string) => Promise; + withError?: string; +} + +export function SetUrlView({ initialValue, onCancel, onVerify, onConfirm, withError }: SetUrlViewProps) { + const [value, setValue] = useState(initialValue || "") + const [urlError, setUrlError] = useState(false) + const [name, setName] = useState(undefined) + const [error, setError] = useState(withError) + useEffect(() => { + try { + const url = canonicalizeBaseUrl(value) + onVerify(url).then(r => { + setUrlError(false) + setName(new URL(url).hostname) + }).catch(() => { + setUrlError(true) + setName(undefined) + }) + } catch { + setUrlError(true) + setName(undefined) + } + }, [value]) + return +
+

Add backup provider

+ + Backup providers may charge for their service +

+ + + setValue(e.currentTarget.value)} /> + + + + setName(e.currentTarget.value)}/> + +

+
+
+ + { + const url = canonicalizeBaseUrl(value) + return onConfirm(url, name!).then(r => r ? setError(r) : undefined) + }}>Next +
+
+} + +export interface ConfirmProviderViewProps { + provider: BackupBackupProviderTerms, + url: string, + onCancel: () => void; + onConfirm: () => void; +} +export function ConfirmProviderView({ url, provider, onCancel, onConfirm }: ConfirmProviderViewProps) { + const [accepted, setAccepted] = useState(false); + + return +
+

Review terms of service

+
Provider URL: {url}
+ Please review and accept this provider's terms of service +

1. Pricing

+

+ {Amounts.isZero(provider.annual_fee) ? 'free of charge' : `${provider.annual_fee} per year of service`} +

+

2. Storage

+

+ {provider.storage_limit_in_megabytes} megabytes of storage per year of service +

+ setAccepted(old => !old)} enabled={accepted} /> +
+
+ + Add provider +
+
+} diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx new file mode 100644 index 000000000..4890e5e9c --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddSetUrl.stories.tsx @@ -0,0 +1,53 @@ +/* + 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { createExample } from '../test-utils'; +import { SetUrlView as TestedComponent } from './ProviderAddPage'; + +export default { + title: 'wallet/backup/add', + component: TestedComponent, + argTypes: { + onRetry: { action: 'onRetry' }, + onDelete: { action: 'onDelete' }, + onBack: { action: 'onBack' }, + } +}; + + +export const Initial = createExample(TestedComponent, { +}); + +export const WithValue = createExample(TestedComponent, { + initialValue: 'sync.demo.taler.net' +}); + +export const WithConnectionError = createExample(TestedComponent, { + withError: 'Network error' +}); + +export const WithClientError = createExample(TestedComponent, { + withError: 'URL may not be right: (404) Not Found' +}); + +export const WithServerError = createExample(TestedComponent, { + withError: 'Try another server: (500) Internal Server Error' +}); diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx new file mode 100644 index 000000000..67ff83442 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetail.stories.tsx @@ -0,0 +1,238 @@ +/* + 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { ProviderPaymentType } from '@gnu-taler/taler-wallet-core'; +import { createExample } from '../test-utils'; +import { ProviderView as TestedComponent } from './ProviderDetailPage'; + +export default { + title: 'wallet/backup/details', + component: TestedComponent, + argTypes: { + onRetry: { action: 'onRetry' }, + onDelete: { action: 'onDelete' }, + onBack: { action: 'onBack' }, + } +}; + + +export const Active = createExample(TestedComponent, { + info: { + "active": true, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.taler:9967/", + "lastSuccessfulBackupTimestamp": { + "t_ms": 1625063925078 + }, + "paymentProposalIds": [ + "43Q5WWRJPNS4SE9YKS54H9THDS94089EDGXW9EHBPN6E7M184XEG" + ], + "paymentStatus": { + "type": ProviderPaymentType.Paid, + "paidUntil": { + "t_ms": 1656599921000 + } + }, + "terms": { + "annualFee": "EUR:1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const ActiveErrorSync = createExample(TestedComponent, { + info: { + "active": true, + name:'sync.demo', + "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": "EUR:1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const ActiveBackupProblemUnreadable = createExample(TestedComponent, { + info: { + "active": true, + name:'sync.demo', + "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": "EUR:1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const ActiveBackupProblemDevice = createExample(TestedComponent, { + info: { + "active": true, + name:'sync.demo', + "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": "EUR:1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const InactiveUnpaid = createExample(TestedComponent, { + info: { + "active": false, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.Unpaid, + }, + "terms": { + "annualFee": "EUR:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const InactiveInsufficientBalance = createExample(TestedComponent, { + info: { + "active": false, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.InsufficientBalance, + }, + "terms": { + "annualFee": "EUR:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + +export const InactivePending = createExample(TestedComponent, { + info: { + "active": false, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.Pending, + }, + "terms": { + "annualFee": "EUR:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + + +export const ActiveTermsChanged = createExample(TestedComponent, { + info: { + "active": true, + name:'sync.demo', + "syncProviderBaseUrl": "http://sync.demo.taler.net/", + "paymentProposalIds": [], + "paymentStatus": { + "type": ProviderPaymentType.TermsChanged, + paidUntil: { + t_ms: 1656599921000 + }, + newTerms: { + "annualFee": "EUR:10", + "storageLimitInMegabytes": 8, + "supportedProtocolVersion": "0.0" + }, + oldTerms: { + "annualFee": "EUR:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + }, + "terms": { + "annualFee": "EUR:0.1", + "storageLimitInMegabytes": 16, + "supportedProtocolVersion": "0.0" + } + } +}); + diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx new file mode 100644 index 000000000..fc361f625 --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx @@ -0,0 +1,197 @@ +/* + 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 +*/ + + +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 { Fragment, VNode, h } from "preact"; +import { ErrorMessage } from "../components/ErrorMessage"; +import { Button, ButtonDestructive, ButtonPrimary, PaymentStatus, WalletBox, SmallTextLight } from "../components/styled"; +import { useProviderStatus } from "../hooks/useProviderStatus"; + +interface Props { + pid: string; + onBack: () => void; +} + +export function ProviderDetailPage({ pid, onBack }: Props): VNode { + const status = useProviderStatus(pid) + if (!status) { + return
Loading...
+ } + if (!status.info) { + onBack() + return
+ } + return status.remove().then(onBack)} + onBack={onBack} + onExtend={() => { null }} + />; +} + +export interface ViewProps { + info: ProviderInfo; + onDelete: () => void; + onSync: () => void; + onBack: () => void; + onExtend: () => 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 + return ( + + {info.backupProblem || info.lastError ?
+ +
: undefined } +
+

{info.name} {info.syncProviderBaseUrl}

+ {isPaid ? 'Paid' : 'Unpaid'} +
+
+

Last backup: {lb == null || lb.t_ms == "never" ? "never" : format(lb.t_ms, 'dd MMM yyyy')}

+ Back up + {info.terms && +

Provider fee: {info.terms && info.terms.annualFee} per year

+
+ } +

{descriptionByStatus(info.paymentStatus)}

+ Extend + + {info.paymentStatus.type === ProviderPaymentType.TermsChanged &&
+

terms has changed, extending the service will imply accepting the new terms of service

+ + + + + + + + + + + + + + + + + + + + + + + + +
old ->new
fee{info.paymentStatus.oldTerms.annualFee}->{info.paymentStatus.newTerms.annualFee}
storage{info.paymentStatus.oldTerms.storageLimitInMegabytes}->{info.paymentStatus.newTerms.storageLimitInMegabytes}
+
} + +
+
+ +
+ remove provider +
+
+
+ ) +} + +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` +} + +function Error({ info }: { info: ProviderInfo }) { + if (info.lastError) { + return + } + if (info.backupProblem) { + switch (info.backupProblem.type) { + case "backup-conflicting-device": + return + There is conflict with another backup from {info.backupProblem.otherDeviceId} + } /> + case "backup-unreadable": + return + default: + return + Unknown backup problem: {JSON.stringify(info.backupProblem)} + } /> + } + } + return null +} + +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) { + 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 + Backup valid until: {format(status.paidUntil.t_ms, 'dd MMM yyyy')} + + } + case ProviderPaymentType.Unpaid: + case ProviderPaymentType.InsufficientBalance: + case ProviderPaymentType.Pending: + return '' + } +} diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx new file mode 100644 index 000000000..deb30e55f --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Settings.stories.tsx @@ -0,0 +1,43 @@ +/* + 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { createExample } from '../test-utils'; +import { SettingsView as TestedComponent } from './Settings'; + +export default { + title: 'wallet/settings', + component: TestedComponent, + argTypes: { + setDeviceName: () => Promise.resolve(), + } +}; + +export const AllOff = createExample(TestedComponent, { + deviceName: 'this-is-the-device-name', + setDeviceName: () => Promise.resolve(), +}); + +export const OneChecked = createExample(TestedComponent, { + deviceName: 'this-is-the-device-name', + permissionsEnabled: true, + setDeviceName: () => Promise.resolve(), +}); + diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx new file mode 100644 index 000000000..52e72ee2f --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -0,0 +1,103 @@ +/* + 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 +*/ + + +import { i18n } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; +import { Checkbox } from "../components/Checkbox"; +import { EditableText } from "../components/EditableText"; +import { SelectList } from "../components/SelectList"; +import { useDevContext } from "../context/devContext"; +import { useBackupDeviceName } from "../hooks/useBackupDeviceName"; +import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; +import { useLang } from "../hooks/useLang"; + +export function SettingsPage(): VNode { + const [permissionsEnabled, togglePermissions] = useExtendedPermissions(); + const { devMode, toggleDevMode } = useDevContext() + const { name, update } = useBackupDeviceName() + const [lang, changeLang] = useLang() + return ; +} + +export interface ViewProps { + lang: string; + changeLang: (s: string) => void; + deviceName: string; + setDeviceName: (s: string) => Promise; + permissionsEnabled: boolean; + togglePermissions: () => void; + developerMode: boolean; + toggleDeveloperMode: () => void; +} + +import { strings as messages } from '../i18n/strings' + +type LangsNames = { + [P in keyof typeof messages]: string +} + +const names: LangsNames = { + es: 'Español [es]', + en: 'English [en]', + fr: 'Français [fr]', + de: 'Deutsch [de]', + sv: 'Svenska [sv]', + it: 'Italiano [it]', +} + + +export function SettingsView({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode { + return ( +
+
+

Wallet

+ + +

Permissions

+ +

Config

+ +
+
+ ) +} \ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx new file mode 100644 index 000000000..0f7ea457d --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx @@ -0,0 +1,276 @@ +/* + 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 + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { + PaymentStatus, + TransactionCommon, TransactionDeposit, TransactionPayment, + TransactionRefresh, TransactionRefund, TransactionTip, TransactionType, + TransactionWithdrawal, + WithdrawalType +} from '@gnu-taler/taler-util'; +import { createExample } from '../test-utils'; +import { TransactionView as TestedComponent } from './Transaction'; + +export default { + title: 'wallet/history/details', + component: TestedComponent, + argTypes: { + onRetry: { action: 'onRetry' }, + onDelete: { action: 'onDelete' }, + onBack: { action: 'onBack' }, + } +}; + +const commonTransaction = { + amountRaw: 'USD:10', + amountEffective: 'USD:9', + pending: false, + timestamp: { + t_ms: new Date().getTime() + }, + transactionId: '12', +} as TransactionCommon + +const exampleData = { + withdraw: { + ...commonTransaction, + type: TransactionType.Withdrawal, + exchangeBaseUrl: 'http://exchange.taler', + withdrawalDetails: { + confirmed: false, + exchangePaytoUris: ['payto://x-taler-bank/bank/account'], + type: WithdrawalType.ManualTransfer, + } + } as TransactionWithdrawal, + payment: { + ...commonTransaction, + amountEffective: 'USD:11', + type: TransactionType.Payment, + info: { + contractTermsHash: 'ASDZXCASD', + merchant: { + name: 'the merchant', + }, + orderId: '2021.167-03NPY6MCYMVGT', + products: [], + summary: "Essay: Why the Devil's Advocate Doesn't Help Reach the Truth", + fulfillmentMessage: '', + }, + proposalId: '1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + status: PaymentStatus.Accepted, + } as TransactionPayment, + deposit: { + ...commonTransaction, + type: TransactionType.Deposit, + depositGroupId: '#groupId', + targetPaytoUri: 'payto://x-taler-bank/bank/account', + } as TransactionDeposit, + refresh: { + ...commonTransaction, + type: TransactionType.Refresh, + exchangeBaseUrl: 'http://exchange.taler', + } as TransactionRefresh, + tip: { + ...commonTransaction, + type: TransactionType.Tip, + merchantBaseUrl: 'http://merchant.taler', + } as TransactionTip, + refund: { + ...commonTransaction, + type: TransactionType.Refund, + refundedTransactionId: 'payment:1EMJJH8EP1NX3XF7733NCYS2DBEJW4Q2KA5KEB37MCQJQ8Q5HMC0', + info: { + contractTermsHash: 'ASDZXCASD', + merchant: { + name: 'the merchant', + }, + orderId: '2021.167-03NPY6MCYMVGT', + products: [], + summary: 'the summary', + fulfillmentMessage: '', + }, + } as TransactionRefund, +} + +const transactionError = { + code: 2000, + details: "details", + hint: "this is a hint for the error", + message: 'message' +} + +export const Withdraw = createExample(TestedComponent, { + transaction: exampleData.withdraw +}); + +export const WithdrawError = createExample(TestedComponent, { + transaction: { + ...exampleData.withdraw, + error: transactionError, + }, +}); + +export const WithdrawPending = createExample(TestedComponent, { + transaction: { ...exampleData.withdraw, pending: true }, +}); + + +export const Payment = createExample(TestedComponent, { + transaction: exampleData.payment +}); + +export const PaymentError = createExample(TestedComponent, { + transaction: { + ...exampleData.payment, + error: transactionError + }, +}); + +export const PaymentWithoutFee = createExample(TestedComponent, { + transaction: { + ...exampleData.payment, + amountRaw: 'USD:11', + + } +}); + +export const PaymentPending = createExample(TestedComponent, { + transaction: { ...exampleData.payment, pending: true }, +}); + +export const PaymentWithProducts = createExample(TestedComponent, { + transaction: { + ...exampleData.payment, + info: { + ...exampleData.payment.info, + summary: 'this order has 5 products', + products: [{ + description: 't-shirt', + unit: 'shirts', + quantity: 1, + }, { + description: 't-shirt', + unit: 'shirts', + quantity: 1, + }, { + description: 'e-book', + }, { + description: 'beer', + unit: 'pint', + quantity: 15, + }, { + description: 'beer', + unit: 'pint', + quantity: 15, + }] + } + } as TransactionPayment, +}); + +export const PaymentWithLongSummary = createExample(TestedComponent, { + transaction: { + ...exampleData.payment, + info: { + ...exampleData.payment.info, + summary: 'this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, this is a very long summary that will occupy severals lines, ', + products: [{ + description: 'an xl sized t-shirt with some drawings on it, color pink', + unit: 'shirts', + quantity: 1, + }, { + description: 'beer', + unit: 'pint', + quantity: 15, + }] + } + } as TransactionPayment, +}); + + +export const Deposit = createExample(TestedComponent, { + transaction: exampleData.deposit +}); + +export const DepositError = createExample(TestedComponent, { + transaction: { + ...exampleData.deposit, + error: transactionError + }, +}); + +export const DepositPending = createExample(TestedComponent, { + transaction: { ...exampleData.deposit, pending: true } +}); + +export const Refresh = createExample(TestedComponent, { + transaction: exampleData.refresh +}); + +export const RefreshError = createExample(TestedComponent, { + transaction: { + ...exampleData.refresh, + error: transactionError + }, +}); + +export const Tip = createExample(TestedComponent, { + transaction: exampleData.tip +}); + +export const TipError = createExample(TestedComponent, { + transaction: { + ...exampleData.tip, + error: transactionError + }, +}); + +export const TipPending = createExample(TestedComponent, { + transaction: { ...exampleData.tip, pending: true } +}); + +export const Refund = createExample(TestedComponent, { + transaction: exampleData.refund +}); + +export const RefundError = createExample(TestedComponent, { + transaction: { + ...exampleData.refund, + error: transactionError + }, +}); + +export const RefundPending = createExample(TestedComponent, { + transaction: { ...exampleData.refund, pending: true } +}); + +export const RefundWithProducts = createExample(TestedComponent, { + transaction: { + ...exampleData.refund, + info: { + ...exampleData.refund.info, + products: [{ + description: 't-shirt', + }, { + description: 'beer', + }] + } + } as TransactionRefund, +}); diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx new file mode 100644 index 000000000..d00abc16a --- /dev/null +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -0,0 +1,217 @@ +/* + 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 + */ + +import { AmountJson, Amounts, i18n, Transaction, TransactionType } from "@gnu-taler/taler-util"; +import { format } from "date-fns"; +import { Fragment, JSX, VNode, h } from "preact"; +import { route } from 'preact-router'; +import { useEffect, useState } from "preact/hooks"; +import * as wxApi from "../wxApi"; +import { Pages } from "../NavigationBar"; +import emptyImg from "../../static/img/empty.png" +import { Button, ButtonDestructive, ButtonPrimary, ListOfProducts, PopupBox, Row, RowBorderGray, SmallTextLight, WalletBox } from "../components/styled"; +import { ErrorMessage } from "../components/ErrorMessage"; + +export function TransactionPage({ tid }: { tid: string; }): JSX.Element { + const [transaction, setTransaction] = useState< + Transaction | undefined + >(undefined); + + useEffect(() => { + const fetchData = async (): Promise => { + const res = await wxApi.getTransactions(); + const ts = res.transactions.filter(t => t.transactionId === tid); + if (ts.length === 1) { + setTransaction(ts[0]); + } else { + route(Pages.history); + } + }; + fetchData(); + }, []); + + if (!transaction) { + return
Loading ...
; + } + return wxApi.deleteTransaction(tid).then(_ => history.go(-1))} + onRetry={() => wxApi.retryTransaction(tid).then(_ => history.go(-1))} + onBack={() => { history.go(-1); }} />; +} + +export interface WalletTransactionProps { + transaction: Transaction, + onDelete: () => void, + onRetry: () => void, + onBack: () => void, +} + + +export function TransactionView({ transaction, onDelete, onRetry, onBack }: WalletTransactionProps) { + + function Status() { + if (transaction.error) { + return (failed) + } + if (transaction.pending) { + return (pending...) + } + return null + } + + function Fee({ value }: { value: AmountJson }) { + if (Amounts.isZero(value)) return null + return (fee {Amounts.stringify(value)}) + } + + function TransactionTemplate({ upperRight, children }: { upperRight: VNode, children: VNode[] }) { + return +
+ + {transaction.timestamp.t_ms === "never" ? "never" : format(transaction.timestamp.t_ms, 'dd/MM/yyyy HH:mm:ss')} + + + {upperRight} + +
+
+ + {children} +
+
+ +
+ {transaction?.error ? retry : null} + delete +
+
+
+ } + + if (transaction.type === TransactionType.Withdrawal) { + const fee = Amounts.sub( + Amounts.parseOrThrow(transaction.amountRaw), + Amounts.parseOrThrow(transaction.amountEffective), + ).amount + return From {transaction.exchangeBaseUrl}}> +

Withdraw

+

{transaction.amountEffective}

+
+ } + + const showLargePic = () => { + + } + + if (transaction.type === TransactionType.Payment) { + const fee = Amounts.sub( + Amounts.parseOrThrow(transaction.amountEffective), + Amounts.parseOrThrow(transaction.amountRaw), + ).amount + + return To {transaction.info.merchant.name}}> +

Payment

+

{transaction.amountEffective}

+ #{transaction.info.orderId} +

+ {transaction.info.summary} +

+
+ {transaction.info.products && transaction.info.products.length > 0 && + + {transaction.info.products.map(p => + + + +
+ {p.quantity && p.quantity > 0 && x {p.quantity} {p.unit}} +
{p.description}
+
+
)} +
+ } +
+
+ } + + if (transaction.type === TransactionType.Deposit) { + const fee = Amounts.sub( + Amounts.parseOrThrow(transaction.amountRaw), + Amounts.parseOrThrow(transaction.amountEffective), + ).amount + return To {transaction.targetPaytoUri}}> +

Deposit

+

{transaction.amountEffective}

+
+ } + + if (transaction.type === TransactionType.Refresh) { + const fee = Amounts.sub( + Amounts.parseOrThrow(transaction.amountRaw), + Amounts.parseOrThrow(transaction.amountEffective), + ).amount + return From {transaction.exchangeBaseUrl}}> +

Refresh

+

{transaction.amountEffective}

+
+ } + + if (transaction.type === TransactionType.Tip) { + const fee = Amounts.sub( + Amounts.parseOrThrow(transaction.amountRaw), + Amounts.parseOrThrow(transaction.amountEffective), + ).amount + return From {transaction.merchantBaseUrl}}> +

Tip

+

{transaction.amountEffective}

+
+ } + + if (transaction.type === TransactionType.Refund) { + const fee = Amounts.sub( + Amounts.parseOrThrow(transaction.amountRaw), + Amounts.parseOrThrow(transaction.amountEffective), + ).amount + return From {transaction.info.merchant.name}}> +

Refund

+

{transaction.amountEffective}

+ + #{transaction.info.orderId} +

+ {transaction.info.summary} +

+
+ {transaction.info.products && transaction.info.products.length > 0 && + + {transaction.info.products.map(p => + + + +
+ {p.quantity && p.quantity > 0 && x {p.quantity} {p.unit}} +
{p.description}
+
+
)} +
+ } +
+
+ } + + + return
+} diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx index 61df45e7e..aa007786c 100644 --- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx +++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx @@ -20,24 +20,28 @@ * @author Florian Dold */ -import { Fragment, render, h } from "preact"; import { setupI18n } from "@gnu-taler/taler-util"; -import { strings } from "./i18n/strings"; import { createHashHistory } from 'history'; - -import { WelcomePage } from "./wallet/Welcome"; -import { HistoryPage } from "./wallet/History"; -import { WithdrawPage } from "./cta/Withdraw"; +import { Fragment, h, render } from "preact"; +import Router, { route, Route } from "preact-router"; +import { useEffect } from "preact/hooks"; +import { LogoHeader } from "./components/LogoHeader"; +import { DevContextProvider } from "./context/devContext"; import { PayPage } from "./cta/Pay"; import { RefundPage } from "./cta/Refund"; import { TipPage } from './cta/Tip'; -import Router, { route, Route } from "preact-router"; -import { DevContextProvider } from "./context/devContext"; -import { LogoHeader } from "./components/LogoHeader"; -import { useEffect } from "preact/hooks"; +import { WithdrawPage } from "./cta/Withdraw"; +import { strings } from "./i18n/strings"; import { Pages, WalletNavBar } from "./NavigationBar"; +import { BalancePage } from "./wallet/BalancePage"; +import { HistoryPage } from "./wallet/History"; +import { SettingsPage } from "./wallet/Settings"; +import { TransactionPage } from './wallet/Transaction'; +import { WelcomePage } from "./wallet/Welcome"; +import { BackupPage } from './wallet/BackupPage'; + function main(): void { try { @@ -76,7 +80,10 @@ function Application() { - + + + +
no yet implemented
} />
no yet implemented
} /> -- cgit v1.2.3