gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (fae6c420 -> ccb50c63)


From: gnunet
Subject: [taler-wallet-core] branch master updated (fae6c420 -> ccb50c63)
Date: Mon, 11 Apr 2022 16:36:48 +0200

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

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

    from fae6c420 multiline for input
     new 56c2a9c6 add payto stringify
     new df7c249c fix ref for copy and paste
     new 2bd6dae0 show amount nicely, into a component
     new 2bf8976d terms of service stories into its own scenarios (removed from 
withdraw)
     new e09ed466 missing index file
     new ccb50c63 new test api to test hooks rendering iteration, testing state 
of withdraw page

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/taler-util/src/payto.ts                   |  48 +-
 .../build-fast-with-linaria.mjs                    |   2 +-
 .../src/components/Amount.tsx                      |  12 +
 .../src/components/BankDetailsByPaytoType.tsx      |  22 +-
 .../src/components/ErrorMessage.tsx                |   2 +-
 packages/taler-wallet-webextension/src/cta/Pay.tsx |  20 +-
 .../src/cta/TermsOfServiceSection.stories.tsx      | 179 ++++++
 .../src/cta/TermsOfServiceSection.tsx              |   2 +-
 .../src/cta/Withdraw.stories.tsx                   | 474 ++++++----------
 .../src/cta/Withdraw.test.ts                       | 122 ++++
 .../taler-wallet-webextension/src/cta/Withdraw.tsx | 626 ++++++++++++---------
 .../src/cta/index.stories.ts                       |   3 +-
 .../src/hooks/useAsyncAsHook.ts                    |   4 +-
 .../src/hooks/useTalerActionURL.test.ts            |  14 +-
 .../src/mui/TextField.stories.tsx                  |  34 +-
 .../src/mui/input/InputBase.tsx                    |   6 +-
 .../taler-wallet-webextension/src/test-utils.ts    |  43 +-
 .../taler-wallet-webextension/src/utils/index.ts   |  17 +-
 .../src/wallet/CreateManualWithdraw.test.ts        | 158 +++---
 .../src/wallet/CreateManualWithdraw.tsx            |  19 +-
 .../src/wallet/DepositPage.test.ts                 |  16 +-
 .../src/wallet/ManualWithdrawPage.tsx              |   2 +-
 .../src/wallet/ReserveCreated.stories.tsx          |  16 +-
 .../src/wallet/ReserveCreated.tsx                  |  25 +-
 .../src/wallet/Transaction.tsx                     |  55 +-
 25 files changed, 1146 insertions(+), 775 deletions(-)
 create mode 100644 packages/taler-wallet-webextension/src/components/Amount.tsx
 create mode 100644 
packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.stories.tsx
 create mode 100644 packages/taler-wallet-webextension/src/cta/Withdraw.test.ts

diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index a7736ea7..c9889160 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -51,6 +51,19 @@ interface PaytoUriBitcoin extends PaytoUriGeneric {
 
 const paytoPfx = "payto://";
 
+
+
+function buildSegwitGenerator(result: PaytoUriBitcoin, targetPath: string) {
+  //generate segwit address just once, save addr in payto object
+  //and use it as cache
+  return function generateSegwitAddress(reserve: string): { addr1: string, 
addr2: string } {
+    if (result.addr1 && result.addr2) return { addr1: result.addr1, addr2: 
result.addr2 };
+    const { addr1, addr2 } = generateFakeSegwitAddress(reserve, targetPath)
+    result.addr1 = addr1
+    result.addr2 = addr2
+    return { addr1, addr2 }
+  }
+}
 /**
  * Add query parameters to a payto URI
  */
@@ -66,6 +79,30 @@ export function addPaytoQueryParams(
   return paytoPfx + acct + "?" + searchParams.toString();
 }
 
+/**
+ * Serialize a PaytoURI into a valid payto:// string
+ * 
+ * @param p 
+ * @returns 
+ */
+export function stringifyPaytoUri(p: PaytoUri): string {
+  const url = `${paytoPfx}${p.targetType}//${p.targetPath}`
+  if (p.params) {
+    const search = Object.entries(p.params)
+      .map(([key, value]) => `${key}=${value}`)
+      .join("&");
+    return `${url}?${search}`
+  }
+  return url
+}
+
+/**
+ * Parse a valid payto:// uri into a PaytoUri object
+ * RFC 8905
+ * 
+ * @param s 
+ * @returns 
+ */
 export function parsePaytoUri(s: string): PaytoUri | undefined {
   if (!s.startsWith(paytoPfx)) {
     return undefined;
@@ -123,16 +160,7 @@ export function parsePaytoUri(s: string): PaytoUri | 
undefined {
       generateSegwitAddress: (): any => null
     }
 
-    //generate segwit address just once, save addr in payto object
-    //and use it as cache
-    function generateSegwitAddress(reserve: string) {
-      if (result.addr1 && result.addr2) return { addr1: result.addr1, addr2: 
result.addr2 };
-      const { addr1, addr2 } = generateFakeSegwitAddress(reserve, targetPath)
-      result.addr1 = addr1
-      result.addr2 = addr2
-      return { addr1, addr2 }
-    }
-    result.generateSegwitAddress = generateSegwitAddress
+    result.generateSegwitAddress = buildSegwitGenerator(result, targetPath)
     return result;
 
   }
diff --git a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs 
b/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
index 77106a4f..f6de6788 100755
--- a/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
+++ b/packages/taler-wallet-webextension/build-fast-with-linaria.mjs
@@ -60,7 +60,7 @@ export const buildConfig = {
   ],
   format: 'iife',
   platform: 'browser',
-  sourcemap: 'external',
+  sourcemap: true, 
   jsxFactory: 'h',
   jsxFragment: 'Fragment',
   // define: {
diff --git a/packages/taler-wallet-webextension/src/components/Amount.tsx 
b/packages/taler-wallet-webextension/src/components/Amount.tsx
new file mode 100644
index 00000000..c41f7faf
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/components/Amount.tsx
@@ -0,0 +1,12 @@
+import { AmountJson, Amounts, AmountString } from "@gnu-taler/taler-util";
+import { h, VNode, Fragment } from "preact";
+
+export function Amount({ value }: { value: AmountJson | AmountString }): VNode 
{
+  const aj = Amounts.jsonifyAmount(value);
+  const amount = Amounts.stringifyValue(aj, 2);
+  return (
+    <Fragment>
+      {amount} {aj.currency}
+    </Fragment>
+  );
+}
diff --git 
a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx 
b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
index aff2bada..182e82a3 100644
--- 
a/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
+++ 
b/packages/taler-wallet-webextension/src/components/BankDetailsByPaytoType.tsx
@@ -16,7 +16,7 @@
 
 import { PaytoUri } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
 import { useTranslationContext } from "../context/translation.js";
 import { CopiedIcon, CopyIcon } from "../svg/index.js";
 import { ButtonBox, TooltipRight } from "./styled/index.js";
@@ -25,7 +25,7 @@ export interface BankDetailsProps {
   payto: PaytoUri | undefined;
   exchangeBaseUrl: string;
   subject: string;
-  amount: string;
+  amount: string | VNode;
 }
 
 export function BankDetailsByPaytoType({
@@ -84,12 +84,17 @@ function Row({
   literal,
 }: {
   name: VNode;
-  value: string;
+  value: string | VNode;
   literal?: boolean;
 }): VNode {
   const [copied, setCopied] = useState(false);
+  const preRef = useRef<HTMLPreElement>(null);
+  const tdRef = useRef<HTMLTableCellElement>(null);
   function copyText(): void {
-    navigator.clipboard.writeText(value);
+    const content = literal
+      ? preRef.current?.textContent
+      : tdRef.current?.textContent;
+    navigator.clipboard.writeText(content || "");
     setCopied(true);
   }
   useEffect(() => {
@@ -98,7 +103,7 @@ function Row({
         setCopied(false);
       }, 1000);
     }
-  }, [copied]);
+  }, [copied, preRef]);
   return (
     <tr>
       <td>
@@ -119,12 +124,15 @@ function Row({
       </td>
       {literal ? (
         <td>
-          <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
+          <pre
+            ref={preRef}
+            style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
+          >
             {value}
           </pre>
         </td>
       ) : (
-        <td>{value}</td>
+        <td ref={tdRef}>{value}</td>
       )}
     </tr>
   );
diff --git a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx 
b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
index f6e2ba2c..7b62a735 100644
--- a/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
+++ b/packages/taler-wallet-webextension/src/components/ErrorMessage.tsx
@@ -23,7 +23,7 @@ export function ErrorMessage({
   description,
 }: {
   title: VNode;
-  description?: string;
+  description?: string | VNode;
 }): VNode | null {
   const [showErrorDetail, setShowErrorDetail] = useState(false);
   return (
diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx 
b/packages/taler-wallet-webextension/src/cta/Pay.tsx
index 35962599..f2661308 100644
--- a/packages/taler-wallet-webextension/src/cta/Pay.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx
@@ -29,6 +29,7 @@ import {
   AmountJson,
   AmountLike,
   Amounts,
+  AmountString,
   ConfirmPayResult,
   ConfirmPayResultDone,
   ConfirmPayResultType,
@@ -41,6 +42,7 @@ import {
 import { TalerError } from "@gnu-taler/taler-wallet-core";
 import { Fragment, h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
+import { Amount } from "../components/Amount.js";
 import { ErrorMessage } from "../components/ErrorMessage.js";
 import { Loading } from "../components/Loading.js";
 import { LoadingError } from "../components/LoadingError.js";
@@ -261,7 +263,7 @@ export function PaymentRequestView({
           <section>
             <ButtonSuccess upperCased onClick={onClick}>
               <i18n.Translate>
-                Pay {amountToString(payStatus.amountEffective)}
+                Pay {<Amount value={payStatus.amountEffective} />}
               </i18n.Translate>
             </ButtonSuccess>
           </section>
@@ -276,8 +278,8 @@ export function PaymentRequestView({
             {balance ? (
               <WarningBox>
                 <i18n.Translate>
-                  Your balance of {amountToString(balance)} is not enough to 
pay
-                  for this purchase
+                  Your balance of {<Amount value={balance} />} is not enough to
+                  pay for this purchase
                 </i18n.Translate>
               </WarningBox>
             ) : (
@@ -374,14 +376,14 @@ export function PaymentRequestView({
             <Part
               big
               title={<i18n.Translate>Total to pay</i18n.Translate>}
-              text={amountToString(payStatus.amountEffective)}
+              text={<Amount value={payStatus.amountEffective} />}
               kind="negative"
             />
           )}
         <Part
           big
           title={<i18n.Translate>Purchase amount</i18n.Translate>}
-          text={amountToString(payStatus.amountRaw)}
+          text={<Amount value={payStatus.amountRaw} />}
           kind="neutral"
         />
         {Amounts.isNonZero(totalFees) && (
@@ -389,7 +391,7 @@ export function PaymentRequestView({
             <Part
               big
               title={<i18n.Translate>Fee</i18n.Translate>}
-              text={amountToString(totalFees)}
+              text={<Amount value={totalFees} />}
               kind="negative"
             />
           </Fragment>
@@ -493,9 +495,3 @@ function ProductList({ products }: { products: Product[] 
}): VNode {
     </Fragment>
   );
 }
-
-function amountToString(text: AmountLike): string {
-  const aj = Amounts.jsonifyAmount(text);
-  const amount = Amounts.stringifyValue(aj, 2);
-  return `${amount} ${aj.currency}`;
-}
diff --git 
a/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.stories.tsx 
b/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.stories.tsx
new file mode 100644
index 00000000..f24da1e1
--- /dev/null
+++ 
b/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.stories.tsx
@@ -0,0 +1,179 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { createExample } from "../test-utils.js";
+import { termsHtml, termsPdf, termsPlain, termsXml } from "./termsExample.js";
+import { TermsOfServiceSection as TestedComponent } from 
"./TermsOfServiceSection.js";
+
+function parseFromString(s: string): Document {
+  if (typeof window === "undefined") {
+    return {} as Document;
+  }
+  return new window.DOMParser().parseFromString(s, "text/xml");
+}
+
+export default {
+  title: "cta/terms of service",
+  component: TestedComponent,
+};
+
+export const ReviewingPLAIN = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "plain",
+      content: termsPlain,
+    },
+    status: "new",
+    version: "",
+  },
+  reviewing: true,
+});
+
+export const ReviewingHTML = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "html",
+      href: new URL(`data:text/html;base64,${toBase64(termsHtml)}`),
+    },
+    version: "",
+    status: "new",
+  },
+  reviewing: true,
+});
+
+function toBase64(str: string): string {
+  return btoa(
+    encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
+      return String.fromCharCode(parseInt(p1, 16));
+    }),
+  );
+}
+
+export const ReviewingPDF = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "pdf",
+      location: new URL(`data:text/html;base64,${toBase64(termsPdf)}`),
+    },
+    status: "new",
+    version: "",
+  },
+  reviewing: true,
+});
+
+export const ReviewingXML = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "xml",
+      document: parseFromString(termsXml),
+    },
+    status: "new",
+    version: "",
+  },
+  reviewing: true,
+});
+
+export const NewAccepted = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "xml",
+      document: parseFromString(termsXml),
+    },
+    status: "new",
+    version: "",
+  },
+  reviewed: true,
+});
+
+export const ShowAgainXML = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "xml",
+      document: parseFromString(termsXml),
+    },
+    version: "",
+    status: "new",
+  },
+  reviewed: true,
+  reviewing: true,
+});
+
+export const ChangedButNotReviewable = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "xml",
+      document: parseFromString(termsXml),
+    },
+    version: "",
+    status: "changed",
+  },
+});
+
+export const ChangedAndAllowReview = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "xml",
+      document: parseFromString(termsXml),
+    },
+    version: "",
+    status: "changed",
+  },
+  onReview: () => null,
+});
+
+export const NewButNotReviewable = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "xml",
+      document: parseFromString(termsXml),
+    },
+    version: "",
+    status: "new",
+  },
+});
+
+export const NewAndAllowReview = createExample(TestedComponent, {
+  terms: {
+    content: {
+      type: "xml",
+      document: parseFromString(termsXml),
+    },
+    version: "",
+    status: "new",
+  },
+  onReview: () => null,
+});
+
+export const NotFound = createExample(TestedComponent, {
+  terms: {
+    content: undefined,
+    status: "notfound",
+    version: "",
+  },
+});
+
+export const AlreadyAccepted = createExample(TestedComponent, {
+  terms: {
+    status: "accepted",
+    content: undefined,
+    version: "",
+  },
+});
diff --git 
a/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx 
b/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx
index b4962768..05714486 100644
--- a/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx
+++ b/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx
@@ -12,7 +12,7 @@ import {
 import { useTranslationContext } from "../context/translation.js";
 import { TermsState } from "../utils/index.js";
 
-interface Props {
+export interface Props {
   reviewing: boolean;
   reviewed: boolean;
   terms: TermsState;
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
index eb18251f..2191205c 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
@@ -19,349 +19,203 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { amountFractionalBase, ExchangeListItem } from "@gnu-taler/taler-util";
 import { createExample } from "../test-utils.js";
-import { termsHtml, termsPdf, termsPlain, termsXml } from "./termsExample.js";
+import { TermsState } from "../utils/index.js";
 import { View as TestedComponent } from "./Withdraw.js";
 
-function parseFromString(s: string): Document {
-  if (typeof window === "undefined") {
-    return {} as Document;
-  }
-  return new window.DOMParser().parseFromString(s, "text/xml");
-}
-
 export default {
   title: "cta/withdraw",
   component: TestedComponent,
 };
 
-const exchangeList: ExchangeListItem[] = [
-  {
-    currency: "USD",
-    exchangeBaseUrl: "exchange.demo.taler.net",
-    tos: {
-      currentVersion: "1",
-      acceptedVersion: "1",
-      content: "terms of service content",
-      contentType: "text/plain",
-    },
-    paytoUris: ["asd"],
-  },
-  {
-    currency: "USD",
-    exchangeBaseUrl: "exchange.test.taler.net",
-    tos: {
-      currentVersion: "1",
-      acceptedVersion: "1",
-      content: "terms of service content",
-      contentType: "text/plain",
-    },
-    paytoUris: ["asd"],
-  },
-];
-
-export const NewTerms = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 1,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
+const exchangeList = {
+  "exchange.demo.taler.net": "http://exchange.demo.taler.net (USD)",
+  "exchange.test.taler.net": "http://exchange.test.taler.net (KUDOS)",
+};
 
-  onSwitchExchange: async () => {
+const nullHandler = {
+  onClick: async (): Promise<void> => {
     null;
   },
-  terms: {
-    content: {
-      type: "xml",
-      document: parseFromString(termsXml),
-    },
-    status: "new",
-    version: "",
-  },
-});
-
-export const TermsReviewingPLAIN = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
+};
 
-  onSwitchExchange: async () => {
-    null;
-  },
+const normalTosState = {
   terms: {
-    content: {
-      type: "plain",
-      content: termsPlain,
-    },
-    status: "new",
+    status: "accepted",
     version: "",
-  },
-  reviewing: true,
-});
-
-export const TermsReviewingHTML = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
+  } as TermsState,
+  onAccept: () => null,
+  onReview: () => null,
+  reviewed: false,
+  reviewing: false,
+};
 
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    content: {
-      type: "html",
-      href: new URL(`data:text/html;base64,${toBase64(termsHtml)}`),
+export const TermsOfServiceNotYetLoaded = createExample(TestedComponent, {
+  state: {
+    hook: undefined,
+    status: "success",
+    cancelEditExchange: nullHandler,
+    confirmEditExchange: nullHandler,
+    chosenAmount: {
+      currency: "USD",
+      value: 2,
+      fraction: 10000000,
     },
-    version: "",
-    status: "new",
-  },
-  reviewing: true,
-});
-
-function toBase64(str: string): string {
-  return btoa(
-    encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
-      return String.fromCharCode(parseInt(p1, 16));
-    }),
-  );
-}
-
-export const TermsReviewingPDF = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    content: {
-      type: "pdf",
-      location: new URL(`data:text/html;base64,${toBase64(termsPdf)}`),
+    doWithdrawal: nullHandler,
+    editExchange: nullHandler,
+    exchange: {
+      list: exchangeList,
+      value: "exchange.demo.taler.net",
+      onChange: () => null,
     },
-    status: "new",
-    version: "",
-  },
-  reviewing: true,
-});
-
-export const TermsReviewingXML = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    content: {
-      type: "xml",
-      document: parseFromString(termsXml),
+    showExchangeSelection: false,
+    mustAcceptFirst: false,
+    withdrawalFee: {
+      currency: "USD",
+      fraction: 10000000,
+      value: 1,
     },
-    status: "new",
-    version: "",
-  },
-  reviewing: true,
-});
-
-export const NewTermsAccepted = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    content: {
-      type: "xml",
-      document: parseFromString(termsXml),
+    toBeReceived: {
+      currency: "USD",
+      fraction: 0,
+      value: 1,
     },
-    status: "new",
-    version: "",
   },
-  reviewed: true,
 });
 
