gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: fixing withdrawal process


From: gnunet
Subject: [taler-wallet-core] branch master updated: fixing withdrawal process
Date: Mon, 11 Oct 2021 21:00:04 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new be8e3f4b fixing withdrawal process
be8e3f4b is described below

commit be8e3f4b1d090a536967f132a7fd4742bbcd5343
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Oct 11 15:59:55 2021 -0300

    fixing withdrawal process
---
 .../src/components/SelectList.tsx                  |   8 +-
 .../src/components/styled/index.tsx                |  23 +-
 packages/taler-wallet-webextension/src/cta/Pay.tsx |  31 +-
 .../src/cta/Withdraw.stories.tsx                   | 148 ++++++-
 .../taler-wallet-webextension/src/cta/Withdraw.tsx | 171 ++++----
 .../hooks/{useBalances.tsx => useAsyncAsHook.ts}   |  39 +-
 .../src/hooks/{useBalances.tsx => useBalances.ts}  |  11 +-
 .../src/popup/Balance.stories.tsx                  | 128 +++++-
 .../src/popup/BalancePage.tsx                      | 129 +++---
 .../taler-wallet-webextension/src/popup/Debug.tsx  |   1 -
 .../src/popup/History.stories.tsx                  |   6 +-
 .../src/popup/History.tsx                          |  30 +-
 .../src/popup/Settings.tsx                         |  19 +-
 .../src/wallet/Balance.stories.tsx                 |  11 +-
 .../src/wallet/BalancePage.tsx                     |   2 +-
 .../src/wallet/History.tsx                         |   2 +-
 .../src/wallet/Settings.tsx                        |  49 ++-
 packages/taler-wallet-webextension/src/wxApi.ts    |   5 +
 pnpm-lock.yaml                                     | 478 ++++++++++++++++++++-
 19 files changed, 1045 insertions(+), 246 deletions(-)

diff --git a/packages/taler-wallet-webextension/src/components/SelectList.tsx 
b/packages/taler-wallet-webextension/src/components/SelectList.tsx
index 7890c3fa..536e5b89 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 e77e7d54..7c3bb394 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 8e02cf6b..675b14ff 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 94fdea8f..69073f50 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 46451e72..52295f1a 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/useBalances.tsx 
b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
similarity index 53%
copy from packages/taler-wallet-webextension/src/hooks/useBalances.tsx
copy to packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
index 503b7a49..2131d45c 100644
--- a/packages/taler-wallet-webextension/src/hooks/useBalances.tsx
+++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
@@ -13,39 +13,36 @@
  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 { BalancesResponse } from "@gnu-taler/taler-util";
+import { ExchangesListRespose } from "@gnu-taler/taler-util";
 import { useEffect, useState } from "preact/hooks";
 import * as wxApi from "../wxApi";
 
-
-interface BalancesHookOk {
-  error: false;
-  response: BalancesResponse;
+interface HookOk<T> {
+  hasError: false;
+  response: T;
 }
 
-interface BalancesHookError {
-  error: true;
+interface HookError {
+  hasError: true;
+  message: string;
 }
 
-export type BalancesHook = BalancesHookOk | BalancesHookError | undefined;
+export type HookResponse<T> = HookOk<T> | HookError | undefined;
 
-export function useBalances(): BalancesHook {
-  const [balance, setBalance] = useState<BalancesHook>(undefined);
+export function useAsyncAsHook<T> (fn: (() => Promise<T>)): HookResponse<T> {
+  const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
   useEffect(() => {
-    async function checkBalance() {
+    async function doAsync() {
       try {
-        const response = await wxApi.getBalance();
-        console.log("got balance", balance);
-        setBalance({ error: false, response });
+        const response = await fn();
+        setHookResponse({ hasError: false, response });
       } catch (e) {
-        console.error("could not retrieve balances", e);
-        setBalance({ error: true });
+        if (e instanceof Error) {
+          setHookResponse({ hasError: true, message: e.message });
+        }
       }
     }
-    checkBalance()
-    return wxApi.onUpdateNotification(checkBalance);
+    doAsync()
   }, []);
-
-  return balance;
+  return result;
 }
diff --git a/packages/taler-wallet-webextension/src/hooks/useBalances.tsx 
b/packages/taler-wallet-webextension/src/hooks/useBalances.ts
similarity index 87%
rename from packages/taler-wallet-webextension/src/hooks/useBalances.tsx
rename to packages/taler-wallet-webextension/src/hooks/useBalances.ts
index 503b7a49..37424fb0 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 a0655d37..382f9b54 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 e3bada8d..8e5c5c42 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 3968b019..ccc74746 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 ca9f545f..daa263a8 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 77d60388..1447da9b 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 52e72ee2..8595c87f 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 1b145345..cccda203 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 e06e884c..eb5a0447 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 2bb59fcd..43b0a663 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 52e72ee2..d1eb012f 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 8a0881a6..664cc564 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 b14baed1..b8f1fd54 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:

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]