From be8e3f4b1d090a536967f132a7fd4742bbcd5343 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 11 Oct 2021 15:59:55 -0300 Subject: fixing withdrawal process --- .../src/components/SelectList.tsx | 8 +- .../src/components/styled/index.tsx | 23 ++- packages/taler-wallet-webextension/src/cta/Pay.tsx | 31 +++- .../src/cta/Withdraw.stories.tsx | 148 ++++++++++++++++-- .../taler-wallet-webextension/src/cta/Withdraw.tsx | 171 +++++++++++---------- .../src/hooks/useAsyncAsHook.ts | 48 ++++++ .../src/hooks/useBalances.ts | 54 +++++++ .../src/hooks/useBalances.tsx | 51 ------ .../src/popup/Balance.stories.tsx | 128 ++++++++++++++- .../src/popup/BalancePage.tsx | 129 ++++++++++------ .../taler-wallet-webextension/src/popup/Debug.tsx | 1 - .../src/popup/History.stories.tsx | 6 +- .../src/popup/History.tsx | 30 ++-- .../src/popup/Settings.tsx | 19 ++- .../src/wallet/Balance.stories.tsx | 11 +- .../src/wallet/BalancePage.tsx | 2 +- .../src/wallet/History.tsx | 2 +- .../src/wallet/Settings.tsx | 49 +++--- packages/taler-wallet-webextension/src/wxApi.ts | 5 + 19 files changed, 651 insertions(+), 265 deletions(-) create mode 100644 packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts create mode 100644 packages/taler-wallet-webextension/src/hooks/useBalances.ts delete mode 100644 packages/taler-wallet-webextension/src/hooks/useBalances.tsx (limited to 'packages/taler-wallet-webextension/src') diff --git a/packages/taler-wallet-webextension/src/components/SelectList.tsx b/packages/taler-wallet-webextension/src/components/SelectList.tsx index 7890c3fa4..536e5b89a 100644 --- a/packages/taler-wallet-webextension/src/components/SelectList.tsx +++ b/packages/taler-wallet-webextension/src/components/SelectList.tsx @@ -19,7 +19,7 @@ import { NiceSelect } from "./styled/index"; import { h } from "preact"; interface Props { - value: string; + value?: string; onChange: (s: string) => void; label: string; list: { @@ -41,9 +41,11 @@ export function SelectList({ name, value, list, canBeNull, onChange, label, desc console.log(e.currentTarget.value, value) onChange(e.currentTarget.value) }}> - + : } {Object.keys(list) .filter((l) => l !== value) .map(key => ) diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx index e77e7d542..7c3bb3943 100644 --- a/packages/taler-wallet-webextension/src/components/styled/index.tsx +++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx @@ -129,6 +129,12 @@ export const WalletBox = styled.div<{ noPadding?: boolean }>` } } ` +export const Middle = styled.div` + justify-content: space-around; + display: flex; + flex-direction: column; + height: 100%; +` export const PopupBox = styled.div<{ noPadding?: boolean }>` height: 290px; @@ -138,11 +144,10 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>` justify-content: space-between; & > section { - padding-left: ${({ noPadding }) => noPadding ? '0px' : '8px'}; - padding-right: ${({ noPadding }) => noPadding ? '0px' : '8px'}; + padding: ${({ noPadding }) => noPadding ? '0px' : '8px'}; // this margin will send the section up when used with a header margin-bottom: auto; - overflow: auto; + overflow-y: auto; table td { padding: 5px 10px; @@ -153,6 +158,16 @@ export const PopupBox = styled.div<{ noPadding?: boolean }>` } } + & > section[data-expanded] { + height: 100%; + } + + & > section[data-centered] { + justify-content: center; + display: flex; + /* flex-direction: column; */ + } + & > header { flex-direction: row; justify-content: space-between; @@ -596,7 +611,7 @@ export const NiceSelect = styled.div` position: relative; display: flex; - width: 10em; + /* width: 10em; */ overflow: hidden; border-radius: .25em; diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx b/packages/taler-wallet-webextension/src/cta/Pay.tsx index 8e02cf6bb..675b14ff9 100644 --- a/packages/taler-wallet-webextension/src/cta/Pay.tsx +++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx @@ -88,7 +88,7 @@ export function PayPage({ talerPayUri }: Props): JSX.Element { const [payErrMsg, setPayErrMsg] = useState(undefined); const balance = useBalances() - const balanceWithoutError = balance?.error ? [] : (balance?.response.balances || []) + const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || []) const foundBalance = balanceWithoutError.find(b => payStatus && Amounts.parseOrThrow(b.available).currency === Amounts.parseOrThrow(payStatus?.amountRaw).currency) const foundAmount = foundBalance ? Amounts.parseOrThrow(foundBalance.available) : undefined @@ -143,17 +143,21 @@ export function PayPage({ talerPayUri }: Props): JSX.Element { } - return ; + return ; } export interface PaymentRequestViewProps { payStatus: PreparePayResult; + payResult?: ConfirmPayResult; onClick: () => void; payErrMsg?: string; uri: string; balance: AmountJson | undefined; } -export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg, balance }: PaymentRequestViewProps) { +export function PaymentRequestView({ uri, payStatus, payResult, onClick, payErrMsg, balance }: PaymentRequestViewProps) { let totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw); const contractTerms: ContractTerms = payStatus.contractTerms; @@ -195,6 +199,16 @@ export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg, balance } function ButtonsSection() { + if (payResult) { + if (payResult.type === ConfirmPayResultType.Pending) { + return
+
+

Processing...

+
+
+ } + return null + } if (payErrMsg) { return
@@ -208,7 +222,7 @@ export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg, balance if (payStatus.status === PreparePayResultType.PaymentPossible) { return
- + {i18n.str`Pay`} {amountToString(payStatus.amountEffective)}
@@ -252,6 +266,15 @@ export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg, balance {payStatus.status === PreparePayResultType.AlreadyConfirmed && (payStatus.paid ? Already paid : Already claimed ) } + {payResult && payResult.type === ConfirmPayResultType.Done && ( + +

Payment complete

+

{!payResult.contractTerms.fulfillment_message ? + "You will now be sent back to the merchant you came from." : + payResult.contractTerms.fulfillment_message + }

+
+ )}
{payStatus.status !== PreparePayResultType.InsufficientBalance && Amounts.isNonZero(totalFees) && diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx index 94fdea8fb..69073f500 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx @@ -31,6 +31,7 @@ export default { title: 'cta/withdraw', component: TestedComponent, argTypes: { + onSwitchExchange: { action: 'onRetry' }, }, }; @@ -381,6 +382,15 @@ const termsXml = ` `; export const WithdrawNewTermsXML = createExample(TestedComponent, { + knownExchanges: [{ + currency: 'USD', + exchangeBaseUrl: 'exchange.demo.taler.net', + paytoUris: ['asd'], + },{ + currency: 'USD', + exchangeBaseUrl: 'exchange.test.taler.net', + paytoUris: ['asd'], + }], details: { exchangeInfo: { baseUrl: 'exchange.demo.taler.net' @@ -391,9 +401,15 @@ export const WithdrawNewTermsXML = createExample(TestedComponent, { value: 0 }, } as ExchangeWithdrawDetails, - amount: 'USD:2', + amount: { + currency: 'USD', + value: 2, + fraction: 10000000 + }, + + onSwitchExchange: async () => { }, terms: { - value : { + value: { type: 'xml', document: new DOMParser().parseFromString(termsXml, "text/xml"), }, @@ -402,6 +418,15 @@ export const WithdrawNewTermsXML = createExample(TestedComponent, { }) export const WithdrawNewTermsReviewingXML = createExample(TestedComponent, { + knownExchanges: [{ + currency: 'USD', + exchangeBaseUrl: 'exchange.demo.taler.net', + paytoUris: ['asd'], + },{ + currency: 'USD', + exchangeBaseUrl: 'exchange.test.taler.net', + paytoUris: ['asd'], + }], details: { exchangeInfo: { baseUrl: 'exchange.demo.taler.net' @@ -412,9 +437,15 @@ export const WithdrawNewTermsReviewingXML = createExample(TestedComponent, { value: 0 }, } as ExchangeWithdrawDetails, - amount: 'USD:2', + amount: { + currency: 'USD', + value: 2, + fraction: 10000000 + }, + + onSwitchExchange: async () => { }, terms: { - value : { + value: { type: 'xml', document: new DOMParser().parseFromString(termsXml, "text/xml"), }, @@ -424,6 +455,15 @@ export const WithdrawNewTermsReviewingXML = createExample(TestedComponent, { }) export const WithdrawNewTermsAcceptedXML = createExample(TestedComponent, { + knownExchanges: [{ + currency: 'USD', + exchangeBaseUrl: 'exchange.demo.taler.net', + paytoUris: ['asd'], + },{ + currency: 'USD', + exchangeBaseUrl: 'exchange.test.taler.net', + paytoUris: ['asd'], + }], details: { exchangeInfo: { baseUrl: 'exchange.demo.taler.net' @@ -434,9 +474,14 @@ export const WithdrawNewTermsAcceptedXML = createExample(TestedComponent, { value: 0 }, } as ExchangeWithdrawDetails, - amount: 'USD:2', + amount: { + currency: 'USD', + value: 2, + fraction: 10000000 + }, + onSwitchExchange: async () => { }, terms: { - value : { + value: { type: 'xml', document: new DOMParser().parseFromString(termsXml, "text/xml"), }, @@ -446,6 +491,15 @@ export const WithdrawNewTermsAcceptedXML = createExample(TestedComponent, { }) export const WithdrawNewTermsShowAfterAcceptedXML = createExample(TestedComponent, { + knownExchanges: [{ + currency: 'USD', + exchangeBaseUrl: 'exchange.demo.taler.net', + paytoUris: ['asd'], + },{ + currency: 'USD', + exchangeBaseUrl: 'exchange.test.taler.net', + paytoUris: ['asd'], + }], details: { exchangeInfo: { baseUrl: 'exchange.demo.taler.net' @@ -456,9 +510,15 @@ export const WithdrawNewTermsShowAfterAcceptedXML = createExample(TestedComponen value: 0 }, } as ExchangeWithdrawDetails, - amount: 'USD:2', + amount: { + currency: 'USD', + value: 2, + fraction: 10000000 + }, + + onSwitchExchange: async () => { }, terms: { - value : { + value: { type: 'xml', document: new DOMParser().parseFromString(termsXml, "text/xml"), }, @@ -469,6 +529,15 @@ export const WithdrawNewTermsShowAfterAcceptedXML = createExample(TestedComponen }) export const WithdrawChangedTermsXML = createExample(TestedComponent, { + knownExchanges: [{ + currency: 'USD', + exchangeBaseUrl: 'exchange.demo.taler.net', + paytoUris: ['asd'], + },{ + currency: 'USD', + exchangeBaseUrl: 'exchange.test.taler.net', + paytoUris: ['asd'], + }], details: { exchangeInfo: { baseUrl: 'exchange.demo.taler.net' @@ -479,9 +548,15 @@ export const WithdrawChangedTermsXML = createExample(TestedComponent, { value: 0 }, } as ExchangeWithdrawDetails, - amount: 'USD:2', + amount: { + currency: 'USD', + value: 2, + fraction: 10000000 + }, + + onSwitchExchange: async () => { }, terms: { - value : { + value: { type: 'xml', document: new DOMParser().parseFromString(termsXml, "text/xml"), }, @@ -490,6 +565,15 @@ export const WithdrawChangedTermsXML = createExample(TestedComponent, { }) export const WithdrawNotFoundTermsXML = createExample(TestedComponent, { + knownExchanges: [{ + currency: 'USD', + exchangeBaseUrl: 'exchange.demo.taler.net', + paytoUris: ['asd'], + },{ + currency: 'USD', + exchangeBaseUrl: 'exchange.test.taler.net', + paytoUris: ['asd'], + }], details: { exchangeInfo: { baseUrl: 'exchange.demo.taler.net' @@ -500,13 +584,28 @@ export const WithdrawNotFoundTermsXML = createExample(TestedComponent, { value: 0 }, } as ExchangeWithdrawDetails, - amount: 'USD:2', + amount: { + currency: 'USD', + value: 2, + fraction: 10000000 + }, + + onSwitchExchange: async () => { }, terms: { status: 'notfound' }, }) export const WithdrawAcceptedTermsXML = createExample(TestedComponent, { + knownExchanges: [{ + currency: 'USD', + exchangeBaseUrl: 'exchange.demo.taler.net', + paytoUris: ['asd'], + },{ + currency: 'USD', + exchangeBaseUrl: 'exchange.test.taler.net', + paytoUris: ['asd'], + }], details: { exchangeInfo: { baseUrl: 'exchange.demo.taler.net' @@ -517,7 +616,13 @@ export const WithdrawAcceptedTermsXML = createExample(TestedComponent, { value: 0 }, } as ExchangeWithdrawDetails, - amount: 'USD:2', + amount: { + currency: 'USD', + value: 2, + fraction: 10000000 + }, + + onSwitchExchange: async () => { }, terms: { status: 'accepted' }, @@ -525,6 +630,15 @@ export const WithdrawAcceptedTermsXML = createExample(TestedComponent, { export const WithdrawAcceptedTermsWithoutFee = createExample(TestedComponent, { + knownExchanges: [{ + currency: 'USD', + exchangeBaseUrl: 'exchange.demo.taler.net', + paytoUris: ['asd'], + },{ + currency: 'USD', + exchangeBaseUrl: 'exchange.test.taler.net', + paytoUris: ['asd'], + }], details: { exchangeInfo: { baseUrl: 'exchange.demo.taler.net' @@ -535,9 +649,15 @@ export const WithdrawAcceptedTermsWithoutFee = createExample(TestedComponent, { value: 0 }, } as ExchangeWithdrawDetails, - amount: 'USD:2', + amount: { + currency: 'USD', + value: 2, + fraction: 10000000 + }, + + onSwitchExchange: async () => { }, terms: { - value : { + value: { type: 'xml', document: new DOMParser().parseFromString(termsXml, "text/xml"), }, diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx index 46451e72c..52295f1af 100644 --- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx +++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx @@ -21,18 +21,21 @@ * @author Florian Dold */ -import { AmountLike, Amounts, i18n, WithdrawUriInfoResponse } from '@gnu-taler/taler-util'; +import { AmountJson, Amounts, ExchangeListItem, i18n, WithdrawUriInfoResponse } from '@gnu-taler/taler-util'; import { ExchangeWithdrawDetails } from '@gnu-taler/taler-wallet-core/src/operations/withdraw'; -import { useEffect, useState } from "preact/hooks"; +import { useState } from "preact/hooks"; +import { Fragment } from 'preact/jsx-runtime'; import { CheckboxOutlined } from '../components/CheckboxOutlined'; import { ExchangeXmlTos } from '../components/ExchangeToS'; import { LogoHeader } from '../components/LogoHeader'; import { Part } from '../components/Part'; -import { ButtonDestructive, ButtonSuccess, ButtonWarning, LinkSuccess, LinkWarning, TermsOfService, WalletAction } from '../components/styled'; +import { SelectList } from '../components/SelectList'; +import { ButtonSuccess, ButtonWarning, LinkSuccess, LinkWarning, TermsOfService, WalletAction } from '../components/styled'; +import { useAsyncAsHook } from '../hooks/useAsyncAsHook'; import { - acceptWithdrawal, getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, onUpdateNotification, setExchangeTosAccepted + acceptWithdrawal, getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, setExchangeTosAccepted, listExchanges } from "../wxApi"; -import { h } from 'preact'; +import { wxMain } from '../wxBackend.js'; interface Props { talerWithdrawUri?: string; @@ -40,7 +43,8 @@ interface Props { export interface ViewProps { details: ExchangeWithdrawDetails; - amount: string; + amount: AmountJson; + onSwitchExchange: (ex: string) => void; onWithdraw: () => Promise; onReview: (b: boolean) => void; onAccept: (b: boolean) => void; @@ -50,7 +54,8 @@ export interface ViewProps { terms: { value?: TermsDocument; status: TermsStatus; - } + }, + knownExchanges: ExchangeListItem[] }; @@ -68,15 +73,18 @@ interface TermsDocumentHtml { href: string, } -function amountToString(text: AmountLike) { +function amountToString(text: AmountJson) { const aj = Amounts.jsonifyAmount(text) const amount = Amounts.stringifyValue(aj) return `${amount} ${aj.currency}` } -export function View({ details, amount, onWithdraw, terms, reviewing, onReview, onAccept, accepted, confirmed }: ViewProps) { +export function View({ details, knownExchanges, amount, onWithdraw, onSwitchExchange, terms, reviewing, onReview, onAccept, accepted, confirmed }: ViewProps) { const needsReview = terms.status === 'changed' || terms.status === 'new' + const [switchingExchange, setSwitchingExchange] = useState(undefined) + const exchanges = knownExchanges.reduce((prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }), {}) + return ( @@ -84,7 +92,7 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview, {i18n.str`Digital cash withdrawal`}
- + {Amounts.isNonZero(details.withdrawFee) && @@ -93,11 +101,21 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview,
{!reviewing &&
- - {i18n.str`Edit exchange`} - + {switchingExchange !== undefined ? +
+ +
+

+ This is the list of known exchanges +

+ onSwitchExchange(switchingExchange)}> + {i18n.str`Confirm exchange selection`} + +
+ : setSwitchingExchange("")}> + {i18n.str`Switch exchange`} + } +
} {!reviewing && accepted && @@ -140,6 +158,9 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview,
} + {/** + * Main action section + */}
{terms.status === 'new' && !accepted && !reviewing && (undefined); - const [details, setDetails] = useState(undefined); - const [cancelled, setCancelled] = useState(false); - const [selecting, setSelecting] = useState(false); - const [error, setError] = useState(false); - const [updateCounter, setUpdateCounter] = useState(1); +export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriInfo: WithdrawUriInfoResponse }) { + const [customExchange, setCustomExchange] = useState(undefined) + const [errorAccepting, setErrorAccepting] = useState(undefined) + const [reviewing, setReviewing] = useState(false) const [accepted, setAccepted] = useState(false) const [confirmed, setConfirmed] = useState(false) - useEffect(() => { - return onUpdateNotification(() => { - console.log('updating...') - setUpdateCounter(updateCounter + 1); - }); - }, []); - - useEffect(() => { - console.log('on effect yes', talerWithdrawUri) - if (!talerWithdrawUri) return - const fetchData = async (): Promise => { - try { - const res = await getWithdrawalDetailsForUri({ talerWithdrawUri }); - setUriInfo(res); - } catch (e) { - console.error('error', JSON.stringify(e, undefined, 2)) - setError(true) - } - }; - fetchData(); - }, [selecting, talerWithdrawUri, updateCounter]); + const knownExchangesHook = useAsyncAsHook(() => listExchanges()) - useEffect(() => { - async function fetchData() { - if (!uriInfo || !uriInfo.defaultExchangeBaseUrl) return - try { - const res = await getExchangeWithdrawalInfo({ - exchangeBaseUrl: uriInfo.defaultExchangeBaseUrl, - amount: Amounts.parseOrThrow(uriInfo.amount), - tosAcceptedFormat: ['text/json', 'text/xml', 'text/pdf'] - }) - setDetails(res) - } catch (e) { - setError(true) - } - } - fetchData() - }, [uriInfo]) + const knownExchanges = !knownExchangesHook || knownExchangesHook.hasError ? [] : knownExchangesHook.response.exchanges + const withdrawAmount = Amounts.parseOrThrow(uriInfo.amount) + const thisCurrencyExchanges = knownExchanges.filter(ex => ex.currency === withdrawAmount.currency) - if (!talerWithdrawUri) { - return missing withdraw uri; + const exchange = customExchange || uriInfo.defaultExchangeBaseUrl || thisCurrencyExchanges[0]?.exchangeBaseUrl + const detailsHook = useAsyncAsHook(async () => { + if (!exchange) throw Error('no default exchange') + return getExchangeWithdrawalInfo({ + exchangeBaseUrl: exchange, + amount: withdrawAmount, + tosAcceptedFormat: ['text/json', 'text/xml', 'text/pdf'] + }) + }) + + if (!detailsHook) { + return Getting withdrawal details.; + } + if (detailsHook.hasError) { + return Problems getting details: {detailsHook.message}; } + const details = detailsHook.response + const onAccept = async (): Promise => { - if (!details) { - throw Error("can't accept, no exchange selected"); - } try { - await setExchangeTosAccepted(details.exchangeDetails.exchangeBaseUrl, details.tosRequested?.tosEtag) + await setExchangeTosAccepted(details.exchangeInfo.baseUrl, details.tosRequested?.tosEtag) setAccepted(true) } catch (e) { - setError(true) + if (e instanceof Error) { + setErrorAccepting(e.message) + } } } const onWithdraw = async (): Promise => { - if (!details) { - throw Error("can't accept, no exchange selected"); - } setConfirmed(true) - console.log("accepting exchange", details.exchangeInfo.baseUrl); + console.log("accepting exchange", details.exchangeDetails.exchangeBaseUrl); try { - const res = await acceptWithdrawal(talerWithdrawUri, details.exchangeInfo.baseUrl); + const res = await acceptWithdrawal(uri, details.exchangeInfo.baseUrl); console.log("accept withdrawal response", res); if (res.confirmTransferUrl) { document.location.href = res.confirmTransferUrl; @@ -261,19 +257,6 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element } }; - if (cancelled) { - return Withdraw operation has been cancelled.; - } - if (error) { - return This URI is not valid anymore.; - } - if (!uriInfo) { - return Loading...; - } - if (!details) { - return Getting withdrawal details.; - } - let termsContent: TermsDocument | undefined = undefined; if (details.tosRequested) { if (details.tosRequested.tosContentType === 'text/xml') { @@ -295,14 +278,32 @@ export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element return } +export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element { + const uriInfoHook = useAsyncAsHook(() => !talerWithdrawUri ? Promise.reject(undefined) : + getWithdrawalDetailsForUri({ talerWithdrawUri }) + ) + + if (!talerWithdrawUri) { + return missing withdraw uri; + } + if (!uriInfoHook) { + return Loading...; + } + if (uriInfoHook.hasError) { + return This URI is not valid anymore: {uriInfoHook.message}; + } + return +} diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts new file mode 100644 index 000000000..2131d45cb --- /dev/null +++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts @@ -0,0 +1,48 @@ +/* + 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 { ExchangesListRespose } from "@gnu-taler/taler-util"; +import { useEffect, useState } from "preact/hooks"; +import * as wxApi from "../wxApi"; + +interface HookOk { + hasError: false; + response: T; +} + +interface HookError { + hasError: true; + message: string; +} + +export type HookResponse = HookOk | HookError | undefined; + +export function useAsyncAsHook (fn: (() => Promise)): HookResponse { + const [result, setHookResponse] = useState>(undefined); + useEffect(() => { + async function doAsync() { + try { + const response = await fn(); + setHookResponse({ hasError: false, response }); + } catch (e) { + if (e instanceof Error) { + setHookResponse({ hasError: true, message: e.message }); + } + } + } + doAsync() + }, []); + return result; +} diff --git a/packages/taler-wallet-webextension/src/hooks/useBalances.ts b/packages/taler-wallet-webextension/src/hooks/useBalances.ts new file mode 100644 index 000000000..37424fb05 --- /dev/null +++ b/packages/taler-wallet-webextension/src/hooks/useBalances.ts @@ -0,0 +1,54 @@ +/* + 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 { BalancesResponse } from "@gnu-taler/taler-util"; +import { useEffect, useState } from "preact/hooks"; +import * as wxApi from "../wxApi"; + + +interface BalancesHookOk { + hasError: false; + response: BalancesResponse; +} + +interface BalancesHookError { + hasError: true; + message: string; +} + +export type BalancesHook = BalancesHookOk | BalancesHookError | undefined; + +export function useBalances(): BalancesHook { + const [balance, setBalance] = useState(undefined); + useEffect(() => { + async function checkBalance() { + try { + const response = await wxApi.getBalance(); + console.log("got balance", balance); + setBalance({ hasError: false, response }); + } catch (e) { + console.error("could not retrieve balances", e); + if (e instanceof Error) { + setBalance({ hasError: true, message: e.message }); + } + } + } + checkBalance() + return wxApi.onUpdateNotification(checkBalance); + }, []); + + return balance; +} diff --git a/packages/taler-wallet-webextension/src/hooks/useBalances.tsx b/packages/taler-wallet-webextension/src/hooks/useBalances.tsx deleted file mode 100644 index 503b7a492..000000000 --- a/packages/taler-wallet-webextension/src/hooks/useBalances.tsx +++ /dev/null @@ -1,51 +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 { BalancesResponse } from "@gnu-taler/taler-util"; -import { useEffect, useState } from "preact/hooks"; -import * as wxApi from "../wxApi"; - - -interface BalancesHookOk { - error: false; - response: BalancesResponse; -} - -interface BalancesHookError { - error: true; -} - -export type BalancesHook = BalancesHookOk | BalancesHookError | undefined; - -export function useBalances(): BalancesHook { - const [balance, setBalance] = useState(undefined); - useEffect(() => { - async function checkBalance() { - try { - const response = await wxApi.getBalance(); - console.log("got balance", balance); - setBalance({ error: false, response }); - } catch (e) { - console.error("could not retrieve balances", e); - setBalance({ error: true }); - } - } - checkBalance() - return wxApi.onUpdateNotification(checkBalance); - }, []); - - return balance; -} diff --git a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx index a0655d379..382f9b549 100644 --- a/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/Balance.stories.tsx @@ -35,14 +35,15 @@ export const NotYetLoaded = createExample(TestedComponent, { export const GotError = createExample(TestedComponent, { balance: { - error: true + hasError: true, + message: 'Network error' }, Linker: NullLink, }); export const EmptyBalance = createExample(TestedComponent, { balance: { - error: false, + hasError: false, response: { balances: [] }, @@ -52,7 +53,7 @@ export const EmptyBalance = createExample(TestedComponent, { export const SomeCoins = createExample(TestedComponent, { balance: { - error: false, + hasError: false, response: { balances: [{ available: 'USD:10.5', @@ -68,7 +69,7 @@ export const SomeCoins = createExample(TestedComponent, { export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, { balance: { - error: false, + hasError: false, response: { balances: [{ available: 'USD:2.23', @@ -82,22 +83,135 @@ export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, { Linker: NullLink, }); +export const SomeCoinsAndOutgoingMoney = createExample(TestedComponent, { + balance: { + hasError: false, + response: { + balances: [{ + available: 'USD:2.23', + hasPendingTransactions: false, + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:5.11', + requiresUserInput: false + }] + }, + }, + Linker: NullLink, +}); + +export const SomeCoinsAndMovingMoney = createExample(TestedComponent, { + balance: { + hasError: false, + response: { + balances: [{ + available: 'USD:2.23', + hasPendingTransactions: false, + pendingIncoming: 'USD:2', + pendingOutgoing: 'USD:5.11', + requiresUserInput: false + }] + }, + }, + Linker: NullLink, +}); + export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, { balance: { - error: false, + hasError: false, response: { balances: [{ available: 'USD:2', hasPendingTransactions: false, - pendingIncoming: 'USD:5', + pendingIncoming: 'USD:5.1', pendingOutgoing: 'USD:0', requiresUserInput: false },{ available: 'EUR:4', hasPendingTransactions: false, - pendingIncoming: 'EUR:5', + pendingIncoming: 'EUR:0', + pendingOutgoing: 'EUR:3.01', + requiresUserInput: false + }] + }, + }, + Linker: NullLink, +}); + +export const SomeCoinsInTreeCurrencies = createExample(TestedComponent, { + balance: { + hasError: false, + response: { + balances: [{ + available: 'USD:1', + hasPendingTransactions: false, + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + requiresUserInput: false + },{ + available: 'COL:2000', + hasPendingTransactions: false, + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + requiresUserInput: false + },{ + available: 'EUR:4', + hasPendingTransactions: false, + pendingIncoming: 'EUR:15', + pendingOutgoing: 'EUR:0', + requiresUserInput: false + }] + }, + }, + Linker: NullLink, +}); + + +export const SomeCoinsInFiveCurrencies = createExample(TestedComponent, { + balance: { + hasError: false, + response: { + balances: [{ + available: 'USD:13451', + hasPendingTransactions: false, + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + requiresUserInput: false + },{ + available: 'EUR:202.02', + hasPendingTransactions: false, + pendingIncoming: 'EUR:0', + pendingOutgoing: 'EUR:0', + requiresUserInput: false + },{ + available: 'ARS:30', + hasPendingTransactions: false, + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + requiresUserInput: false + },{ + available: 'JPY:51223233', + hasPendingTransactions: false, + pendingIncoming: 'EUR:0', + pendingOutgoing: 'EUR:0', + requiresUserInput: false + },{ + available: 'JPY:51223233', + hasPendingTransactions: false, + pendingIncoming: 'EUR:0', pendingOutgoing: 'EUR:0', requiresUserInput: false + },{ + available: 'DEMOKUDOS:6', + hasPendingTransactions: false, + pendingIncoming: 'USD:0', + pendingOutgoing: 'USD:0', + requiresUserInput: false + },{ + available: 'TESTKUDOS:6', + hasPendingTransactions: false, + pendingIncoming: 'USD:5', + pendingOutgoing: 'USD:0', + requiresUserInput: false }] }, }, diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx index e3bada8d4..8e5c5c42e 100644 --- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx @@ -19,8 +19,9 @@ import { Balance, BalancesResponse, i18n } from "@gnu-taler/taler-util"; -import { JSX, h } from "preact"; -import { PopupBox, Centered, ButtonPrimary } from "../components/styled/index"; +import { JSX, h, Fragment } from "preact"; +import { ErrorMessage } from "../components/ErrorMessage"; +import { PopupBox, Centered, ButtonPrimary, ErrorBox, Middle } from "../components/styled/index"; import { BalancesHook, useBalances } from "../hooks/useBalances"; import { PageLink, renderAmount } from "../renderHtml"; @@ -34,34 +35,6 @@ export interface BalanceViewProps { Linker: typeof PageLink; goToWalletManualWithdraw: () => void; } -export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: 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; @@ -74,11 +47,20 @@ function formatPending(entry: Balance): JSX.Element { if (!Amounts.isZero(pendingIncoming)) { incoming = ( - + {"+"} {renderAmount(entry.pendingIncoming)} {" "} - incoming + + ); + } + if (!Amounts.isZero(pendingOutgoing)) { + payment = ( + + + {"-"} + {renderAmount(entry.pendingOutgoing)} + {" "} ); } @@ -89,36 +71,85 @@ function formatPending(entry: Balance): JSX.Element { } if (l.length === 1) { - return ({l}); + return {l}; } return ( - ({l[0]}, {l[1]}) + {l[0]}, {l[1]} ); } -function ShowBalances({ wallet, onWithdraw }: { wallet: BalancesResponse, onWithdraw: () => void }) { - return -
- {wallet.balances.map((entry) => { +export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) { + + function Content() { + if (!balance) { + return + } + + if (balance.hasError) { + return (
+ {balance.message} +

+ 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
+ {balance.response.balances.map((entry) => { const av = Amounts.parseOrThrow(entry.available); - const v = av.value + av.fraction / amountFractionalBase; - return ( -

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

+ // Create our number formatter. + let formatter; + try { + formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: av.currency, + currencyDisplay: 'symbol' + // These options are needed to round to whole numbers if that's what you want. + //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1) + //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) + }); + } catch { + formatter = new Intl.NumberFormat('en-US', { + // style: 'currency', + // currency: av.currency, + // These options are needed to round to whole numbers if that's what you want. + //minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1) + //maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501) + }); + } + + const v = formatter.format(av.value + av.fraction / amountFractionalBase); + const fontSize = v.length < 8 ? '3em' : (v.length < 13 ? '2em' : '1em') + return ( + + + + ); - })} + })}
{v}{av.currency}{formatPending(entry)}
+ } + + return + {/*
*/} + + {/*
*/}
- Withdraw + Withdraw
} diff --git a/packages/taler-wallet-webextension/src/popup/Debug.tsx b/packages/taler-wallet-webextension/src/popup/Debug.tsx index 3968b0191..ccc747466 100644 --- a/packages/taler-wallet-webextension/src/popup/Debug.tsx +++ b/packages/taler-wallet-webextension/src/popup/Debug.tsx @@ -28,7 +28,6 @@ export function DeveloperPage(props: any): JSX.Element {
-
); diff --git a/packages/taler-wallet-webextension/src/popup/History.stories.tsx b/packages/taler-wallet-webextension/src/popup/History.stories.tsx index ca9f545fe..daa263a81 100644 --- a/packages/taler-wallet-webextension/src/popup/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/popup/History.stories.tsx @@ -105,7 +105,7 @@ const exampleData = { } as TransactionRefund, } -export const Empty = createExample(TestedComponent, { +export const EmptyWithBalance = createExample(TestedComponent, { list: [], balances: [{ available: 'TESTKUDOS:10', @@ -116,6 +116,10 @@ export const Empty = createExample(TestedComponent, { }] }); +export const EmptyWithNoBalance = createExample(TestedComponent, { + list: [], + balances: [] +}); export const One = createExample(TestedComponent, { list: [exampleData.withdraw], diff --git a/packages/taler-wallet-webextension/src/popup/History.tsx b/packages/taler-wallet-webextension/src/popup/History.tsx index 77d603886..1447da9b0 100644 --- a/packages/taler-wallet-webextension/src/popup/History.tsx +++ b/packages/taler-wallet-webextension/src/popup/History.tsx @@ -14,7 +14,7 @@ TALER; see the file COPYING. If not, see */ -import { AmountString, Balance, Transaction, TransactionsResponse } from "@gnu-taler/taler-util"; +import { AmountString, Balance, i18n, Transaction, TransactionsResponse } from "@gnu-taler/taler-util"; import { h, JSX } from "preact"; import { useEffect, useState } from "preact/hooks"; import { PopupBox } from "../components/styled"; @@ -28,7 +28,7 @@ export function HistoryPage(props: any): JSX.Element { TransactionsResponse | undefined >(undefined); const balance = useBalances() - const balanceWithoutError = balance?.error ? [] : (balance?.response.balances || []) + const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || []) useEffect(() => { const fetchData = async (): Promise => { @@ -64,16 +64,24 @@ export function HistoryView({ list, balances }: { list: Transaction[], balances: Balance: {amountToString(balances[0].available)} } } -
- {list.slice(0, 3).map((tx, i) => ( - - ))} -
+ {list.length === 0 ?
+

+ You have no history yet, here you will be able to check your last transactions. +

+
: +
+ {list.slice(0, 3).map((tx, i) => ( + + ))} +
+ } } diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx b/packages/taler-wallet-webextension/src/popup/Settings.tsx index 52e72ee2f..8595c87ff 100644 --- a/packages/taler-wallet-webextension/src/popup/Settings.tsx +++ b/packages/taler-wallet-webextension/src/popup/Settings.tsx @@ -20,6 +20,7 @@ import { VNode, h } from "preact"; import { Checkbox } from "../components/Checkbox"; import { EditableText } from "../components/EditableText"; import { SelectList } from "../components/SelectList"; +import { PopupBox } from "../components/styled"; import { useDevContext } from "../context/devContext"; import { useBackupDeviceName } from "../hooks/useBackupDeviceName"; import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; @@ -67,10 +68,10 @@ const names: LangsNames = { export function SettingsView({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode { return ( -
-
-

Wallet

- +
+ {/*

Wallet

*/} + {/* + /> */}

Permissions

-
+ + ) } \ 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 index 1b145345f..cccda203e 100644 --- a/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Balance.stories.tsx @@ -35,14 +35,15 @@ export const NotYetLoaded = createExample(TestedComponent, { export const GotError = createExample(TestedComponent, { balance: { - error: true + hasError: true, + message: 'Network error' }, Linker: NullLink, }); export const EmptyBalance = createExample(TestedComponent, { balance: { - error: false, + hasError: false, response: { balances: [] }, @@ -52,7 +53,7 @@ export const EmptyBalance = createExample(TestedComponent, { export const SomeCoins = createExample(TestedComponent, { balance: { - error: false, + hasError: false, response: { balances: [{ available: 'USD:10.5', @@ -68,7 +69,7 @@ export const SomeCoins = createExample(TestedComponent, { export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, { balance: { - error: false, + hasError: false, response: { balances: [{ available: 'USD:2.23', @@ -84,7 +85,7 @@ export const SomeCoinsAndIncomingMoney = createExample(TestedComponent, { export const SomeCoinsInTwoCurrencies = createExample(TestedComponent, { balance: { - error: false, + hasError: false, response: { balances: [{ available: 'USD:2', diff --git a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx index e06e884ce..eb5a0447c 100644 --- a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx +++ b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx @@ -41,7 +41,7 @@ export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: Balan return } - if (balance.error) { + if (balance.hasError) { return (

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

diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx index 2bb59fcdb..43b0a6630 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.tsx @@ -29,7 +29,7 @@ export function HistoryPage(props: any): JSX.Element { TransactionsResponse | undefined >(undefined); const balance = useBalances() - const balanceWithoutError = balance?.error ? [] : (balance?.response.balances || []) + const balanceWithoutError = balance?.hasError ? [] : (balance?.response.balances || []) useEffect(() => { const fetchData = async (): Promise => { diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx b/packages/taler-wallet-webextension/src/wallet/Settings.tsx index 52e72ee2f..d1eb012fc 100644 --- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx @@ -15,23 +15,29 @@ */ -import { i18n } from "@gnu-taler/taler-util"; -import { VNode, h } from "preact"; +import { ExchangeListItem, i18n } from "@gnu-taler/taler-util"; +import { VNode, h, Fragment } from "preact"; import { Checkbox } from "../components/Checkbox"; import { EditableText } from "../components/EditableText"; import { SelectList } from "../components/SelectList"; +import { ButtonPrimary, ButtonSuccess, WalletBox } from "../components/styled"; import { useDevContext } from "../context/devContext"; import { useBackupDeviceName } from "../hooks/useBackupDeviceName"; import { useExtendedPermissions } from "../hooks/useExtendedPermissions"; +import { useAsyncAsHook } from "../hooks/useAsyncAsHook"; import { useLang } from "../hooks/useLang"; +import * as wxApi from "../wxApi"; export function SettingsPage(): VNode { const [permissionsEnabled, togglePermissions] = useExtendedPermissions(); const { devMode, toggleDevMode } = useDevContext() const { name, update } = useBackupDeviceName() const [lang, changeLang] = useLang() + const exchangesHook = useAsyncAsHook(() => wxApi.listExchanges()); + return void; developerMode: boolean; toggleDeveloperMode: () => void; + knownExchanges: Array; } import { strings as messages } from '../i18n/strings' @@ -65,26 +72,24 @@ const names: LangsNames = { } -export function SettingsView({ lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode { +export function SettingsView({ knownExchanges, lang, changeLang, deviceName, setDeviceName, permissionsEnabled, togglePermissions, developerMode, toggleDeveloperMode }: ViewProps): VNode { return ( -
-
-

Wallet

- - + +
+ +

Known exchanges

+ {!knownExchanges || !knownExchanges.length ?
+ No exchange yet! +
: +
+ {knownExchanges.map(e => +
{e.currency}
+
{e.exchangeBaseUrl}
+
{e.paytoUris}
+
)} +
+ } + add exchange

Permissions

-
+ ) } \ No newline at end of file diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index 8a0881a6c..664cc564b 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -43,6 +43,7 @@ import { AcceptManualWithdrawalResult, AcceptManualWithdrawalRequest, AmountJson, + ExchangesListRespose, } from "@gnu-taler/taler-util"; import { AddBackupProviderRequest, BackupProviderState, OperationFailedError, RemoveBackupProviderRequest } from "@gnu-taler/taler-wallet-core"; import { BackupInfo } from "@gnu-taler/taler-wallet-core"; @@ -170,6 +171,10 @@ export function listKnownCurrencies(): Promise { }); } +export function listExchanges(): Promise { + return callBackend("listExchanges", {}) +} + /** * Get information about the current state of wallet backups. */ -- cgit v1.2.3