-export const TermsShowAgainXML = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    content: {
-      type: "xml",
-      document: parseFromString(termsXml),
+export const WithSomeFee = createExample(TestedComponent, {
+  state: {
+    hook: undefined,
+    status: "success",
+    cancelEditExchange: nullHandler,
+    confirmEditExchange: nullHandler,
+    chosenAmount: {
+      currency: "USD",
+      value: 2,
+      fraction: 10000000,
     },
-    version: "",
-    status: "new",
-  },
-  reviewed: true,
-  reviewing: true,
-});
-
-export const TermsChanged = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    content: {
-      type: "xml",
-      document: parseFromString(termsXml),
+    doWithdrawal: nullHandler,
+    editExchange: nullHandler,
+    exchange: {
+      list: exchangeList,
+      value: "exchange.demo.taler.net",
+      onChange: () => null,
     },
-    version: "",
-    status: "changed",
+    showExchangeSelection: false,
+    mustAcceptFirst: false,
+    withdrawalFee: {
+      currency: "USD",
+      fraction: 10000000,
+      value: 1,
+    },
+    toBeReceived: {
+      currency: "USD",
+      fraction: 0,
+      value: 1,
+    },
+    tosProps: normalTosState,
   },
 });
 
-export const TermsNotFound = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    content: undefined,
-    status: "notfound",
-    version: "",
+export const WithoutFee = createExample(TestedComponent, {
+  state: {
+    hook: undefined,
+    status: "success",
+    cancelEditExchange: nullHandler,
+    confirmEditExchange: nullHandler,
+    chosenAmount: {
+      currency: "USD",
+      value: 2,
+      fraction: 10000000,
+    },
+    doWithdrawal: nullHandler,
+    editExchange: nullHandler,
+    exchange: {
+      list: exchangeList,
+      value: "exchange.demo.taler.net",
+      onChange: () => null,
+    },
+    showExchangeSelection: false,
+    mustAcceptFirst: false,
+    withdrawalFee: {
+      currency: "USD",
+      fraction: 0,
+      value: 0,
+    },
+    toBeReceived: {
+      currency: "USD",
+      fraction: 0,
+      value: 2,
+    },
+    tosProps: normalTosState,
   },
 });
 
-export const TermsAlreadyAccepted = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: amountFractionalBase * 0.5,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    status: "accepted",
-    content: undefined,
-    version: "",
+export const EditExchangeUntouched = createExample(TestedComponent, {
+  state: {
+    hook: undefined,
+    status: "success",
+    cancelEditExchange: nullHandler,
+    confirmEditExchange: nullHandler,
+    chosenAmount: {
+      currency: "USD",
+      value: 2,
+      fraction: 10000000,
+    },
+    doWithdrawal: nullHandler,
+    editExchange: nullHandler,
+    exchange: {
+      list: exchangeList,
+      value: "exchange.demo.taler.net",
+      onChange: () => null,
+    },
+    showExchangeSelection: true,
+    mustAcceptFirst: false,
+    withdrawalFee: {
+      currency: "USD",
+      fraction: 0,
+      value: 0,
+    },
+    toBeReceived: {
+      currency: "USD",
+      fraction: 0,
+      value: 2,
+    },
+    tosProps: normalTosState,
   },
 });
 
-export const WithoutFee = createExample(TestedComponent, {
-  knownExchanges: exchangeList,
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    content: {
-      type: "xml",
-      document: parseFromString(termsXml),
+export const EditExchangeModified = createExample(TestedComponent, {
+  state: {
+    hook: undefined,
+    status: "success",
+    cancelEditExchange: nullHandler,
+    confirmEditExchange: nullHandler,
+    chosenAmount: {
+      currency: "USD",
+      value: 2,
+      fraction: 10000000,
     },
-    status: "accepted",
-    version: "",
+    doWithdrawal: nullHandler,
+    editExchange: nullHandler,
+    exchange: {
+      list: exchangeList,
+      isDirty: true,
+      value: "exchange.test.taler.net",
+      onChange: () => null,
+    },
+    showExchangeSelection: true,
+    mustAcceptFirst: false,
+    withdrawalFee: {
+      currency: "USD",
+      fraction: 0,
+      value: 0,
+    },
+    toBeReceived: {
+      currency: "USD",
+      fraction: 0,
+      value: 2,
+    },
+    tosProps: normalTosState,
   },
 });
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.test.ts 
b/packages/taler-wallet-webextension/src/cta/Withdraw.test.ts
new file mode 100644
index 00000000..5a28c4cf
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.test.ts
@@ -0,0 +1,122 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { ExchangeListItem } from "@gnu-taler/taler-util";
+import { expect } from "chai";
+import { mountHook } from "../test-utils.js";
+import { useComponentState } from "./Withdraw.js";
+
+const exchanges: ExchangeListItem[] = [{
+  currency: 'ARS',
+  exchangeBaseUrl: 'http://exchange.demo.taler.net',
+  paytoUris: [],
+  tos: {
+    acceptedVersion: '',
+  }
+}]
+
+describe("Withdraw CTA states", () => {
+  it("should tell the user that the URI is missing", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(undefined, {
+        listExchanges: async () => ({ exchanges }),
+        getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
+          amount: 'ARS:2',
+          possibleExchanges: exchanges,
+        })
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading-uri')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+
+      expect(status).equals('loading-uri')
+      expect(hook).deep.equals({ "hasError": true, "operational": false, 
"message": "ERROR_NO-URI-FOR-WITHDRAWAL" });
+    }
+    await waitNextUpdate()
+    {
+      const { status, hook } = getLastResultOrThrow()
+
+      expect(status).equals('loading-uri')
+      expect(hook).deep.equals({ "hasError": true, "operational": false, 
"message": "ERROR_NO-URI-FOR-WITHDRAWAL" });
+    }
+
+    await assertNoPendingUpdate()
+  });
+
+  it("should tell the user that there is not known exchange", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState('taler-withdraw://', {
+        listExchanges: async () => ({ exchanges }),
+        getWithdrawalDetailsForUri: async ({ talerWithdrawUri }: any) => ({
+          amount: 'EUR:2',
+          possibleExchanges: [],
+        })
+      } as any),
+    );
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+      expect(status).equals('loading-uri')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+
+      expect(status).equals('loading-exchange')
+      expect(hook).undefined;
+    }
+
+    await waitNextUpdate()
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+
+      expect(status).equals('loading-exchange')
+
+      expect(hook).deep.equals({ "hasError": true, "operational": false, 
"message": "ERROR_NO-DEFAULT-EXCHANGE" });
+    }
+
+    await waitNextUpdate()
+
+    {
+      const { status, hook } = getLastResultOrThrow()
+
+      expect(status).equals('loading-exchange')
+
+      expect(hook).deep.equals({ "hasError": true, "operational": false, 
"message": "ERROR_NO-DEFAULT-EXCHANGE" });
+    }
+
+    await assertNoPendingUpdate()
+  });
+
+});
\ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
index 676c65d2..9739e1a4 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
@@ -21,17 +21,14 @@
  * @author sebasjm
  */
 
-import {
-  AmountJson,
-  Amounts,
-  ExchangeListItem,
-  WithdrawUriInfoResponse,
-} from "@gnu-taler/taler-util";
+import { AmountJson, Amounts } from "@gnu-taler/taler-util";
+import { TalerError } from "@gnu-taler/taler-wallet-core";
 import { Fragment, h, VNode } from "preact";
-import { useCallback, useMemo, useState } from "preact/hooks";
+import { useState } from "preact/hooks";
+import { Amount } from "../components/Amount.js";
+import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
 import { Loading } from "../components/Loading.js";
 import { LoadingError } from "../components/LoadingError.js";
-import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
 import { LogoHeader } from "../components/LogoHeader.js";
 import { Part } from "../components/Part.js";
 import { SelectList } from "../components/SelectList.js";
@@ -42,72 +39,198 @@ import {
   SubTitle,
   WalletAction,
 } from "../components/styled/index.js";
-import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { useTranslationContext } from "../context/translation.js";
+import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { buildTermsOfServiceState } from "../utils/index.js";
 import {
-  amountToString,
-  buildTermsOfServiceState,
-  TermsState,
-} from "../utils/index.js";
+  ButtonHandler,
+  SelectFieldHandler,
+} from "../wallet/CreateManualWithdraw.js";
 import * as wxApi from "../wxApi.js";
-import { TermsOfServiceSection } from "./TermsOfServiceSection.js";
-import { useTranslationContext } from "../context/translation.js";
-import { TalerError } from "@gnu-taler/taler-wallet-core";
+import {
+  Props as TermsOfServiceSectionProps,
+  TermsOfServiceSection,
+} from "./TermsOfServiceSection.js";
 
 interface Props {
   talerWithdrawUri?: string;
 }
 
