diff options
19 files changed, 1075 insertions, 225 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. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b14baed12..b8f1fd547 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,43 @@ importers: ava: 3.15.0 typescript: 4.4.3 + packages/anastasis-webui: + specifiers: + '@types/enzyme': ^3.10.5 + '@types/jest': ^26.0.8 + '@typescript-eslint/eslint-plugin': ^2.25.0 + '@typescript-eslint/parser': ^2.25.0 + enzyme: ^3.11.0 + enzyme-adapter-preact-pure: ^3.1.0 + eslint: ^6.8.0 + eslint-config-preact: ^1.1.1 + jest: ^26.2.2 + jest-preset-preact: ^4.0.2 + preact: ^10.3.1 + preact-cli: ^3.0.0 + preact-render-to-string: ^5.1.4 + preact-router: ^3.2.1 + sirv-cli: ^1.0.0-next.3 + typescript: ^3.7.5 + dependencies: + preact: 10.5.14 + preact-render-to-string: 5.1.19_preact@10.5.14 + preact-router: 3.2.1_preact@10.5.14 + devDependencies: + '@types/enzyme': 3.10.9 + '@types/jest': 26.0.24 + '@typescript-eslint/eslint-plugin': 2.34.0_2b015b1c4b7c4a3ed9a197dc233b1a35 + '@typescript-eslint/parser': 2.34.0_eslint@6.8.0+typescript@3.9.10 + enzyme: 3.11.0 + enzyme-adapter-preact-pure: 3.1.0_enzyme@3.11.0+preact@10.5.14 + eslint: 6.8.0 + eslint-config-preact: 1.1.4_eslint@6.8.0+typescript@3.9.10 + jest: 26.6.3 + jest-preset-preact: 4.0.2_9b3f24ae35a87c3c82fffbe3fdf70e1e + preact-cli: 3.2.2_517d24bd855b57d7e424aceed04e063b + sirv-cli: 1.0.14 + typescript: 3.9.10 + packages/idb-bridge: specifiers: '@rollup/plugin-commonjs': ^17.1.0 @@ -4094,6 +4131,10 @@ packages: - supports-color dev: true + /@mdn/browser-compat-data/3.3.14: + resolution: {integrity: sha512-n2RC9d6XatVbWFdHLimzzUJxJ1KY8LdjqrW6YvGPiRmsHkhOUx74/Ct10x5Yo7bC/Jvqx7cDEW8IMPv/+vwEzA==} + dev: true + /@mdx-js/loader/1.6.22: resolution: {integrity: sha512-9CjGwy595NaxAYp0hF9B/A0lH6C8Rms97e2JS9d3jVUtILn6pT5i5IV965ra3lIWc7Rs1GG1tBdVF7dCowYe6Q==} dependencies: @@ -6342,6 +6383,10 @@ packages: '@types/react': 17.0.19 dev: true + /@types/eslint-visitor-keys/1.0.0: + resolution: {integrity: sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==} + dev: true + /@types/estree/0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} dev: true @@ -6617,6 +6662,28 @@ packages: '@types/yargs-parser': 20.2.1 dev: true + /@typescript-eslint/eslint-plugin/2.34.0_2b015b1c4b7c4a3ed9a197dc233b1a35: + resolution: {integrity: sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + peerDependencies: + '@typescript-eslint/parser': ^2.0.0 + eslint: ^5.0.0 || ^6.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/experimental-utils': 2.34.0_eslint@6.8.0+typescript@3.9.10 + '@typescript-eslint/parser': 2.34.0_eslint@6.8.0+typescript@3.9.10 + eslint: 6.8.0 + functional-red-black-tree: 1.0.1 + regexpp: 3.1.0 + tsutils: 3.19.1_typescript@3.9.10 + typescript: 3.9.10 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/eslint-plugin/4.14.0_980e7d90d2d08155204a38366bd3b934: resolution: {integrity: sha512-IJ5e2W7uFNfg4qh9eHkHRUCbgZ8VKtGwD07kannJvM5t/GU8P8+24NX8gi3Hf5jST5oWPY8kyV1s/WtfiZ4+Ww==} engines: {node: ^10.12.0 || >=12.0.0} @@ -6643,6 +6710,22 @@ packages: - supports-color dev: true + /@typescript-eslint/experimental-utils/2.34.0_eslint@6.8.0+typescript@3.9.10: + resolution: {integrity: sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + peerDependencies: + eslint: '*' + dependencies: + '@types/json-schema': 7.0.9 + '@typescript-eslint/typescript-estree': 2.34.0_typescript@3.9.10 + eslint: 6.8.0 + eslint-scope: 5.1.1 + eslint-utils: 2.1.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/experimental-utils/4.14.0_eslint@7.18.0+typescript@4.1.3: resolution: {integrity: sha512-6i6eAoiPlXMKRbXzvoQD5Yn9L7k9ezzGRvzC/x1V3650rUk3c3AOjQyGYyF9BDxQQDK2ElmKOZRD0CbtdkMzQQ==} engines: {node: ^10.12.0 || >=12.0.0} @@ -6661,6 +6744,26 @@ packages: - typescript dev: true + /@typescript-eslint/parser/2.34.0_eslint@6.8.0+typescript@3.9.10: + resolution: {integrity: sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + peerDependencies: + eslint: ^5.0.0 || ^6.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@types/eslint-visitor-keys': 1.0.0 + '@typescript-eslint/experimental-utils': 2.34.0_eslint@6.8.0+typescript@3.9.10 + '@typescript-eslint/typescript-estree': 2.34.0_typescript@3.9.10 + eslint: 6.8.0 + eslint-visitor-keys: 1.3.0 + typescript: 3.9.10 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser/4.14.0_eslint@7.18.0+typescript@4.1.3: resolution: {integrity: sha512-sUDeuCjBU+ZF3Lzw0hphTyScmDDJ5QVkyE21pRoBo8iDl7WBtVFS+WDN3blY1CH3SBt7EmYCw6wfmJjF0l/uYg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -6727,6 +6830,27 @@ packages: engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} dev: true + /@typescript-eslint/typescript-estree/2.34.0_typescript@3.9.10: + resolution: {integrity: sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + debug: 4.3.2 + eslint-visitor-keys: 1.3.0 + glob: 7.1.7 + is-glob: 4.0.1 + lodash: 4.17.21 + semver: 7.3.5 + tsutils: 3.19.1_typescript@3.9.10 + typescript: 3.9.10 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree/4.14.0_typescript@4.1.3: resolution: {integrity: sha512-wRjZ5qLao+bvS2F7pX4qi2oLcOONIB+ru8RGBieDptq/SudYwshveORwCVU4/yMAd4GK7Fsf8Uq1tjV838erag==} engines: {node: ^10.12.0 || >=12.0.0} @@ -7393,10 +7517,21 @@ packages: engines: {node: '>=0.10.0'} dev: true + /ast-metadata-inferer/0.7.0: + resolution: {integrity: sha512-OkMLzd8xelb3gmnp6ToFvvsHLtS6CbagTkFQvQ+ZYFe3/AIl9iKikNR9G7pY3GfOR/2Xc222hwBjzI7HLkE76Q==} + dependencies: + '@mdn/browser-compat-data': 3.3.14 + dev: true + /ast-types-flow/0.0.7: resolution: {integrity: sha1-9wtzXGvKGlycItmCw+Oef+ujva0=} dev: true + /astral-regex/1.0.0: + resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} + engines: {node: '>=4'} + dev: true + /astral-regex/2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -7546,7 +7681,7 @@ packages: /axios/0.21.1: resolution: {integrity: sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==} dependencies: - follow-redirects: 1.14.2_debug@4.3.2 + follow-redirects: 1.14.2 transitivePeerDependencies: - debug @@ -7554,6 +7689,24 @@ packages: resolution: {integrity: sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==} dev: true + /babel-eslint/10.1.0_eslint@6.8.0: + resolution: {integrity: sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==} + engines: {node: '>=6'} + deprecated: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates. + peerDependencies: + eslint: '>= 4.12.1' + dependencies: + '@babel/code-frame': 7.14.5 + '@babel/parser': 7.15.3 + '@babel/traverse': 7.15.0 + '@babel/types': 7.15.0 + eslint: 6.8.0 + eslint-visitor-keys: 1.3.0 + resolve: 1.20.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-esm-plugin/0.9.0_webpack@4.46.0: resolution: {integrity: sha512-OyPyLI6LUuUqNm3HNUldAkynWrLzXkhcZo4fGTsieCgHqvbCoCIMMOwJmfG9Lmp91S7WDIuUr0mvOeI8pAb/pw==} peerDependencies: @@ -8464,6 +8617,10 @@ packages: resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} dev: true + /chardet/0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + dev: true + /cheerio-select/1.5.0: resolution: {integrity: sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==} dependencies: @@ -8632,6 +8789,11 @@ packages: string-width: 4.2.2 dev: true + /cli-width/3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + dev: true + /cliui/5.0.0: resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==} dependencies: @@ -10482,6 +10644,22 @@ packages: object.entries: 1.1.3 dev: true + /eslint-config-preact/1.1.4_eslint@6.8.0+typescript@3.9.10: + resolution: {integrity: sha512-j00/BpjPpVoaX8UTpXFPAsfBIzuwJX+sBvgPFyb53Lqi31fM0Oiq516qYXRyaZ7q1BRCjO8s67NCLal6v/Z8Lg==} + peerDependencies: + eslint: 6.x || 7.x + dependencies: + babel-eslint: 10.1.0_eslint@6.8.0 + eslint: 6.8.0 + eslint-plugin-compat: 3.13.0_eslint@6.8.0 + eslint-plugin-jest: 23.20.0_eslint@6.8.0+typescript@3.9.10 + eslint-plugin-react: 7.22.0_eslint@6.8.0 + eslint-plugin-react-hooks: 4.2.0_eslint@6.8.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /eslint-import-resolver-node/0.3.4: resolution: {integrity: sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==} dependencies: @@ -10497,6 +10675,23 @@ packages: pkg-dir: 2.0.0 dev: true + /eslint-plugin-compat/3.13.0_eslint@6.8.0: + resolution: {integrity: sha512-cv8IYMuTXm7PIjMVDN2y4k/KVnKZmoNGHNq27/9dLstOLydKblieIv+oe2BN2WthuXnFNhaNvv3N1Bvl4dbIGA==} + engines: {node: '>=9.x'} + peerDependencies: + eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + dependencies: + '@mdn/browser-compat-data': 3.3.14 + ast-metadata-inferer: 0.7.0 + browserslist: 4.16.8 + caniuse-lite: 1.0.30001251 + core-js: 3.16.2 + eslint: 6.8.0 + find-up: 5.0.0 + lodash.memoize: 4.1.2 + semver: 7.3.5 + dev: true + /eslint-plugin-import/2.22.1_eslint@7.18.0: resolution: {integrity: sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==} engines: {node: '>=4'} @@ -10519,6 +10714,19 @@ packages: tsconfig-paths: 3.9.0 dev: true + /eslint-plugin-jest/23.20.0_eslint@6.8.0+typescript@3.9.10: + resolution: {integrity: sha512-+6BGQt85OREevBDWCvhqj1yYA4+BFK4XnRZSGJionuEYmcglMZYLNNBBemwzbqUAckURaHdJSBcjHPyrtypZOw==} + engines: {node: '>=8'} + peerDependencies: + eslint: '>=5' + dependencies: + '@typescript-eslint/experimental-utils': 2.34.0_eslint@6.8.0+typescript@3.9.10 + eslint: 6.8.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /eslint-plugin-jsx-a11y/6.4.1_eslint@7.18.0: resolution: {integrity: sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==} engines: {node: '>=4.0'} @@ -10539,6 +10747,15 @@ packages: language-tags: 1.0.5 dev: true + /eslint-plugin-react-hooks/4.2.0_eslint@6.8.0: + resolution: {integrity: sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + dependencies: + eslint: 6.8.0 + dev: true + /eslint-plugin-react-hooks/4.2.0_eslint@7.18.0: resolution: {integrity: sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==} engines: {node: '>=10'} @@ -10548,6 +10765,26 @@ packages: eslint: 7.18.0 dev: true + /eslint-plugin-react/7.22.0_eslint@6.8.0: + resolution: {integrity: sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 + dependencies: + array-includes: 3.1.2 + array.prototype.flatmap: 1.2.4 + doctrine: 2.1.0 + eslint: 6.8.0 + has: 1.0.3 + jsx-ast-utils: 3.2.0 + object.entries: 1.1.3 + object.fromentries: 2.0.3 + object.values: 1.1.2 + prop-types: 15.7.2 + resolve: 1.19.0 + string.prototype.matchall: 4.0.3 + dev: true + /eslint-plugin-react/7.22.0_eslint@7.18.0: resolution: {integrity: sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA==} engines: {node: '>=4'} @@ -10584,6 +10821,13 @@ packages: estraverse: 4.3.0 dev: true + /eslint-utils/1.4.3: + resolution: {integrity: sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + /eslint-utils/2.1.0: resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} engines: {node: '>=6'} @@ -10601,6 +10845,52 @@ packages: engines: {node: '>=10'} dev: true + /eslint/6.8.0: + resolution: {integrity: sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + hasBin: true + dependencies: + '@babel/code-frame': 7.14.5 + ajv: 6.12.6 + chalk: 2.4.2 + cross-spawn: 6.0.5 + debug: 4.3.2 + doctrine: 3.0.0 + eslint-scope: 5.1.1 + eslint-utils: 1.4.3 + eslint-visitor-keys: 1.3.0 + espree: 6.2.1 + esquery: 1.3.1 + esutils: 2.0.3 + file-entry-cache: 5.0.1 + functional-red-black-tree: 1.0.1 + glob-parent: 5.1.2 + globals: 12.4.0 + ignore: 4.0.6 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + inquirer: 7.3.3 + is-glob: 4.0.1 + js-yaml: 3.14.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.3.0 + lodash: 4.17.21 + minimatch: 3.0.4 + mkdirp: 0.5.5 + natural-compare: 1.4.0 + optionator: 0.8.3 + progress: 2.0.3 + regexpp: 2.0.1 + semver: 6.3.0 + strip-ansi: 5.2.0 + strip-json-comments: 3.1.1 + table: 5.4.6 + text-table: 0.2.0 + v8-compile-cache: 2.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /eslint/7.18.0: resolution: {integrity: sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==} engines: {node: ^10.12.0 || >=12.0.0} @@ -10652,6 +10942,15 @@ packages: engines: {node: '>=6'} dev: true + /espree/6.2.1: + resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==} + engines: {node: '>=6.0.0'} + dependencies: + acorn: 7.4.1 + acorn-jsx: 5.3.2_acorn@7.4.1 + eslint-visitor-keys: 1.3.0 + dev: true + /espree/7.3.1: resolution: {integrity: sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==} engines: {node: ^10.12.0 || >=12.0.0} @@ -10849,6 +11148,15 @@ packages: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: true + /external-editor/3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + dev: true + /extglob/2.0.4: resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} engines: {node: '>=0.10.0'} @@ -10972,6 +11280,13 @@ packages: escape-string-regexp: 1.0.5 dev: true + /file-entry-cache/5.0.1: + resolution: {integrity: sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==} + engines: {node: '>=4'} + dependencies: + flat-cache: 2.0.1 + dev: true + /file-entry-cache/6.0.0: resolution: {integrity: sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==} engines: {node: ^10.12.0 || >=12.0.0} @@ -11097,6 +11412,15 @@ packages: micromatch: 3.1.10 dev: true + /flat-cache/2.0.1: + resolution: {integrity: sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==} + engines: {node: '>=4'} + dependencies: + flatted: 2.0.2 + rimraf: 2.6.3 + write: 1.0.3 + dev: true + /flat-cache/3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -11105,6 +11429,10 @@ packages: rimraf: 3.0.2 dev: true + /flatted/2.0.2: + resolution: {integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==} + dev: true + /flatted/3.1.1: resolution: {integrity: sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==} dev: true @@ -11116,7 +11444,7 @@ packages: readable-stream: 2.3.7 dev: true - /follow-redirects/1.14.2_debug@4.3.2: + /follow-redirects/1.14.2: resolution: {integrity: sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==} engines: {node: '>=4.0'} peerDependencies: @@ -11124,8 +11452,6 @@ packages: peerDependenciesMeta: debug: optional: true - dependencies: - debug: 4.3.2_supports-color@6.1.0 /for-each/0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -11406,6 +11732,11 @@ packages: engines: {node: '>=8.0.0'} dev: true + /get-port/3.2.0: + resolution: {integrity: sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=} + engines: {node: '>=4'} + dev: true + /get-port/5.1.1: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} @@ -12154,7 +12485,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.14.2_debug@4.3.2 + follow-redirects: 1.14.2 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -12364,6 +12695,25 @@ packages: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} dev: true + /inquirer/7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.2 + strip-ansi: 6.0.0 + through: 2.3.8 + dev: true + /internal-ip/4.3.0: resolution: {integrity: sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==} engines: {node: '>=6'} @@ -13963,6 +14313,11 @@ packages: json5: 2.2.0 dev: true + /local-access/1.1.0: + resolution: {integrity: sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==} + engines: {node: '>=6'} + dev: true + /locate-path/2.0.0: resolution: {integrity: sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=} engines: {node: '>=4'} @@ -14588,6 +14943,10 @@ packages: thunky: 1.1.0 dev: true + /mute-stream/0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + dev: true + /nan/2.15.0: resolution: {integrity: sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==} dev: true @@ -15133,6 +15492,11 @@ packages: resolution: {integrity: sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=} dev: true + /os-tmpdir/1.0.2: + resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=} + engines: {node: '>=0.10.0'} + dev: true + /overlayscrollbars/1.13.1: resolution: {integrity: sha512-gIQfzgGgu1wy80EB4/6DaJGHMEGmizq27xHIESrzXq0Y/J0Ay1P3DWk6tuVmEPIZH15zaBlxeEJOqdJKmowHCQ==} dev: true @@ -16459,7 +16823,6 @@ packages: dependencies: preact: 10.5.14 pretty-format: 3.8.0 - dev: true /preact-router/3.2.1_preact@10.5.14: resolution: {integrity: sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg==} @@ -16533,7 +16896,6 @@ packages: /pretty-format/3.8.0: resolution: {integrity: sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U=} - dev: true /pretty-hrtime/1.0.3: resolution: {integrity: sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=} @@ -17248,6 +17610,11 @@ packages: define-properties: 1.1.3 dev: true + /regexpp/2.0.1: + resolution: {integrity: sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==} + engines: {node: '>=6.5.0'} + dev: true + /regexpp/3.1.0: resolution: {integrity: sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==} engines: {node: '>=8'} @@ -17553,6 +17920,13 @@ packages: resolution: {integrity: sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=} dev: true + /rimraf/2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + hasBin: true + dependencies: + glob: 7.1.7 + dev: true + /rimraf/2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} hasBin: true @@ -17708,6 +18082,11 @@ packages: engines: {node: 6.* || >= 7.*} dev: true + /run-async/2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + dev: true + /run-parallel/1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: @@ -17720,6 +18099,13 @@ packages: aproba: 1.2.0 dev: true + /rxjs/6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + dependencies: + tslib: 1.14.1 + dev: true + /sade/1.7.4: resolution: {integrity: sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==} engines: {node: '>= 6'} @@ -17837,6 +18223,11 @@ packages: node-forge: 0.10.0 dev: true + /semiver/1.1.0: + resolution: {integrity: sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==} + engines: {node: '>=6'} + dev: true + /semver-diff/3.1.1: resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} engines: {node: '>=8'} @@ -18075,6 +18466,21 @@ packages: is-arrayish: 0.3.2 dev: true + /sirv-cli/1.0.14: + resolution: {integrity: sha512-yyUTNr984ANKDloqepkYbBSqvx3buwYg2sQKPWjSU+IBia5loaoka2If8N9CMwt8AfP179cdEl7kYJ//iWJHjQ==} + engines: {node: '>= 10'} + hasBin: true + dependencies: + console-clear: 1.1.1 + get-port: 3.2.0 + kleur: 3.0.3 + local-access: 1.1.0 + sade: 1.7.4 + semiver: 1.1.0 + sirv: 1.0.14 + tinydate: 1.3.0 + dev: true + /sirv/1.0.14: resolution: {integrity: sha512-czTFDFjK9lXj0u9mJ3OmJoXFztoilYS+NdRPcJoT182w44wSEkHSiO7A2517GLJ8wKM4GjCm2OXE66Dhngbzjg==} engines: {node: '>= 10'} @@ -18116,6 +18522,15 @@ packages: engines: {node: '>=8'} dev: true + /slice-ansi/2.1.0: + resolution: {integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==} + engines: {node: '>=6'} + dependencies: + ansi-styles: 3.2.1 + astral-regex: 1.0.0 + is-fullwidth-code-point: 2.0.0 + dev: true + /slice-ansi/3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} @@ -18797,6 +19212,16 @@ packages: object.getownpropertydescriptors: 2.1.2 dev: true + /table/5.4.6: + resolution: {integrity: sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==} + engines: {node: '>=6.0.0'} + dependencies: + ajv: 6.12.6 + lodash: 4.17.21 + slice-ansi: 2.1.0 + string-width: 3.1.0 + dev: true + /table/6.0.7: resolution: {integrity: sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==} engines: {node: '>=10.0.0'} @@ -18971,6 +19396,10 @@ packages: engines: {node: '>=10'} dev: true + /through/2.3.8: + resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=} + dev: true + /through2/2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} dependencies: @@ -19006,6 +19435,18 @@ packages: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false + /tinydate/1.3.0: + resolution: {integrity: sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==} + engines: {node: '>=4'} + dev: true + + /tmp/0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + dependencies: + os-tmpdir: 1.0.2 + dev: true + /tmpl/1.0.4: resolution: {integrity: sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=} dev: true @@ -19171,6 +19612,16 @@ packages: /tslib/2.3.1: resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} + /tsutils/3.19.1_typescript@3.9.10: + resolution: {integrity: sha512-GEdoBf5XI324lu7ycad7s6laADfnAqCw6wLGI+knxvw9vsIYBaJfYdmeCEG3FMMUiSm3OGgNb+m6utsWf5h9Vw==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 3.9.10 + dev: true + /tsutils/3.19.1_typescript@4.1.3: resolution: {integrity: sha512-GEdoBf5XI324lu7ycad7s6laADfnAqCw6wLGI+knxvw9vsIYBaJfYdmeCEG3FMMUiSm3OGgNb+m6utsWf5h9Vw==} engines: {node: '>= 6'} @@ -19293,6 +19744,12 @@ packages: typescript: 4.1.3 dev: true + /typescript/3.9.10: + resolution: {integrity: sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /typescript/4.1.3: resolution: {integrity: sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==} engines: {node: '>=4.2.0'} @@ -20363,6 +20820,13 @@ packages: typedarray-to-buffer: 3.1.5 dev: true + /write/1.0.3: + resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==} + engines: {node: '>=4'} + dependencies: + mkdirp: 0.5.5 + dev: true + /ws/6.2.2: resolution: {integrity: sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==} dependencies: |