diff options
Diffstat (limited to 'packages')
18 files changed, 604 insertions, 218 deletions
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) }}> - <option selected> + {value !== undefined ? <option selected> {list[value]} - </option> + </option> : <option selected disabled> + Select one option + </option>} {Object.keys(list) .filter((l) => l !== value) .map(key => <option value={key} key={key}>{list[key]}</option>) 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<string | undefined>(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 <PaymentRequestView uri={talerPayUri} payStatus={payStatus} onClick={onClick} payErrMsg={payErrMsg} balance={foundAmount} />; + return <PaymentRequestView uri={talerPayUri} + payStatus={payStatus} payResult={payResult} + onClick={onClick} payErrMsg={payErrMsg} + balance={foundAmount} />; } 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 <section> + <div> + <p>Processing...</p> + </div> + </section> + } + return null + } if (payErrMsg) { return <section> <div> @@ -208,7 +222,7 @@ export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg, balance if (payStatus.status === PreparePayResultType.PaymentPossible) { return <Fragment> <section> - <ButtonSuccess upperCased> + <ButtonSuccess upperCased onClick={onClick}> {i18n.str`Pay`} {amountToString(payStatus.amountEffective)} </ButtonSuccess> </section> @@ -252,6 +266,15 @@ export function PaymentRequestView({ uri, payStatus, onClick, payErrMsg, balance {payStatus.status === PreparePayResultType.AlreadyConfirmed && (payStatus.paid ? <SuccessBox> Already paid </SuccessBox> : <WarningBox> Already claimed </WarningBox>) } + {payResult && payResult.type === ConfirmPayResultType.Done && ( + <SuccessBox> + <h3>Payment complete</h3> + <p>{!payResult.contractTerms.fulfillment_message ? + "You will now be sent back to the merchant you came from." : + payResult.contractTerms.fulfillment_message + }</p> + </SuccessBox> + )} <section> {payStatus.status !== PreparePayResultType.InsufficientBalance && Amounts.isNonZero(totalFees) && <Part big title="Total to pay" text={amountToString(payStatus.amountEffective)} kind='negative' /> 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 = `<?xml version="1.0" encoding="utf-8"?> `; 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<void>; 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<string | undefined>(undefined) + const exchanges = knownExchanges.reduce((prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }), {}) + return ( <WalletAction> <LogoHeader /> @@ -84,7 +92,7 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview, {i18n.str`Digital cash withdrawal`} </h2> <section> - <Part title="Total to withdraw" text={amountToString(Amounts.sub(Amounts.parseOrThrow(amount), details.withdrawFee).amount)} kind='positive' /> + <Part title="Total to withdraw" text={amountToString(Amounts.sub(amount, details.withdrawFee).amount)} kind='positive' /> <Part title="Chosen amount" text={amountToString(amount)} kind='neutral' /> {Amounts.isNonZero(details.withdrawFee) && <Part title="Exchange fee" text={amountToString(details.withdrawFee)} kind='negative' /> @@ -93,11 +101,21 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview, </section> {!reviewing && <section> - <LinkSuccess - upperCased - > - {i18n.str`Edit exchange`} - </LinkSuccess> + {switchingExchange !== undefined ? <Fragment> + <div> + <SelectList label="Known exchanges" list={exchanges} name="" onChange={onSwitchExchange} /> + </div> + <p> + This is the list of known exchanges + </p> + <LinkSuccess upperCased onClick={() => onSwitchExchange(switchingExchange)}> + {i18n.str`Confirm exchange selection`} + </LinkSuccess> + </Fragment> + : <LinkSuccess upperCased onClick={() => setSwitchingExchange("")}> + {i18n.str`Switch exchange`} + </LinkSuccess>} + </section> } {!reviewing && accepted && @@ -140,6 +158,9 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview, </section> } + {/** + * Main action section + */} <section> {terms.status === 'new' && !accepted && !reviewing && <ButtonSuccess @@ -178,80 +199,55 @@ export function View({ details, amount, onWithdraw, terms, reviewing, onReview, ) } -export function WithdrawPage({ talerWithdrawUri, ...rest }: Props): JSX.Element { - const [uriInfo, setUriInfo] = useState<WithdrawUriInfoResponse | undefined>(undefined); - const [details, setDetails] = useState<ExchangeWithdrawDetails | undefined>(undefined); - const [cancelled, setCancelled] = useState(false); - const [selecting, setSelecting] = useState(false); - const [error, setError] = useState<boolean>(false); - const [updateCounter, setUpdateCounter] = useState(1); +export function WithdrawPageWithParsedURI({ uri, uriInfo }: { uri: string, uriInfo: WithdrawUriInfoResponse }) { + const [customExchange, setCustomExchange] = useState<string | undefined>(undefined) + const [errorAccepting, setErrorAccepting] = useState<string | undefined>(undefined) + const [reviewing, setReviewing] = useState<boolean>(false) const [accepted, setAccepted] = useState<boolean>(false) const [confirmed, setConfirmed] = useState<boolean>(false) - useEffect(() => { - return onUpdateNotification(() => { - console.log('updating...') - setUpdateCounter(updateCounter + 1); - }); - }, []); - - useEffect(() => { - console.log('on effect yes', talerWithdrawUri) - if (!talerWithdrawUri) return - const fetchData = async (): Promise<void> => { - 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 <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>; + 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 <span><i18n.Translate>Getting withdrawal details.</i18n.Translate></span>; + } + if (detailsHook.hasError) { + return <span><i18n.Translate>Problems getting details: {detailsHook.message}</i18n.Translate></span>; } + const details = detailsHook.response + const onAccept = async (): Promise<void> => { - 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<void> => { - 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 <span><i18n.Translate>Withdraw operation has been cancelled.</i18n.Translate></span>; - } - if (error) { - return <span><i18n.Translate>This URI is not valid anymore.</i18n.Translate></span>; - } - if (!uriInfo) { - return <span><i18n.Translate>Loading...</i18n.Translate></span>; - } - if (!details) { - return <span><i18n.Translate>Getting withdrawal details.</i18n.Translate></span>; - } - 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 <View onWithdraw={onWithdraw} // setCancelled={setCancelled} setSelecting={setSelecting} - details={details} amount={uriInfo.amount} + details={details} amount={withdrawAmount} terms={{ status, value: termsContent }} + onSwitchExchange={setCustomExchange} + knownExchanges={knownExchanges} confirmed={confirmed} accepted={accepted} onAccept={onAccept} reviewing={reviewing} onReview={setReviewing} // terms={[]} /> } +export function WithdrawPage({ talerWithdrawUri }: Props): JSX.Element { + const uriInfoHook = useAsyncAsHook(() => !talerWithdrawUri ? Promise.reject(undefined) : + getWithdrawalDetailsForUri({ talerWithdrawUri }) + ) + + if (!talerWithdrawUri) { + return <span><i18n.Translate>missing withdraw uri</i18n.Translate></span>; + } + if (!uriInfoHook) { + return <span><i18n.Translate>Loading...</i18n.Translate></span>; + } + if (uriInfoHook.hasError) { + return <span><i18n.Translate>This URI is not valid anymore: {uriInfoHook.message}</i18n.Translate></span>; + } + return <WithdrawPageWithParsedURI uri={talerWithdrawUri} uriInfo={uriInfoHook.response} /> +} 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 <http://www.gnu.org/licenses/> + */ +import { ExchangesListRespose } from "@gnu-taler/taler-util"; +import { useEffect, useState } from "preact/hooks"; +import * as wxApi from "../wxApi"; + +interface HookOk<T> { + hasError: false; + response: T; +} + +interface HookError { + hasError: true; + message: string; +} + +export type HookResponse<T> = HookOk<T> | HookError | undefined; + +export function useAsyncAsHook<T> (fn: (() => Promise<T>)): HookResponse<T> { + const [result, setHookResponse] = useState<HookResponse<T>>(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.tsx b/packages/taler-wallet-webextension/src/hooks/useBalances.ts index 503b7a492..37424fb05 100644 --- a/packages/taler-wallet-webextension/src/hooks/useBalances.tsx +++ b/packages/taler-wallet-webextension/src/hooks/useBalances.ts @@ -20,12 +20,13 @@ import * as wxApi from "../wxApi"; interface BalancesHookOk { - error: false; + hasError: false; response: BalancesResponse; } interface BalancesHookError { - error: true; + hasError: true; + message: string; } export type BalancesHook = BalancesHookOk | BalancesHookError | undefined; @@ -37,10 +38,12 @@ export function useBalances(): BalancesHook { try { const response = await wxApi.getBalance(); console.log("got balance", balance); - setBalance({ error: false, response }); + setBalance({ hasError: false, response }); } catch (e) { console.error("could not retrieve balances", e); - setBalance({ error: true }); + if (e instanceof Error) { + setBalance({ hasError: true, message: e.message }); + } } } checkBalance() 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 <span /> - } - - if (balance.error) { - return ( - <div> - <p>{i18n.str`Error: could not retrieve balance information.`}</p> - <p> - Click <Linker pageName="welcome">here</Linker> for help and - diagnostics. - </p> - </div> - ) - } - if (balance.response.balances.length === 0) { - return ( - <p><i18n.Translate> - You have no balance to show. Need some{" "} - <Linker pageName="/welcome">help</Linker> getting started? - </i18n.Translate></p> - ) - } - return <ShowBalances wallet={balance.response} - onWithdraw={goToWalletManualWithdraw} - /> -} 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 = ( <span><i18n.Translate> - <span style={{ color: "darkgreen" }}> + <span style={{ color: "darkgreen" }} title="incoming amount"> {"+"} {renderAmount(entry.pendingIncoming)} </span>{" "} - incoming + </i18n.Translate></span> + ); + } + if (!Amounts.isZero(pendingOutgoing)) { + payment = ( + <span><i18n.Translate> + <span style={{ color: "darkred" }} title="outgoing amount"> + {"-"} + {renderAmount(entry.pendingOutgoing)} + </span>{" "} </i18n.Translate></span> ); } @@ -89,36 +71,85 @@ function formatPending(entry: Balance): JSX.Element { } if (l.length === 1) { - return <span>({l})</span>; + return <span>{l}</span>; } return ( <span> - ({l[0]}, {l[1]}) + {l[0]}, {l[1]} </span> ); } -function ShowBalances({ wallet, onWithdraw }: { wallet: BalancesResponse, onWithdraw: () => void }) { - return <PopupBox> - <section> - <Centered>{wallet.balances.map((entry) => { +export function BalanceView({ balance, Linker, goToWalletManualWithdraw }: BalanceViewProps) { + + function Content() { + if (!balance) { + return <span /> + } + + if (balance.hasError) { + return (<section> + <ErrorBox>{balance.message}</ErrorBox> + <p> + Click <Linker pageName="welcome">here</Linker> for help and + diagnostics. + </p> + </section>) + } + if (balance.response.balances.length === 0) { + return (<section data-expanded> + <Middle> + <p><i18n.Translate> + You have no balance to show. Need some{" "} + <Linker pageName="/welcome">help</Linker> getting started? + </i18n.Translate></p> + </Middle> + </section>) + } + return <section data-expanded data-centered> + <table style={{width:'100%'}}>{balance.response.balances.map((entry) => { const av = Amounts.parseOrThrow(entry.available); - const v = av.value + av.fraction / amountFractionalBase; - return ( - <p key={av.currency}> - <span> - <span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "} - <span>{av.currency}</span> - </span> - {formatPending(entry)} - </p> + // 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 (<tr> + <td style={{ height: 50, fontSize, width: '60%', textAlign: 'right', padding: 0 }}>{v}</td> + <td style={{ maxWidth: '2em', overflowX: 'hidden' }}>{av.currency}</td> + <td style={{ fontSize: 'small', color: 'gray' }}>{formatPending(entry)}</td> + </tr> ); - })}</Centered> + })}</table> </section> + } + + return <PopupBox> + {/* <section> */} + <Content /> + {/* </section> */} <footer> <div /> - <ButtonPrimary onClick={onWithdraw} >Withdraw</ButtonPrimary> + <ButtonPrimary onClick={goToWalletManualWithdraw}>Withdraw</ButtonPrimary> </footer> </PopupBox> } 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 { <button onClick={openExtensionPage("/static/popup.html")}>wallet tab</button> <br /> <button onClick={confirmReset}>reset</button> - <button onClick={reload}>reload chrome extension</button> <Diagnostics diagnostics={status} timedOut={timedOut} /> </div> ); 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 <http://www.gnu.org/licenses/> */ -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<void> => { @@ -64,16 +64,24 @@ export function HistoryView({ list, balances }: { list: Transaction[], balances: Balance: <span>{amountToString(balances[0].available)}</span> </div>} </header>} - <section> - {list.slice(0, 3).map((tx, i) => ( - <TransactionItem key={i} tx={tx} multiCurrency={multiCurrency}/> - ))} - </section> + {list.length === 0 ? <section data-expanded data-centered> + <p><i18n.Translate> + You have no history yet, here you will be able to check your last transactions. + </i18n.Translate></p> + </section> : + <section> + {list.slice(0, 3).map((tx, i) => ( + <TransactionItem key={i} tx={tx} multiCurrency={multiCurrency} /> + ))} + </section> + } <footer style={{ justifyContent: 'space-around' }}> - <a target="_blank" - rel="noopener noreferrer" - style={{ color: 'darkgreen', textDecoration: 'none' }} - href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/history`) : '#'}>VIEW MORE TRANSACTIONS</a> + {list.length > 0 && + <a target="_blank" + rel="noopener noreferrer" + style={{ color: 'darkgreen', textDecoration: 'none' }} + href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/history`) : '#'}>VIEW MORE TRANSACTIONS</a> + } </footer> </PopupBox> } 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 ( - <div> - <section style={{ height: 300, overflow: 'auto' }}> - <h2><i18n.Translate>Wallet</i18n.Translate></h2> - <SelectList + <PopupBox> + <section> + {/* <h2><i18n.Translate>Wallet</i18n.Translate></h2> */} + {/* <SelectList value={lang} onChange={changeLang} name="lang" @@ -84,7 +85,7 @@ export function SettingsView({ lang, changeLang, deviceName, setDeviceName, perm name="device-id" label={i18n.str`Device name`} description="(This is how you will recognize the wallet in the backup provider)" - /> + /> */} <h2><i18n.Translate>Permissions</i18n.Translate></h2> <Checkbox label="Automatically open wallet based on page content" name="perm" @@ -98,6 +99,12 @@ export function SettingsView({ lang, changeLang, deviceName, setDeviceName, perm enabled={developerMode} onToggle={toggleDeveloperMode} /> </section> - </div> + <footer style={{ justifyContent: 'space-around' }}> + <a target="_blank" + rel="noopener noreferrer" + style={{ color: 'darkgreen', textDecoration: 'none' }} + href={chrome.extension ? chrome.extension.getURL(`/static/wallet.html#/settings`) : '#'}>VIEW MORE SETTINGS</a> + </footer> + </PopupBox> ) }
\ 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 <span /> } - if (balance.error) { + if (balance.hasError) { return ( <div> <p>{i18n.str`Error: could not retrieve balance information.`}</p> 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<void> => { 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 <SettingsView lang={lang} changeLang={changeLang} + knownExchanges={!exchangesHook || exchangesHook.hasError ? [] : exchangesHook.response.exchanges} deviceName={name} setDeviceName={update} permissionsEnabled={permissionsEnabled} togglePermissions={togglePermissions} developerMode={devMode} toggleDeveloperMode={toggleDevMode} @@ -47,6 +53,7 @@ export interface ViewProps { togglePermissions: () => void; developerMode: boolean; toggleDeveloperMode: () => void; + knownExchanges: Array<ExchangeListItem>; } 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 ( - <div> - <section style={{ height: 300, overflow: 'auto' }}> - <h2><i18n.Translate>Wallet</i18n.Translate></h2> - <SelectList - value={lang} - onChange={changeLang} - name="lang" - list={names} - label={i18n.str`Language`} - description="(Choose your preferred lang)" - /> - <EditableText - value={deviceName} - onChange={setDeviceName} - name="device-id" - label={i18n.str`Device name`} - description="(This is how you will recognize the wallet in the backup provider)" - /> + <WalletBox> + <section> + + <h2><i18n.Translate>Known exchanges</i18n.Translate></h2> + {!knownExchanges || !knownExchanges.length ? <div> + No exchange yet! + </div> : + <dl> + {knownExchanges.map(e => <Fragment> + <dt>{e.currency}</dt> + <dd>{e.exchangeBaseUrl}</dd> + <dd>{e.paytoUris}</dd> + </Fragment>)} + </dl> + } + <ButtonPrimary>add exchange</ButtonPrimary> <h2><i18n.Translate>Permissions</i18n.Translate></h2> <Checkbox label="Automatically open wallet based on page content" name="perm" @@ -98,6 +103,6 @@ export function SettingsView({ lang, changeLang, deviceName, setDeviceName, perm enabled={developerMode} onToggle={toggleDeveloperMode} /> </section> - </div> + </WalletBox> ) }
\ 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<ListOfKnownCurrencies> { }); } +export function listExchanges(): Promise<ExchangesListRespose> { + return callBackend("listExchanges", {}) +} + /** * Get information about the current state of wallet backups. */ |