-export interface ViewProps {
-  withdrawalFee: AmountJson;
-  exchangeBaseUrl?: string;
-  amount: AmountJson;
-  onSwitchExchange: (ex: string) => void;
-  onWithdraw: () => Promise<void>;
-  onReview: (b: boolean) => void;
-  onAccept: (b: boolean) => void;
-  reviewing: boolean;
-  reviewed: boolean;
-  terms: TermsState;
-  knownExchanges: ExchangeListItem[];
+type State = LoadingUri | LoadingExchange | LoadingInfoError | Success;
+
+interface LoadingUri {
+  status: "loading-uri";
+  hook: HookError | undefined;
+}
+interface LoadingExchange {
+  status: "loading-exchange";
+  hook: HookError | undefined;
+}
+interface LoadingInfoError {
+  status: "loading-info";
+  hook: HookError | undefined;
 }
 
-export function View({
-  withdrawalFee,
-  exchangeBaseUrl,
-  knownExchanges,
-  amount,
-  onWithdraw,
-  onSwitchExchange,
-  terms,
-  reviewing,
-  onReview,
-  onAccept,
-  reviewed,
-}: ViewProps): VNode {
-  const { i18n } = useTranslationContext();
-  const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
+type Success = {
+  status: "success";
+  hook: undefined;
+
+  exchange: SelectFieldHandler;
+
+  editExchange: ButtonHandler;
+  cancelEditExchange: ButtonHandler;
+  confirmEditExchange: ButtonHandler;
+
+  showExchangeSelection: boolean;
+  chosenAmount: AmountJson;
+  withdrawalFee: AmountJson;
+  toBeReceived: AmountJson;
+
+  doWithdrawal: ButtonHandler;
+  tosProps?: TermsOfServiceSectionProps;
+  mustAcceptFirst: boolean;
+};
+
+export function useComponentState(
+  talerWithdrawUri: string | undefined,
+  api: typeof wxApi,
+): State {
+  const [customExchange, setCustomExchange] = useState<string | undefined>(
     undefined,
   );
-  const [confirmDisabled, setConfirmDisabled] = useState<boolean>(false);
 
-  const needsReview = terms.status === "changed" || terms.status === "new";
+  const uriInfoHook = useAsyncAsHook(async () => {
+    if (!talerWithdrawUri) throw Error("ERROR_NO-URI-FOR-WITHDRAWAL");
+
+    const uriInfo = await api.getWithdrawalDetailsForUri({
+      talerWithdrawUri,
+    });
+    const { exchanges: knownExchanges } = await api.listExchanges();
+
+    return { uriInfo, knownExchanges };
+  });
+
+  const exchangeAndAmount = useAsyncAsHook(
+    async () => {
+      if (!uriInfoHook || uriInfoHook.hasError || !uriInfoHook.response) 
return;
+      const { uriInfo, knownExchanges } = uriInfoHook.response;
+
+      const amount = Amounts.parseOrThrow(uriInfo.amount);
+
+      const thisCurrencyExchanges = knownExchanges.filter(
+        (ex) => ex.currency === amount.currency,
+      );
+
+      const thisExchange: string | undefined =
+        customExchange ??
+        uriInfo.defaultExchangeBaseUrl ??
+        (thisCurrencyExchanges[0]
+          ? thisCurrencyExchanges[0].exchangeBaseUrl
+          : undefined);
+
+      if (!thisExchange) throw Error("ERROR_NO-DEFAULT-EXCHANGE");
+
+      return { amount, thisExchange, thisCurrencyExchanges };
+    },
+    [],
+    [!uriInfoHook || uriInfoHook.hasError ? undefined : uriInfoHook],
+  );
+
+  const terms = useAsyncAsHook(
+    async () => {
+      if (
+        !exchangeAndAmount ||
+        exchangeAndAmount.hasError ||
+        !exchangeAndAmount.response
+      )
+        return;
+      const { thisExchange } = exchangeAndAmount.response;
+      const exchangeTos = await api.getExchangeTos(thisExchange, ["text/xml"]);
+
+      const state = buildTermsOfServiceState(exchangeTos);
+
+      return { state };
+    },
+    [],
+    [
+      !exchangeAndAmount || exchangeAndAmount.hasError
+        ? undefined
+        : exchangeAndAmount,
+    ],
+  );
+
+  const info = useAsyncAsHook(
+    async () => {
+      if (
+        !exchangeAndAmount ||
+        exchangeAndAmount.hasError ||
+        !exchangeAndAmount.response
+      )
+        return;
+      const { thisExchange, amount } = exchangeAndAmount.response;
+
+      const info = await api.getExchangeWithdrawalInfo({
+        exchangeBaseUrl: thisExchange,
+        amount,
+        tosAcceptedFormat: ["text/xml"],
+      });
+
+      const withdrawalFee = Amounts.sub(
+        Amounts.parseOrThrow(info.withdrawalAmountRaw),
+        Amounts.parseOrThrow(info.withdrawalAmountEffective),
+      ).amount;
 
-  const [switchingExchange, setSwitchingExchange] = useState(false);
-  const [nextExchange, setNextExchange] = useState<string | undefined>(
+      return { info, withdrawalFee };
+    },
+    [],
+    [
+      !exchangeAndAmount || exchangeAndAmount.hasError
+        ? undefined
+        : exchangeAndAmount,
+    ],
+  );
+
+  const [reviewing, setReviewing] = useState<boolean>(false);
+  const [reviewed, setReviewed] = useState<boolean>(false);
+
+  const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
     undefined,
   );
+  const [confirmDisabled, setConfirmDisabled] = useState<boolean>(false);
 
-  const exchanges = knownExchanges
-    .filter((e) => e.currency === amount.currency)
-    .reduce(
-      (prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }),
-      {},
-    );
+  const [showExchangeSelection, setShowExchangeSelection] = useState(false);
+  const [nextExchange, setNextExchange] = useState<string | undefined>();
+
+  if (!uriInfoHook || uriInfoHook.hasError) {
+    return {
+      status: "loading-uri",
+      hook: uriInfoHook,
+    };
+  }
+
+  if (!exchangeAndAmount || exchangeAndAmount.hasError) {
+    return {
+      status: "loading-exchange",
+      hook: exchangeAndAmount,
+    };
+  }
+  if (!exchangeAndAmount.response) {
+    return {
+      status: "loading-exchange",
+      hook: undefined,
+    };
+  }
+  const { thisExchange, thisCurrencyExchanges, amount } =
+    exchangeAndAmount.response;
 
   async function doWithdrawAndCheckError(): Promise<void> {
     try {
       setConfirmDisabled(true);
-      await onWithdraw();
+      if (!talerWithdrawUri) return;
+      const res = await api.acceptWithdrawal(talerWithdrawUri, thisExchange);
+      if (res.confirmTransferUrl) {
+        document.location.href = res.confirmTransferUrl;
+      }
     } catch (e) {
       if (e instanceof TalerError) {
         setWithdrawError(e);
@@ -116,6 +239,107 @@ export function View({
     }
   }
 
+  const exchanges = thisCurrencyExchanges.reduce(
+    (prev, ex) => ({ ...prev, [ex.exchangeBaseUrl]: ex.exchangeBaseUrl }),
+    {},
+  );
+
+  if (!info || info.hasError) {
+    return {
+      status: "loading-info",
+      hook: info,
+    };
+  }
+  if (!info.response) {
+    return {
+      status: "loading-info",
+      hook: undefined,
+    };
+  }
+
+  const exchangeHandler: SelectFieldHandler = {
+    onChange: setNextExchange,
+    value: nextExchange || thisExchange,
+    list: exchanges,
+    isDirty: nextExchange !== thisExchange,
+  };
+
+  const editExchange: ButtonHandler = {
+    onClick: async () => {
+      setShowExchangeSelection(true);
+    },
+  };
+  const cancelEditExchange: ButtonHandler = {
+    onClick: async () => {
+      setShowExchangeSelection(false);
+    },
+  };
+  const confirmEditExchange: ButtonHandler = {
+    onClick: async () => {
+      setCustomExchange(exchangeHandler.value);
+      setShowExchangeSelection(false);
+    },
+  };
+
+  const { withdrawalFee } = info.response;
+  const toBeReceived = Amounts.sub(amount, withdrawalFee).amount;
+
+  const { state: termsState } = (!terms
+    ? undefined
+    : terms.hasError
+    ? undefined
+    : terms.response) || { state: undefined };
+
+  async function onAccept(accepted: boolean): Promise<void> {
+    if (!termsState) return;
+
+    try {
+      await api.setExchangeTosAccepted(
+        thisExchange,
+        accepted ? termsState.version : undefined,
+      );
+      setReviewed(accepted);
+    } catch (e) {
+      if (e instanceof Error) {
+        //FIXME: uncomment this and display error
+        // setErrorAccepting(e.message);
+      }
+    }
+  }
+
+  return {
+    status: "success",
+    hook: undefined,
+    exchange: exchangeHandler,
+    editExchange,
+    cancelEditExchange,
+    confirmEditExchange,
+    showExchangeSelection,
+    toBeReceived,
+    withdrawalFee,
+    chosenAmount: amount,
+    doWithdrawal: {
+      onClick: doWithdrawAndCheckError,
+      error: withdrawError,
+      disabled: confirmDisabled,
+    },
+    tosProps: !termsState
+      ? undefined
+      : {
+          onAccept,
+          onReview: setReviewing,
+          reviewed: reviewed,
+          reviewing: reviewing,
+          terms: termsState,
+        },
+    mustAcceptFirst:
+      termsState !== undefined &&
+      (termsState.status === "changed" || termsState.status === "new"),
+  };
+}
+
+export function View({ state }: { state: Success }): VNode {
+  const { i18n } = useTranslationContext();
   return (
     <WalletAction>
       <LogoHeader />
@@ -123,267 +347,159 @@ export function View({
         <i18n.Translate>Digital cash withdrawal</i18n.Translate>
       </SubTitle>
 
-      {withdrawError && (
+      {state.doWithdrawal.error && (
         <ErrorTalerOperation
           title={
             <i18n.Translate>
               Could not finish the withdrawal operation
             </i18n.Translate>
           }
-          error={withdrawError.errorDetail}
+          error={state.doWithdrawal.error.errorDetail}
         />
       )}
 
       <section>
         <Part
           title={<i18n.Translate>Total to withdraw</i18n.Translate>}
-          text={amountToString(Amounts.sub(amount, withdrawalFee).amount)}
+          text={<Amount value={state.toBeReceived} />}
           kind="positive"
         />
-        {Amounts.isNonZero(withdrawalFee) && (
+        {Amounts.isNonZero(state.withdrawalFee) && (
           <Fragment>
             <Part
               title={<i18n.Translate>Chosen amount</i18n.Translate>}
-              text={amountToString(amount)}
+              text={<Amount value={state.chosenAmount} />}
               kind="neutral"
             />
             <Part
               title={<i18n.Translate>Exchange fee</i18n.Translate>}
-              text={amountToString(withdrawalFee)}
+              text={<Amount value={state.withdrawalFee} />}
               kind="negative"
             />
           </Fragment>
         )}
-        {exchangeBaseUrl && (
-          <Part
-            title={<i18n.Translate>Exchange</i18n.Translate>}
-            text={exchangeBaseUrl}
-            kind="neutral"
-            big
-          />
-        )}
-        {!reviewing &&
-          (switchingExchange ? (
-            <Fragment>
-              <div>
-                <SelectList
-                  label={<i18n.Translate>Known exchanges</i18n.Translate>}
-                  list={exchanges}
-                  value={nextExchange}
-                  name="switchingExchange"
-                  onChange={setNextExchange}
-                />
-              </div>
-              <LinkSuccess
-                upperCased
-                style={{ fontSize: "small" }}
-                onClick={() => {
-                  if (nextExchange !== undefined) {
-                    onSwitchExchange(nextExchange);
-                  }
-                  setSwitchingExchange(false);
-                }}
-              >
-                {nextExchange === undefined ? (
-                  <i18n.Translate>Cancel exchange selection</i18n.Translate>
-                ) : (
-                  <i18n.Translate>Confirm exchange selection</i18n.Translate>
-                )}
-              </LinkSuccess>
-            </Fragment>
-          ) : (
+        <Part
+          title={<i18n.Translate>Exchange</i18n.Translate>}
+          text={state.exchange.value}
+          kind="neutral"
+          big
+        />
+        {state.showExchangeSelection ? (
+          <Fragment>
+            <div>
+              <SelectList
+                label={<i18n.Translate>Known exchanges</i18n.Translate>}
+                list={state.exchange.list}
+                value={state.exchange.value}
+                name="switchingExchange"
+                onChange={state.exchange.onChange}
+              />
+            </div>
             <LinkSuccess
-              style={{ fontSize: "small" }}
               upperCased
-              onClick={() => setSwitchingExchange(true)}
+              style={{ fontSize: "small" }}
+              onClick={state.confirmEditExchange.onClick}
             >
-              <i18n.Translate>Edit exchange</i18n.Translate>
+              {state.exchange.isDirty ? (
+                <i18n.Translate>Confirm exchange selection</i18n.Translate>
+              ) : (
+                <i18n.Translate>Cancel exchange selection</i18n.Translate>
+              )}
             </LinkSuccess>
-          ))}
-      </section>
-      <TermsOfServiceSection
-        reviewed={reviewed}
-        reviewing={reviewing}
-        terms={terms}
-        onAccept={onAccept}
-        onReview={onReview}
-      />
-      <section>
-        {(terms.status === "accepted" || (needsReview && reviewed)) && (
-          <ButtonSuccess
-            upperCased
-            disabled={!exchangeBaseUrl || confirmDisabled}
-            onClick={doWithdrawAndCheckError}
-          >
-            <i18n.Translate>Confirm withdrawal</i18n.Translate>
-          </ButtonSuccess>
-        )}
-        {terms.status === "notfound" && (
-          <ButtonWarning
+          </Fragment>
+        ) : (
+          <LinkSuccess
+            style={{ fontSize: "small" }}
             upperCased
-            disabled={!exchangeBaseUrl}
-            onClick={doWithdrawAndCheckError}
+            onClick={state.editExchange.onClick}
           >
-            <i18n.Translate>Withdraw anyway</i18n.Translate>
-          </ButtonWarning>
+            <i18n.Translate>Edit exchange</i18n.Translate>
+          </LinkSuccess>
         )}
       </section>
+      {state.tosProps && <TermsOfServiceSection {...state.tosProps} />}
+      {state.tosProps ? (
+        <section>
+          {(state.tosProps.terms.status === "accepted" ||
+            (state.mustAcceptFirst && state.tosProps.reviewed)) && (
+            <ButtonSuccess
+              upperCased
+              disabled={state.doWithdrawal.disabled}
+              onClick={state.doWithdrawal.onClick}
+            >
+              <i18n.Translate>Confirm withdrawal</i18n.Translate>
+            </ButtonSuccess>
+          )}
+          {state.tosProps.terms.status === "notfound" && (
+            <ButtonWarning
+              upperCased
+              disabled={state.doWithdrawal.disabled}
+              onClick={state.doWithdrawal.onClick}
+            >
+              <i18n.Translate>Withdraw anyway</i18n.Translate>
+            </ButtonWarning>
+          )}
+        </section>
+      ) : (
+        <section>
+          <i18n.Translate>Loading terms of service...</i18n.Translate>
+        </section>
+      )}
     </WalletAction>
   );
 }
 
-export function WithdrawPageWithParsedURI({
-  uri,
-  uriInfo,
-}: {
-  uri: string;
-  uriInfo: WithdrawUriInfoResponse;
-}): VNode {
+export function WithdrawPage({ talerWithdrawUri }: Props): VNode {
   const { i18n } = useTranslationContext();
-  const [customExchange, setCustomExchange] = useState<string | undefined>(
-    undefined,
-  );
-
-  const [reviewing, setReviewing] = useState<boolean>(false);
-  const [reviewed, setReviewed] = useState<boolean>(false);
-
-  const knownExchangesHook = useAsyncAsHook(wxApi.listExchanges);
-
-  const knownExchanges = useMemo(
-    () =>
-      !knownExchangesHook || knownExchangesHook.hasError
-        ? []
-        : knownExchangesHook.response.exchanges,
-    [knownExchangesHook],
-  );
-  const withdrawAmount = useMemo(
-    () => Amounts.parseOrThrow(uriInfo.amount),
-    [uriInfo.amount],
-  );
-  const thisCurrencyExchanges = useMemo(
-    () =>
-      knownExchanges.filter((ex) => ex.currency === withdrawAmount.currency),
-    [knownExchanges, withdrawAmount.currency],
-  );
-
-  const exchange: string | undefined = useMemo(
-    () =>
-      customExchange ??
-      uriInfo.defaultExchangeBaseUrl ??
-      (thisCurrencyExchanges[0]
-        ? thisCurrencyExchanges[0].exchangeBaseUrl
-        : undefined),
-    [customExchange, thisCurrencyExchanges, uriInfo.defaultExchangeBaseUrl],
-  );
 
-  const detailsHook = useAsyncAsHook(async () => {
-    if (!exchange) throw Error("no default exchange");
-    const tos = await wxApi.getExchangeTos(exchange, ["text/xml"]);
+  const state = useComponentState(talerWithdrawUri, wxApi);
 
-    const tosState = buildTermsOfServiceState(tos);
-
-    const info = await wxApi.getExchangeWithdrawalInfo({
-      exchangeBaseUrl: exchange,
-      amount: withdrawAmount,
-      tosAcceptedFormat: ["text/xml"],
-    });
-    return { tos: tosState, info };
-  });
+  if (!talerWithdrawUri) {
+    return (
+      <span>
+        <i18n.Translate>missing withdraw uri</i18n.Translate>
+      </span>
+    );
+  }
 
-  if (!detailsHook) {
+  if (!state) {
     return <Loading />;
   }
-  if (detailsHook.hasError) {
+
+  console.log(state);
+  if (state.status === "loading-uri") {
+    if (!state.hook) return <Loading />;
+
     return (
       <LoadingError
         title={
-          <i18n.Translate>Could not load the withdrawal 
details</i18n.Translate>
+          <i18n.Translate>Could not get the info from the URI</i18n.Translate>
         }
-        error={detailsHook}
+        error={state.hook}
       />
     );
   }
+  if (state.status === "loading-exchange") {
+    if (!state.hook) return <Loading />;
 
-  const details = detailsHook.response;
-
-  const onAccept = async (accepted: boolean): Promise<void> => {
-    if (!exchange) return;
-    try {
-      await wxApi.setExchangeTosAccepted(
-        exchange,
-        accepted ? details.tos.version : undefined,
-      );
-      setReviewed(accepted);
-    } catch (e) {
-      if (e instanceof Error) {
-        //FIXME: uncomment this and display error
-        // setErrorAccepting(e.message);
-      }
-    }
-  };
-
-  const onWithdraw = async (): Promise<void> => {
-    if (!exchange) return;
-    const res = await wxApi.acceptWithdrawal(uri, exchange);
-    if (res.confirmTransferUrl) {
-      document.location.href = res.confirmTransferUrl;
-    }
-  };
-
-  const withdrawalFee = Amounts.sub(
-    Amounts.parseOrThrow(details.info.withdrawalAmountRaw),
-    Amounts.parseOrThrow(details.info.withdrawalAmountEffective),
-  ).amount;
-
-  return (
-    <View
-      onWithdraw={onWithdraw}
-      amount={withdrawAmount}
-      exchangeBaseUrl={exchange}
-      withdrawalFee={withdrawalFee}
-      terms={detailsHook.response.tos}
-      onSwitchExchange={setCustomExchange}
-      knownExchanges={knownExchanges}
-      reviewed={reviewed}
-      onAccept={onAccept}
-      reviewing={reviewing}
-      onReview={setReviewing}
-    />
-  );
-}
-export function WithdrawPage({ talerWithdrawUri }: Props): VNode {
-  const { i18n } = useTranslationContext();
-  const uriInfoHook = useAsyncAsHook(() =>
-    !talerWithdrawUri
-      ? Promise.reject(undefined)
-      : wxApi.getWithdrawalDetailsForUri({ talerWithdrawUri }),
-  );
-
-  if (!talerWithdrawUri) {
     return (
-      <span>
-        <i18n.Translate>missing withdraw uri</i18n.Translate>
-      </span>
+      <LoadingError
+        title={<i18n.Translate>Could not get exchange</i18n.Translate>}
+        error={state.hook}
+      />
     );
   }
-  if (!uriInfoHook) {
-    return <Loading />;
-  }
-  if (uriInfoHook.hasError) {
+  if (state.status === "loading-info") {
+    if (!state.hook) return <Loading />;
     return (
       <LoadingError
         title={
-          <i18n.Translate>Could not get the info from the URI</i18n.Translate>
+          <i18n.Translate>Could not get info of withdrawal</i18n.Translate>
         }
-        error={uriInfoHook}
+        error={state.hook}
       />
     );
   }
 
-  return (
-    <WithdrawPageWithParsedURI
-      uri={talerWithdrawUri}
-      uriInfo={uriInfoHook.response}
-    />
-  );
+  return <View state={state} />;
 }
diff --git a/packages/taler-wallet-webextension/src/cta/index.stories.ts 
b/packages/taler-wallet-webextension/src/cta/index.stories.ts
index 225b784a..279375c8 100644
--- a/packages/taler-wallet-webextension/src/cta/index.stories.ts
+++ b/packages/taler-wallet-webextension/src/cta/index.stories.ts
@@ -24,5 +24,6 @@ import * as a3 from "./Pay.stories.jsx";
 import * as a4 from "./Refund.stories.jsx";
 import * as a5 from "./Tip.stories.jsx";
 import * as a6 from "./Withdraw.stories.jsx";
+import * as a7 from "./TermsOfServiceSection.stories.js";
 
-export default [a1, a3, a4, a5, a6];
+export default [a1, a3, a4, a5, a6, a7];
diff --git a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts 
b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
index b2d71874..51123d15 100644
--- a/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useAsyncAsHook.ts
@@ -17,10 +17,10 @@ import {
   NotificationType, TalerErrorDetail
 } from "@gnu-taler/taler-util";
 import { TalerError } from "@gnu-taler/taler-wallet-core";
-import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
+import { useEffect, useMemo, useState } from "preact/hooks";
 import * as wxApi from "../wxApi.js";
 
-interface HookOk<T> {
+export interface HookOk<T> {
   hasError: false;
   response: T;
 }
diff --git 
a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts 
b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts
index 25513f57..4893d43f 100644
--- a/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts
+++ b/packages/taler-wallet-webextension/src/hooks/useTalerActionURL.test.ts
@@ -32,30 +32,30 @@ describe('useTalerActionURL hook', () => {
       })
     }
 
-    const { result, waitNextUpdate } = mountHook(useTalerActionURL, ctx)
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(useTalerActionURL, ctx)
 
     {
-      const [url] = result.current!
+      const [url] = getLastResultOrThrow()
       expect(url).undefined;
     }
 
+
     await waitNextUpdate("waiting for useEffect")
 
     {
-      const [url] = result.current!
+      const [url, setDismissed] = getLastResultOrThrow()
       expect(url).equals("asd");
+      setDismissed(true)
     }
 
-    const [, setDismissed] = result.current!
-    setDismissed(true)
-
     await waitNextUpdate("after dismiss")
 
     {
-      const [url] = result.current!
+      const [url] = getLastResultOrThrow()
       if (url !== undefined) throw Error('invalid')
       expect(url).undefined;
     }
 
+    await assertNoPendingUpdate()
   })
 })
\ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx 
b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
index d0ee3b2f..c0e5d063 100644
--- a/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
@@ -103,12 +103,12 @@ export const Multiline = (): VNode => {
   const [value, onChange] = useState("");
   return (
     <Container>
-      {/* <TextField
+      <TextField
         {...{ value, onChange }}
         label="Multiline"
         variant="standard"
         multiline
-      /> */}
+      />
       <TextField
         {...{ value, onChange }}
         label="Max row 4"
@@ -116,13 +116,39 @@ export const Multiline = (): VNode => {
         multiline
         maxRows={4}
       />
-      {/* <TextField
+      <TextField
         {...{ value, onChange }}
         label="Row 10"
         variant="standard"
         multiline
         rows={10}
-      /> */}
+      />
+    </Container>
+  );
+};
+
+export const Select = (): VNode => {
+  const [value, onChange] = useState("");
+  return (
+    <Container>
+      <TextField
+        {...{ value, onChange }}
+        label="Multiline"
+        variant="standard"
+        select
+      />
+      <TextField
+        {...{ value, onChange }}
+        label="Max row 4"
+        variant="standard"
+        select
+      />
+      <TextField
+        {...{ value, onChange }}
+        label="Row 10"
+        variant="standard"
+        select
+      />
     </Container>
   );
 };
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx 
b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
index 8992aa69..180370a0 100644
--- a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
+++ b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
@@ -304,9 +304,9 @@ function getStyleValue(
 
 function debounce(func: any, wait = 166): any {
   let timeout: any;
-  function debounced(...args) {
+  function debounced(...args: any[]): void {
     const later = () => {
-      func.apply(this, args);
+      func.apply({}, args);
     };
     clearTimeout(timeout);
     timeout = setTimeout(later, wait);
@@ -452,7 +452,7 @@ export function TextareaAutoSize({
     renders.current = 0;
   }, [value]);
 
-  const handleChange = (event) => {
+  const handleChange = (event: any): void => {
     renders.current = 0;
 
     if (!isControlled) {
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts 
b/packages/taler-wallet-webextension/src/test-utils.ts
index 39ffbda0..f10e49ac 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -64,23 +64,27 @@ export function renderNodeOrBrowser(Component: any, args: 
any): void {
 
 interface Mounted<T> {
   unmount: () => void;
-  result: { current: T | null };
+  getLastResult: () => T | null;
+  getLastResultOrThrow: () => T;
+  assertNoPendingUpdate: () => void;
   waitNextUpdate: (s?: string) => Promise<void>;
 }
 
 const isNode = typeof window === "undefined"
 
 export function mountHook<T>(callback: () => T, Context?: ({ children }: { 
children: any }) => VNode): Mounted<T> {
-  const result: { current: T | null } = {
-    current: null
-  }
+  // const result: { current: T | null } = {
+  //   current: null
+  // }
+  let lastResult: T | null = null;
+
   const listener: Array<() => void> = []
 
   // component that's going to hold the hook
   function Component(): VNode {
     const hookResult = callback()
     // save the hook result
-    result.current = hookResult
+    lastResult = hookResult
     // notify to everyone waiting for an update and clean the queue
     listener.splice(0, listener.length).forEach(cb => cb())
     return create(Fragment, {})
@@ -119,7 +123,34 @@ export function mountHook<T>(callback: () => T, Context?: 
({ children }: { child
     }
   }
 
+  function getLastResult(): T | null {
+    const copy = lastResult
+    lastResult = null
+    return copy;
+  }
+
+  function getLastResultOrThrow(): T {
+    const r = getLastResult()
+    if (!r) throw Error('there was no last result')
+    return r;
+  }
+
+  async function assertNoPendingUpdate(): Promise<void> {
+    await new Promise((res, rej) => {
+      const tid = setTimeout(() => {
+        res(undefined)
+      }, 10)
+
+      listener.push(() => {
+        clearTimeout(tid)
+        rej(Error(`Expecting no pending result but the hook get updated. Check 
the dependencies of the hooks.`))
+      })
+    })
+
+    const r = getLastResult()
+    if (r) throw Error('There are still pending results. This may happen 
because the hook did a new update but the test didn\'t get the result using 
getLastResult');
+  }
   return {
-    unmount, result, waitNextUpdate
+    unmount, getLastResult, getLastResultOrThrow, waitNextUpdate, 
assertNoPendingUpdate
   }
 }
diff --git a/packages/taler-wallet-webextension/src/utils/index.ts 
b/packages/taler-wallet-webextension/src/utils/index.ts
index b652f275..9181ee5b 100644
--- a/packages/taler-wallet-webextension/src/utils/index.ts
+++ b/packages/taler-wallet-webextension/src/utils/index.ts
@@ -156,34 +156,27 @@ type TermsDocument =
   | TermsDocumentJson
   | TermsDocumentPdf;
 
-interface TermsDocumentXml {
+export interface TermsDocumentXml {
   type: "xml";
   document: Document;
 }
 
-interface TermsDocumentHtml {
+export interface TermsDocumentHtml {
   type: "html";
   href: URL;
 }
 
-interface TermsDocumentPlain {
+export interface TermsDocumentPlain {
   type: "plain";
   content: string;
 }
 
-interface TermsDocumentJson {
+export interface TermsDocumentJson {
   type: "json";
   data: any;
 }
 
-interface TermsDocumentPdf {
+export interface TermsDocumentPdf {
   type: "pdf";
   location: URL;
 }
-
-export function amountToString(text: AmountJson): string {
-  const aj = Amounts.jsonifyAmount(text);
-  const amount = Amounts.stringifyValue(aj);
-  return `${amount} ${aj.currency}`;
-}
-
diff --git 
a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts 
b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts
index e6e699ce..f2bb4a7d 100644
--- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts
@@ -36,174 +36,182 @@ const exchangeListEmpty = {
 
 describe("CreateManualWithdraw states", () => {
   it("should set noExchangeFound when exchange list is empty", () => {
-    const { result } = mountHook(() =>
+    const { getLastResultOrThrow } = mountHook(() =>
       useComponentState(exchangeListEmpty, undefined, undefined),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
+    const { noExchangeFound } = getLastResultOrThrow()
 
-    expect(result.current.noExchangeFound).equal(true)
+    expect(noExchangeFound).equal(true)
   });
 
   it("should set noExchangeFound when exchange list doesn't include selected 
currency", () => {
-    const { result } = mountHook(() =>
+    const { getLastResultOrThrow } = mountHook(() =>
       useComponentState(exchangeListWithARSandUSD, undefined, "COL"),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
+    const { noExchangeFound } = getLastResultOrThrow()
 
-    expect(result.current.noExchangeFound).equal(true)
+    expect(noExchangeFound).equal(true)
   });
 
 
   it("should select the first exchange from the list", () => {
-    const { result } = mountHook(() =>
+    const { getLastResultOrThrow } = mountHook(() =>
       useComponentState(exchangeListWithARSandUSD, undefined, undefined),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
+    const { exchange } = getLastResultOrThrow()
 
-    expect(result.current.exchange.value).equal("url1")
+    expect(exchange.value).equal("url1")
   });
 
   it("should select the first exchange with the selected currency", () => {
-    const { result } = mountHook(() =>
+    const { getLastResultOrThrow } = mountHook(() =>
       useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
+    const { exchange } = getLastResultOrThrow()
 
-    expect(result.current.exchange.value).equal("url2")
+    expect(exchange.value).equal("url2")
   });
 
   it("should change the exchange when currency change", async () => {
-    const { result, waitNextUpdate } = mountHook(() =>
+    const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
       useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
 
-    expect(result.current.exchange.value).equal("url2")
+    {
+      const { exchange, currency } = getLastResultOrThrow()
+
+      expect(exchange.value).equal("url2")
 
-    result.current.currency.onChange("USD")
+      currency.onChange("USD")
+    }
 
     await waitNextUpdate()
 
-    expect(result.current.exchange.value).equal("url1")
+    {
+      const { exchange } = getLastResultOrThrow()
+      expect(exchange.value).equal("url1")
+    }
 
   });
 
   it("should change the currency when exchange change", async () => {
-    const { result, waitNextUpdate } = mountHook(() =>
+    const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
       useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
+    {
+      const { exchange, currency } = getLastResultOrThrow()
 
-    expect(result.current.exchange.value).equal("url2")
-    expect(result.current.currency.value).equal("ARS")
+      expect(exchange.value).equal("url2")
+      expect(currency.value).equal("ARS")
 
-    result.current.exchange.onChange("url1")
+      exchange.onChange("url1")
+    }
 
     await waitNextUpdate()
 
-    expect(result.current.exchange.value).equal("url1")
-    expect(result.current.currency.value).equal("USD")
+    {
+      const { exchange, currency } = getLastResultOrThrow()
+
+      expect(exchange.value).equal("url1")
+      expect(currency.value).equal("USD")
+    }
   });
 
   it("should update parsed amount when amount change", async () => {
-    const { result, waitNextUpdate } = mountHook(() =>
+    const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
       useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
+    {
+      const { amount, parsedAmount } = getLastResultOrThrow()
 
-    expect(result.current.parsedAmount).equal(undefined)
+      expect(parsedAmount).equal(undefined)
 
-    result.current.amount.onInput("12")
+      amount.onInput("12")
+    }
 
     await waitNextUpdate()
 
-    expect(result.current.parsedAmount).deep.equals({
-      value: 12, fraction: 0, currency: "ARS"
-    })
+    {
+      const { parsedAmount } = getLastResultOrThrow()
+
+      expect(parsedAmount).deep.equals({
+        value: 12, fraction: 0, currency: "ARS"
+      })
+    }
   });
 
   it("should have an amount field", async () => {
-    const { result, waitNextUpdate } = mountHook(() =>
+    const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
       useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
-
-    await defaultTestForInputText(waitNextUpdate, () => result.current!.amount)
+    await defaultTestForInputText(waitNextUpdate, () => 
getLastResultOrThrow().amount)
   })
 
   it("should have an exchange selector ", async () => {
-    const { result, waitNextUpdate } = mountHook(() =>
+    const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
       useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
-
-    await defaultTestForInputSelect(waitNextUpdate, () => 
result.current!.exchange)
+    await defaultTestForInputSelect(waitNextUpdate, () => 
getLastResultOrThrow().exchange)
   })
 
   it("should have a currency selector ", async () => {
-    const { result, waitNextUpdate } = mountHook(() =>
+    const { getLastResultOrThrow, waitNextUpdate } = mountHook(() =>
       useComponentState(exchangeListWithARSandUSD, undefined, "ARS"),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
-    }
-
-    await defaultTestForInputSelect(waitNextUpdate, () => 
result.current!.currency)
+    await defaultTestForInputSelect(waitNextUpdate, () => 
getLastResultOrThrow().currency)
   })
 
 });
 
 
 async function defaultTestForInputText(awaiter: () => Promise<void>, getField: 
() => TextFieldHandler): Promise<void> {
-  const initialValue = getField().value;
-  const otherValue = `${initialValue} something else`
-  getField().onInput(otherValue)
+  let nextValue = ''
+  {
+    const field = getField()
+    const initialValue = field.value;
+    nextValue = `${initialValue} something else`
+    field.onInput(nextValue)
+  }
 
   await awaiter()
 
-  expect(getField().value).equal(otherValue)
+  {
+    const field = getField()
+    expect(field.value).equal(nextValue)
+  }
 }
 
 
 async function defaultTestForInputSelect(awaiter: () => Promise<void>, 
getField: () => SelectFieldHandler): Promise<void> {
-  const initialValue = getField().value;
-  const keys = Object.keys(getField().list)
-  const nextIdx = keys.indexOf(initialValue) + 1
-  if (keys.length < nextIdx) {
-    throw new Error('no enough values')
+  let nextValue = ''
+
+  {
+    const field = getField();
+    const initialValue = field.value;
+    const keys = Object.keys(field.list)
+    const nextIdx = keys.indexOf(initialValue) + 1
+    if (keys.length < nextIdx) {
+      throw new Error('no enough values')
+    }
+    nextValue = keys[nextIdx]
+    field.onChange(nextValue)
   }
-  const nextValue = keys[nextIdx]
-  getField().onChange(nextValue)
 
   await awaiter()
 
-  expect(getField().value).equal(nextValue)
+  {
+    const field = getField();
+
+    expect(field.value).equal(nextValue)
+  }
 }
diff --git 
a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx 
b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
index 215aa437..b9d39891 100644
--- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
@@ -21,6 +21,7 @@
  */
 
 import { AmountJson, Amounts } from "@gnu-taler/taler-util";
+import { TalerError } from "@gnu-taler/taler-wallet-core";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { ErrorMessage } from "../components/ErrorMessage.js";
@@ -60,10 +61,17 @@ export interface TextFieldHandler {
   error?: string;
 }
 
+export interface ButtonHandler {
+  onClick: () => Promise<void>;
+  disabled?: boolean;
+  error?: TalerError;
+}
+
 export interface SelectFieldHandler {
   onChange: (value: string) => void;
   error?: string;
   value: string;
+  isDirty?: boolean;
   list: Record<string, string>;
 }
 
@@ -139,17 +147,6 @@ export function useComponentState(
   };
 }
 
-export interface InputHandler {
-  value: string;
-  onInput: (s: string) => void;
-}
-
-export interface SelectInputHandler {
-  list: Record<string, string>;
-  value: string;
-  onChange: (s: string) => void;
-}
-
 export function CreateManualWithdraw({
   initialAmount,
   exchangeUrlWithCurrency,
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts 
b/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts
index 69831cd3..ac4e0ea9 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts
@@ -39,26 +39,26 @@ const someBalance = [{
 
 describe("DepositPage states", () => {
   it("should have status 'no-balance' when balance is empty", () => {
-    const { result } = mountHook(() =>
+    const { getLastResultOrThrow } = mountHook(() =>
       useComponentState(currency, [], [], feeCalculator),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
+    {
+      const { status } = getLastResultOrThrow()
+      expect(status).equal("no-balance")
     }
 
-    expect(result.current.status).equal("no-balance")
   });
 
   it("should have status 'no-accounts' when balance is not empty and accounts 
is empty", () => {
-    const { result } = mountHook(() =>
+    const { getLastResultOrThrow } = mountHook(() =>
       useComponentState(currency, [], someBalance, feeCalculator),
     );
 
-    if (!result.current) {
-      expect.fail("hook didn't render");
+    {
+      const { status } = getLastResultOrThrow()
+      expect(status).equal("no-accounts")
     }
 
-    expect(result.current.status).equal("no-accounts")
   });
 });
\ No newline at end of file
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx 
b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx
index 3fbdadee..a8c6b3c1 100644
--- a/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ManualWithdrawPage.tsx
@@ -85,7 +85,7 @@ export function ManualWithdrawPage({ currency, onCancel }: 
Props): VNode {
       <ReserveCreated
         reservePub={success.response.reservePub}
         paytoURI={success.paytoURI}
-        payto={success.payto}
+        // payto={success.payto}
         exchangeBaseUrl={success.exchangeBaseUrl}
         amount={success.amount}
         onCancel={onCancel}
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
index 4e5595ef..587e24e9 100644
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx
@@ -19,6 +19,7 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
+import { parsePaytoUri } from "@gnu-taler/taler-util";
 import { createExample } from "../test-utils.js";
 import { ReserveCreated as TestedComponent } from "./ReserveCreated.js";
 
@@ -30,8 +31,9 @@ export default {
 
 export const TalerBank = createExample(TestedComponent, {
   reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
-  payto:
+  paytoURI: parsePaytoUri(
     
"payto://x-taler-bank/bank.taler:5882/exchangeminator?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+  ),
   amount: {
     currency: "USD",
     value: 10,
@@ -42,8 +44,9 @@ export const TalerBank = createExample(TestedComponent, {
 
 export const IBAN = createExample(TestedComponent, {
   reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
-  payto:
+  paytoURI: parsePaytoUri(
     
"payto://iban/ASDQWEASDZXCASDQWE?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG",
+  ),
   amount: {
     currency: "USD",
     value: 10,
@@ -54,8 +57,9 @@ export const IBAN = createExample(TestedComponent, {
 
 export const Bitcoin = createExample(TestedComponent, {
   reservePub: "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
-  payto:
+  paytoURI: parsePaytoUri(
     
"payto://bitcoin/bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
+  ),
   amount: {
     currency: "BTC",
     value: 0,
@@ -66,8 +70,9 @@ export const Bitcoin = createExample(TestedComponent, {
 
 export const BitcoinRegTest = createExample(TestedComponent, {
   reservePub: "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
-  payto:
+  paytoURI: parsePaytoUri(
     
"payto://bitcoin/bcrt1q6ps8qs6v8tkqrnru4xqqqa6rfwcx5ufpdfqht4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
+  ),
   amount: {
     currency: "BTC",
     value: 0,
@@ -77,8 +82,9 @@ export const BitcoinRegTest = createExample(TestedComponent, {
 });
 export const BitcoinTest = createExample(TestedComponent, {
   reservePub: "0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
-  payto:
+  paytoURI: parsePaytoUri(
     
"payto://bitcoin/tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00",
+  ),
   amount: {
     currency: "BTC",
     value: 0,
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx 
b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
index 50ab5175..e656393c 100644
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
@@ -3,9 +3,12 @@ import {
   Amounts,
   PaytoUri,
   segwitMinAmount,
+  stringifyPaytoUri,
 } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
+import { Amount } from "../components/Amount.js";
 import { BankDetailsByPaytoType } from 
"../components/BankDetailsByPaytoType.js";
+import { ErrorMessage } from "../components/ErrorMessage.js";
 import { QR } from "../components/QR.js";
 import {
   ButtonDestructive,
@@ -13,11 +16,9 @@ import {
   WarningBox,
 } from "../components/styled/index.js";
 import { useTranslationContext } from "../context/translation.js";
-import { amountToString } from "../utils/index.js";
 export interface Props {
   reservePub: string;
   paytoURI: PaytoUri | undefined;
-  payto: string;
   exchangeBaseUrl: string;
   amount: AmountJson;
   onCancel: () => void;
@@ -26,7 +27,6 @@ export interface Props {
 export function ReserveCreated({
   reservePub,
   paytoURI,
-  payto,
   onCancel,
   exchangeBaseUrl,
   amount,
@@ -34,11 +34,10 @@ export function ReserveCreated({
   const { i18n } = useTranslationContext();
   if (!paytoURI) {
     return (
-      <div>
-        <i18n.Translate>
-          could not parse payto uri from exchange {payto}
-        </i18n.Translate>
-      </div>
+      <ErrorMessage
+        title={<i18n.Translate>Could not parse the payto URI</i18n.Translate>}
+        description={<i18n.Translate>Please check the uri</i18n.Translate>}
+      />
     );
   }
   function TransferDetails(): VNode {
@@ -97,7 +96,7 @@ export function ReserveCreated({
     return (
       <section>
         <BankDetailsByPaytoType
-          amount={amountToString(amount)}
+          amount={<Amount value={amount} />}
           exchangeBaseUrl={exchangeBaseUrl}
           payto={paytoURI}
           subject={reservePub}
@@ -123,7 +122,7 @@ export function ReserveCreated({
         <p>
           <i18n.Translate>
             To complete the process you need to wire{` `}
-            <b>{amountToString(amount)}</b> to the exchange bank account
+            <b>{<Amount value={amount} />}</b> to the exchange bank account
           </i18n.Translate>
         </p>
       </section>
@@ -132,11 +131,11 @@ export function ReserveCreated({
         <p>
           <i18n.Translate>
             Alternative, you can also scan this QR code or open{" "}
-            <a href={payto}>this link</a> if you have a banking app installed
-            that supports RFC 8905
+            <a href={stringifyPaytoUri(paytoURI)}>this link</a> if you have a
+            banking app installed that supports RFC 8905
           </i18n.Translate>
         </p>
-        <QR text={payto} />
+        <QR text={stringifyPaytoUri(paytoURI)} />
       </section>
       <footer>
         <div />
diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx 
b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
index 8fe6f9f3..62e40d02 100644
--- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx
@@ -28,6 +28,7 @@ import { differenceInSeconds } from "date-fns";
 import { ComponentChildren, Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import emptyImg from "../../static/img/empty.png";
+import { Amount } from "../components/Amount.js";
 import { BankDetailsByPaytoType } from 
"../components/BankDetailsByPaytoType.js";
 import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
 import { Loading } from "../components/Loading.js";
@@ -180,12 +181,6 @@ export function TransactionView({
     );
   }
 
-  function amountToString(text: AmountLike): string {
-    const aj = Amounts.jsonifyAmount(text);
-    const amount = Amounts.stringifyValue(aj);
-    return `${amount} ${aj.currency}`;
-  }
-
   if (transaction.type === TransactionType.Withdrawal) {
     const fee = Amounts.sub(
       Amounts.parseOrThrow(transaction.amountRaw),
@@ -229,7 +224,7 @@ export function TransactionView({
           WithdrawalType.ManualTransfer ? (
             <Fragment>
               <BankDetailsByPaytoType
-                amount={amountToString(transaction.amountRaw)}
+                amount={<Amount value={transaction.amountRaw} />}
                 exchangeBaseUrl={transaction.exchangeBaseUrl}
                 payto={parsePaytoUri(
                   transaction.withdrawalDetails.exchangePaytoUris[0],
@@ -247,13 +242,13 @@ export function TransactionView({
               <Part
                 big
                 title={<i18n.Translate>Total withdrawn</i18n.Translate>}
-                text={amountToString(transaction.amountEffective)}
+                text={<Amount value={transaction.amountEffective} />}
                 kind="positive"
               />
               <Part
                 big
                 title={<i18n.Translate>Exchange fee</i18n.Translate>}
-                text={amountToString(fee)}
+                text={<Amount value={fee} />}
                 kind="negative"
               />
             </Fragment>
@@ -284,19 +279,19 @@ export function TransactionView({
               <Part
                 big
                 title={<i18n.Translate>Total withdrawn</i18n.Translate>}
-                text={amountToString(transaction.amountEffective)}
+                text={<Amount value={transaction.amountEffective} />}
                 kind="positive"
               />
               <Part
                 big
                 title={<i18n.Translate>Chosen amount</i18n.Translate>}
-                text={amountToString(transaction.amountRaw)}
+                text={<Amount value={transaction.amountRaw} />}
                 kind="neutral"
               />
               <Part
                 big
                 title={<i18n.Translate>Exchange fee</i18n.Translate>}
-                text={amountToString(fee)}
+                text={<Amount value={fee} />}
                 kind="negative"
               />
             </Fragment>
@@ -306,19 +301,19 @@ export function TransactionView({
             <Part
               big
               title={<i18n.Translate>Total withdrawn</i18n.Translate>}
-              text={amountToString(transaction.amountEffective)}
+              text={<Amount value={transaction.amountEffective} />}
               kind="positive"
             />
             <Part
               big
               title={<i18n.Translate>Chosen amount</i18n.Translate>}
-              text={amountToString(transaction.amountRaw)}
+              text={<Amount value={transaction.amountRaw} />}
               kind="neutral"
             />
             <Part
               big
               title={<i18n.Translate>Exchange fee</i18n.Translate>}
-              text={amountToString(fee)}
+              text={<Amount value={fee} />}
               kind="negative"
             />
           </Fragment>
@@ -355,19 +350,19 @@ export function TransactionView({
         <Part
           big
           title={<i18n.Translate>Total paid</i18n.Translate>}
-          text={amountToString(transaction.amountEffective)}
+          text={<Amount value={transaction.amountEffective} />}
           kind="negative"
         />
         <Part
           big
           title={<i18n.Translate>Purchase amount</i18n.Translate>}
-          text={amountToString(transaction.amountRaw)}
+          text={<Amount value={transaction.amountRaw} />}
           kind="neutral"
         />
         <Part
           big
           title={<i18n.Translate>Fee</i18n.Translate>}
-          text={amountToString(fee)}
+          text={<Amount value={fee} />}
           kind="negative"
         />
         <Part
@@ -441,19 +436,19 @@ export function TransactionView({
         <Part
           big
           title={<i18n.Translate>Total send</i18n.Translate>}
-          text={amountToString(transaction.amountEffective)}
+          text={<Amount value={transaction.amountEffective} />}
           kind="neutral"
         />
         <Part
           big
           title={<i18n.Translate>Deposit amount</i18n.Translate>}
-          text={amountToString(transaction.amountRaw)}
+          text={<Amount value={transaction.amountRaw} />}
           kind="positive"
         />
         <Part
           big
           title={<i18n.Translate>Fee</i18n.Translate>}
-          text={amountToString(fee)}
+          text={<Amount value={fee} />}
           kind="negative"
         />
       </TransactionTemplate>
@@ -478,19 +473,19 @@ export function TransactionView({
         <Part
           big
           title={<i18n.Translate>Total refresh</i18n.Translate>}
-          text={amountToString(transaction.amountEffective)}
+          text={<Amount value={transaction.amountEffective} />}
           kind="negative"
         />
         <Part
           big
           title={<i18n.Translate>Refresh amount</i18n.Translate>}
-          text={amountToString(transaction.amountRaw)}
+          text={<Amount value={transaction.amountRaw} />}
           kind="neutral"
         />
         <Part
           big
           title={<i18n.Translate>Fee</i18n.Translate>}
-          text={amountToString(fee)}
+          text={<Amount value={fee} />}
           kind="negative"
         />
       </TransactionTemplate>
@@ -515,19 +510,19 @@ export function TransactionView({
         <Part
           big
           title={<i18n.Translate>Total tip</i18n.Translate>}
-          text={amountToString(transaction.amountEffective)}
+          text={<Amount value={transaction.amountEffective} />}
           kind="positive"
         />
         <Part
           big
           title={<i18n.Translate>Received amount</i18n.Translate>}
-          text={amountToString(transaction.amountRaw)}
+          text={<Amount value={transaction.amountRaw} />}
           kind="neutral"
         />
         <Part
           big
           title={<i18n.Translate>Fee</i18n.Translate>}
-          text={amountToString(fee)}
+          text={<Amount value={fee} />}
           kind="negative"
         />
       </TransactionTemplate>
@@ -552,19 +547,19 @@ export function TransactionView({
         <Part
           big
           title={<i18n.Translate>Total refund</i18n.Translate>}
-          text={amountToString(transaction.amountEffective)}
+          text={<Amount value={transaction.amountEffective} />}
           kind="positive"
         />
         <Part
           big
           title={<i18n.Translate>Refund amount</i18n.Translate>}
-          text={amountToString(transaction.amountRaw)}
+          text={<Amount value={transaction.amountRaw} />}
           kind="neutral"
         />
         <Part
           big
           title={<i18n.Translate>Fee</i18n.Translate>}
-          text={amountToString(fee)}
+          text={<Amount value={fee} />}
           kind="negative"
         />
         <Part

-- 
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]