gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: add exchange feature


From: gnunet
Subject: [taler-wallet-core] branch master updated: add exchange feature
Date: Mon, 22 Nov 2021 21:34:40 +0100

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 829a59e1 add exchange feature
829a59e1 is described below

commit 829a59e1a24d6a99ce7554d28acfd05f21baeaf8
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Nov 22 17:34:27 2021 -0300

    add exchange feature
---
 .../src/NavigationBar.tsx                          |   1 +
 .../src/components/CheckboxOutlined.tsx            |   8 +-
 .../src/components/styled/index.tsx                |  11 +
 .../src/cta/TermsOfServiceSection.tsx              | 129 ++++
 .../src/cta/Withdraw.stories.tsx                   | 792 +--------------------
 .../taler-wallet-webextension/src/cta/Withdraw.tsx | 255 ++-----
 .../cta/{Withdraw.stories.tsx => termsExample.ts}  | 428 +----------
 .../src/popup/BalancePage.tsx                      |   5 +
 .../src/popup/Settings.stories.tsx                 |   7 +-
 .../src/popup/Settings.tsx                         |  82 +--
 .../src/popupEntryPoint.tsx                        |  10 +
 .../taler-wallet-webextension/src/utils/index.ts   | 162 +++++
 .../src/wallet/BalancePage.tsx                     |  17 +-
 .../src/wallet/CreateManualWithdraw.tsx            |  28 +-
 .../src/wallet/ExchangeAddConfirm.stories.tsx      |  67 ++
 .../src/wallet/ExchangeAddConfirm.tsx              | 152 ++++
 .../src/wallet/ExchangeAddPage.tsx                 |  75 ++
 .../src/wallet/ExchangeAddSetUrl.stories.tsx       |  62 ++
 .../src/wallet/ExchangeSetUrl.tsx                  | 130 ++++
 .../src/wallet/ProviderAddPage.tsx                 |  31 +-
 .../src/wallet/ReserveCreated.tsx                  |   5 +-
 .../src/wallet/Settings.tsx                        |   5 +-
 .../src/walletEntryPoint.tsx                       |   9 +
 23 files changed, 923 insertions(+), 1548 deletions(-)

diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx 
b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index 56704fb5..8dc73efd 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -42,6 +42,7 @@ export enum Pages {
   transaction = "/transaction/:tid",
   provider_detail = "/provider/:pid",
   provider_add = "/provider/add",
+  exchange_add = "/exchange/add",
 
   reset_required = "/reset-required",
   payback = "/payback",
diff --git 
a/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx 
b/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx
index 3b9519f3..c22103a8 100644
--- a/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx
+++ b/packages/taler-wallet-webextension/src/components/CheckboxOutlined.tsx
@@ -48,8 +48,8 @@ export function CheckboxOutlined({
   label,
 }: Props): VNode {
   return (
-    <Outlined>
-      <StyledCheckboxLabel onClick={onToggle}>
+    <StyledCheckboxLabel onClick={onToggle}>
+      <Outlined>
         <span>
           <input
             type="checkbox"
@@ -62,7 +62,7 @@ export function CheckboxOutlined({
           </div>
           <label for={name}>{label}</label>
         </span>
-      </StyledCheckboxLabel>
-    </Outlined>
+      </Outlined>
+    </StyledCheckboxLabel>
   );
 }
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx 
b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index b2ca1380..7cef8789 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -476,6 +476,14 @@ const ButtonVariant = styled(Button)`
   text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
 `;
 
+export const LinkDestructive = styled(Link)`
+  background-color: rgb(202, 60, 60);
+`;
+
+export const LinkPrimary = styled(Link)`
+  color: rgb(66, 184, 221);
+`;
+
 export const ButtonPrimary = styled(ButtonVariant)<{ small?: boolean }>`
   font-size: ${({ small }) => (small ? "small" : "inherit")};
   background-color: rgb(66, 184, 221);
@@ -892,12 +900,14 @@ export const StyledCheckboxLabel = styled.div`
   text-transform: uppercase;
   /* font-weight: bold; */
   text-align: center;
+  cursor: pointer;
   span {
     input {
       display: none;
       opacity: 0;
       width: 1em;
       height: 1em;
+      cursor: pointer;
     }
     div {
       display: inline-grid;
@@ -916,6 +926,7 @@ export const StyledCheckboxLabel = styled.div`
     label {
       padding: 0px;
       font-size: small;
+      cursor: pointer;
     }
   }
 
diff --git 
a/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx 
b/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx
new file mode 100644
index 00000000..5eddde64
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx
@@ -0,0 +1,129 @@
+import { i18n } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { CheckboxOutlined } from "../components/CheckboxOutlined";
+import { ExchangeXmlTos } from "../components/ExchangeToS";
+import {
+  ButtonSuccess,
+  ButtonWarning,
+  LinkSuccess,
+  TermsOfService,
+  WarningBox,
+} from "../components/styled";
+import { TermsState } from "../utils";
+
+interface Props {
+  reviewing: boolean;
+  reviewed: boolean;
+  terms: TermsState;
+  onReview?: (b: boolean) => void;
+  onAccept: (b: boolean) => void;
+}
+export function TermsOfServiceSection({
+  reviewed,
+  reviewing,
+  terms,
+  onAccept,
+  onReview,
+}: Props): VNode {
+  if (!reviewing) {
+    if (!reviewed) {
+      if (!onReview) {
+        return <section>Terms of service status: {terms.status}</section>;
+      }
+      return (
+        <Fragment>
+          {terms.status === "new" && (
+            <section>
+              <ButtonSuccess upperCased onClick={() => onReview(true)}>
+                {i18n.str`Review exchange terms of service`}
+              </ButtonSuccess>
+            </section>
+          )}
+          {terms.status === "changed" && (
+            <section>
+              <ButtonWarning upperCased onClick={() => onReview(true)}>
+                {i18n.str`Review new version of terms of service`}
+              </ButtonWarning>
+            </section>
+          )}
+        </Fragment>
+      );
+    }
+    return (
+      <Fragment>
+        {onReview && (
+          <section>
+            <LinkSuccess upperCased onClick={() => onReview(true)}>
+              {i18n.str`Show terms of service`}
+            </LinkSuccess>
+          </section>
+        )}
+        <section>
+          <CheckboxOutlined
+            name="terms"
+            enabled={reviewed}
+            label={i18n.str`I accept the exchange terms of service`}
+            onToggle={() => {
+              console.log("asdasd", reviewed);
+              onAccept(!reviewed);
+              if (onReview) onReview(false);
+            }}
+          />
+        </section>
+      </Fragment>
+    );
+  }
+  return (
+    <Fragment>
+      {terms.status !== "notfound" && !terms.content && (
+        <section>
+          <WarningBox>
+            The exchange reply with a empty terms of service
+          </WarningBox>
+        </section>
+      )}
+      {terms.status !== "accepted" && terms.content && (
+        <section>
+          {terms.content.type === "xml" && (
+            <TermsOfService>
+              <ExchangeXmlTos doc={terms.content.document} />
+            </TermsOfService>
+          )}
+          {terms.content.type === "plain" && (
+            <div style={{ textAlign: "left" }}>
+              <pre>{terms.content.content}</pre>
+            </div>
+          )}
+          {terms.content.type === "html" && (
+            <iframe src={terms.content.href.toString()} />
+          )}
+          {terms.content.type === "pdf" && (
+            <a href={terms.content.location.toString()} download="tos.pdf">
+              Download Terms of Service
+            </a>
+          )}
+        </section>
+      )}
+      {reviewed && onReview && (
+        <section>
+          <LinkSuccess upperCased onClick={() => onReview(false)}>
+            {i18n.str`Hide terms of service`}
+          </LinkSuccess>
+        </section>
+      )}
+      {terms.status !== "notfound" && (
+        <section>
+          <CheckboxOutlined
+            name="terms"
+            enabled={reviewed}
+            label={i18n.str`I accept the exchange terms of service`}
+            onToggle={() => {
+              onAccept(!reviewed);
+              if (onReview) onReview(false);
+            }}
+          />
+        </section>
+      )}
+    </Fragment>
+  );
+}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
index 54ae19c6..fbbecd6f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
@@ -21,6 +21,7 @@
 
 import { amountFractionalBase } from "@gnu-taler/taler-util";
 import { createExample } from "../test-utils";
+import { termsHtml, termsPdf, termsPlain, termsXml } from "./termsExample";
 import { View as TestedComponent } from "./Withdraw";
 
 export default {
@@ -31,751 +32,6 @@ export default {
   },
 };
 
-const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml"; lang="en">
-<head>
-  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-  <title>Terms Of Service &#8212; Taler Terms of Service</title>
-</head><body>
-  <div>
-    Terms of service
-  </div>
-  <div>
-    A complete separated html with it's own design
-  </div>
-</body>
-</html>
-`;
-const termsPlain = `
-Terms Of Service
-****************
-
-Last Updated: 12.4.2019
-
-Welcome! Taler Systems SA (“we,” “our,” or “us”) provides a payment
-service through our Internet presence (collectively the “Services”).
-Before using our Services, please read the Terms of Service (the
-“Terms” or the “Agreement”) carefully.
-
-
-Overview
-========
-
-This section provides a brief summary of the highlights of this
-Agreement. Please note that when you accept this Agreement, you are
-accepting all of the terms and conditions and not just this section.
-We and possibly other third parties provide Internet services which
-interact with the Taler Wallet’s self-hosted personal payment
-application. When using the Taler Wallet to interact with our
-Services, you are agreeing to our Terms, so please read carefully.
-
-
-Highlights:
------------
-
-   * You are responsible for keeping the data in your Taler Wallet at
-     all times under your control. Any losses arising from you not
-     being in control of your private information are your problem.
-
-   * We will try to transfer funds we hold in escrow for our users to
-     any legal recipient to the best of our ability within the
-     limitations of the law and our implementation. However, the
-     Services offered today are highly experimental and the set of
-     recipients of funds is severely restricted.
-
-   * For our Services, we may charge transaction fees. The specific
-     fee structure is provided based on the Taler protocol and should
-     be shown to you when you withdraw electronic coins using a Taler
-     Wallet. You agree and understand that the Taler protocol allows
-     for the fee structure to change.
-
-   * You agree to not intentionally overwhelm our systems with
-     requests and follow responsible disclosure if you find security
-     issues in our services.
-
-   * We cannot be held accountable for our Services not being
-     available due to circumstances beyond our control. If we modify
-     or terminate our services, we will try to give you the
-     opportunity to recover your funds. However, given the
-     experimental state of the Services today, this may not be
-     possible. You are strongly advised to limit your use of the
-     Service to small-scale experiments expecting total loss of all
-     funds.
-
-These terms outline approved uses of our Services. The Services and
-these Terms are still at an experimental stage. If you have any
-questions or comments related to this Agreement, please send us a
-message to legal@taler-systems.com. If you do not agree to this
-Agreement, you must not use our Services.
-
-
-How you accept this policy
-==========================
-
-By sending funds to us (to top-up your Taler Wallet), you acknowledge
-that you have read, understood, and agreed to these Terms. We reserve
-the right to change these Terms at any time. If you disagree with the
-change, we may in the future offer you with an easy option to recover
-your unspent funds. However, in the current experimental period you
-acknowledge that this feature is not yet available, resulting in your
-funds being lost unless you accept the new Terms. If you continue to
-use our Services other than to recover your unspent funds, your
-continued use of our Services following any such change will signify
-your acceptance to be bound by the then current Terms. Please check
-the effective date above to determine if there have been any changes
-since you have last reviewed these Terms.
-
-
-Services
-========
-
-We will try to transfer funds that we hold in escrow for our users to
-any legal recipient to the best of our ability and within the
-limitations of the law and our implementation. However, the Services
-offered today are highly experimental and the set of recipients of
-funds is severely restricted.  The Taler Wallet can be loaded by
-exchanging fiat currencies against electronic coins. We are providing
-this exchange service. Once your Taler Wallet is loaded with
-electronic coins they can be spent for purchases if the seller is
-accepting Taler as a means of payment. We are not guaranteeing that
-any seller is accepting Taler at all or a particular seller.  The
-seller or recipient of deposits of electronic coins must specify the
-target account, as per the design of the Taler protocol. They are
-responsible for following the protocol and specifying the correct bank
-account, and are solely liable for any losses that may arise from
-specifying the wrong account. We will allow the government to link
-wire transfers to the underlying contract hash. It is the
-responsibility of recipients to preserve the full contracts and to pay
-whatever taxes and charges may be applicable. Technical issues may
-lead to situations where we are unable to make transfers at all or
-lead to incorrect transfers that cannot be reversed. We will only
-refuse to execute transfers if the transfers are prohibited by a
-competent legal authority and we are ordered to do so.
-
-
-Fees
-====
-
-You agree to pay the fees for exchanges and withdrawals completed via
-the Taler Wallet ("Fees") as defined by us, which we may change from
-time to time. With the exception of wire transfer fees, Taler
-transaction fees are set for any electronic coin at the time of
-withdrawal and fixed throughout the validity period of the respective
-electronic coin. Your wallet should obtain and display applicable fees
-when withdrawing funds. Fees for coins obtained as change may differ
-from the fees applicable to the original coin. Wire transfer fees that
-are independent from electronic coins may change annually.  You
-authorize us to charge or deduct applicable fees owed in connection
-with deposits, exchanges and withdrawals following the rules of the
-Taler protocol. We reserve the right to provide different types of
-rewards to users either in the form of discount for our Services or in
-any other form at our discretion and without prior notice to you.
-
-
-Eligibility
-===========
-
-To be eligible to use our Services, you must be able to form legally
-binding contracts or have the permission of your legal guardian. By
-using our Services, you represent and warrant that you meet all
-eligibility requirements that we outline in these Terms.
-
-
-Financial self-responsibility
-=============================
-
-You will be responsible for maintaining the availability, integrity
-and confidentiality of the data stored in your wallet. When you setup
-a Taler Wallet, you are strongly advised to follow the precautionary
-measures offered by the software to minimize the chances to losse
-access to or control over your Wallet data. We will not be liable for
-any loss or damage arising from your failure to comply with this
-paragraph.
-
-
-Copyrights and trademarks
-=========================
-
-The Taler Wallet is released under the terms of the GNU General Public
-License (GNU GPL). You have the right to access, use, and share the
-Taler Wallet, in modified or unmodified form. However, the GPL is a
-strong copyleft license, which means that any derivative works must be
-distributed under the same license terms as the original software. If
-you have any questions, you should review the GNU GPL’s full terms and
-conditions at https://www.gnu.org/licenses/gpl-3.0.en.html.  “Taler”
-itself is a trademark of Taler Systems SA. You are welcome to use the
-name in relation to processing payments using the Taler protocol,
-assuming your use is compatible with an official release from the GNU
-Project that is not older than two years.
-
-
-Your use of our services
-========================
-
-When using our Services, you agree to not take any action that
-intentionally imposes an unreasonable load on our infrastructure. If
-you find security problems in our Services, you agree to first report
-them to security@taler-systems.com and grant us the right to publish
-your report. We warrant that we will ourselves publicly disclose any
-issues reported within 3 months, and that we will not prosecute anyone
-reporting security issues if they did not exploit the issue beyond a
-proof-of-concept, and followed the above responsible disclosure
-practice.
-
-
-Limitation of liability & disclaimer of warranties
-==================================================
-
-You understand and agree that we have no control over, and no duty to
-take any action regarding: Failures, disruptions, errors, or delays in
-processing that you may experience while using our Services; The risk
-of failure of hardware, software, and Internet connections; The risk
-of malicious software being introduced or found in the software
-underlying the Taler Wallet; The risk that third parties may obtain
-unauthorized access to information stored within your Taler Wallet,
-including, but not limited to your Taler Wallet coins or backup
-encryption keys.  You release us from all liability related to any
-losses, damages, or claims arising from:
-
-1. user error such as forgotten passwords, incorrectly constructed
-   transactions;
-
-2. server failure or data loss;
-
-3. unauthorized access to the Taler Wallet application;
-
-4. bugs or other errors in the Taler Wallet software; and
-
-5. any unauthorized third party activities, including, but not limited
-   to, the use of viruses, phishing, brute forcing, or other means of
-   attack against the Taler Wallet. We make no representations
-   concerning any Third Party Content contained in or accessed through
-   our Services.
-
-Any other terms, conditions, warranties, or representations associated
-with such content, are solely between you and such organizations
-and/or individuals.
-
-
-Limitation of liability
-=======================
-
-To the fullest extent permitted by applicable law, in no event will we
-or any of our officers, directors, representatives, agents, servants,
-counsel, employees, consultants, lawyers, and other personnel
-authorized to act, acting, or purporting to act on our behalf
-(collectively the “Taler Parties”) be liable to you under contract,
-tort, strict liability, negligence, or any other legal or equitable
-theory, for:
-
-1. any lost profits, data loss, cost of procurement of substitute
-   goods or services, or direct, indirect, incidental, special,
-   punitive, compensatory, or consequential damages of any kind
-   whatsoever resulting from:
-
-   1. your use of, or conduct in connection with, our services;
-
-   2. any unauthorized use of your wallet and/or private key due to
-      your failure to maintain the confidentiality of your wallet;
-
-   3. any interruption or cessation of transmission to or from the
-      services; or
-
-   4. any bugs, viruses, trojan horses, or the like that are found in
-      the Taler Wallet software or that may be transmitted to or
-      through our services by any third party (regardless of the
-      source of origination), or
-
-2. any direct damages.
-
-These limitations apply regardless of legal theory, whether based on
-tort, strict liability, breach of contract, breach of warranty, or any
-other legal theory, and whether or not we were advised of the
-possibility of such damages. Some jurisdictions do not allow the
-exclusion or limitation of liability for consequential or incidental
-damages, so the above limitation may not apply to you.
-
-
-Warranty disclaimer
-===================
-
-Our services are provided "as is" and without warranty of any kind. To
-the maximum extent permitted by law, we disclaim all representations
-and warranties, express or implied, relating to the services and
-underlying software or any content on the services, whether provided
-or owned by us or by any third party, including without limitation,
-warranties of merchantability, fitness for a particular purpose,
-title, non-infringement, freedom from computer virus, and any implied
-warranties arising from course of dealing, course of performance, or
-usage in trade, all of which are expressly disclaimed. In addition, we
-do not represent or warrant that the content accessible via the
-services is accurate, complete, available, current, free of viruses or
-other harmful components, or that the results of using the services
-will meet your requirements. Some states do not allow the disclaimer
-of implied warranties, so the foregoing disclaimers may not apply to
-you. This paragraph gives you specific legal rights and you may also
-have other legal rights that vary from state to state.
-
-
-Indemnity
-=========
-
-To the extent permitted by applicable law, you agree to defend,
-indemnify, and hold harmless the Taler Parties from and against any
-and all claims, damages, obligations, losses, liabilities, costs or
-debt, and expenses (including, but not limited to, attorney’s fees)
-arising from: (a) your use of and access to the Services; (b) any
-feedback or submissions you provide to us concerning the Taler Wallet;
-(c) your violation of any term of this Agreement; or (d) your
-violation of any law, rule, or regulation, or the rights of any third
-party.
-
-
-Time limitation on claims
-=========================
-
-You agree that any claim you may have arising out of or related to
-your relationship with us must be filed within one year after such
-claim arises, otherwise, your claim in permanently barred.
-
-
-Governing law
-=============
-
-No matter where you’re located, the laws of Switzerland will govern
-these Terms. If any provisions of these Terms are inconsistent with
-any applicable law, those provisions will be superseded or modified
-only to the extent such provisions are inconsistent. The parties agree
-to submit to the ordinary courts in Zurich, Switzerland for exclusive
-jurisdiction of any dispute arising out of or related to your use of
-the Services or your breach of these Terms.
-
-
-Termination
-===========
-
-In the event of termination concerning your use of our Services, your
-obligations under this Agreement will still continue.
-
-
-Discontinuance of services
-==========================
-
-We may, in our sole discretion and without cost to you, with or
-without prior notice, and at any time, modify or discontinue,
-temporarily or permanently, any portion of our Services. We will use
-the Taler protocol’s provisions to notify Wallets if our Services are
-to be discontinued. It is your responsibility to ensure that the Taler
-Wallet is online at least once every three months to observe these
-notifications. We shall not be held responsible or liable for any loss
-of funds in the event that we discontinue or depreciate the Services
-and your Taler Wallet fails to transfer out the coins within a three
-months notification period.
-
-
-No waiver
-=========
-
-Our failure to exercise or delay in exercising any right, power, or
-privilege under this Agreement shall not operate as a waiver; nor
-shall any single or partial exercise of any right, power, or privilege
-preclude any other or further exercise thereof.
-
-
-Severability
-============
-
-If it turns out that any part of this Agreement is invalid, void, or
-for any reason unenforceable, that term will be deemed severable and
-limited or eliminated to the minimum extent necessary.
-
-
-Force majeure
-=============
-
-We shall not be held liable for any delays, failure in performance, or
-interruptions of service which result directly or indirectly from any
-cause or condition beyond our reasonable control, including but not
-limited to: any delay or failure due to any act of God, act of civil
-or military authorities, act of terrorism, civil disturbance, war,
-strike or other labor dispute, fire, interruption in
-telecommunications or Internet services or network provider services,
-failure of equipment and/or software, other catastrophe, or any other
-occurrence which is beyond our reasonable control and shall not affect
-the validity and enforceability of any remaining provisions.
-
-
-Assignment
-==========
-
-You agree that we may assign any of our rights and/or transfer, sub-
-contract, or delegate any of our obligations under these Terms.
-
-
-Entire agreement
-================
-
-This Agreement sets forth the entire understanding and agreement as to
-the subject matter hereof and supersedes any and all prior
-discussions, agreements, and understandings of any kind (including,
-without limitation, any prior versions of this Agreement) and every
-nature between us. Except as provided for above, any modification to
-this Agreement must be in writing and must be signed by both parties.
-
-
-Questions or comments
-=====================
-
-We welcome comments, questions, concerns, or suggestions. Please send
-us a message on our contact page at legal@taler-systems.com.
-
-`;
-
-const termsXml = `<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE document PUBLIC "+//IDN docutils.sourceforge.net//DTD Docutils 
Generic//EN//XML" "http://docutils.sourceforge.net/docs/ref/docutils.dtd";>
-<!-- Generated by Docutils 0.14 -->
-<document source="/home/grothoff/research/taler/exchange/contrib/tos/tos.rst">
-    <section ids="terms-of-service" names="terms\ of\ service">
-        <title>Terms Of Service</title>
-        <paragraph>Last Updated: 12.4.2019</paragraph>
-        <paragraph>Welcome! Taler Systems SA (“we,” “our,” or “us”) provides a 
payment service
-            through our Internet presence (collectively the “Services”). 
Before using our
-            Services, please read the Terms of Service (the “Terms” or the 
“Agreement”)
-            carefully.</paragraph>
-        <section ids="overview" names="overview">
-            <title>Overview</title>
-            <paragraph>This section provides a brief summary of the highlights 
of this
-                Agreement. Please note that when you accept this Agreement, 
you are accepting
-                all of the terms and conditions and not just this section. We 
and possibly
-                other third parties provide Internet services which interact 
with the Taler
-                Wallet’s self-hosted personal payment application. When using 
the Taler Wallet
-                to interact with our Services, you are agreeing to our Terms, 
so please read
-                carefully.</paragraph>
-            <section ids="highlights" names="highlights:">
-                <title>Highlights:</title>
-                <block_quote>
-                    <bullet_list bullet="•">
-                        <list_item>
-                            <paragraph>You are responsible for keeping the 
data in your Taler Wallet at all times
-                                under your control. Any losses arising from 
you not being in control of
-                                your private information are your 
problem.</paragraph>
-                        </list_item>
-                        <list_item>
-                            <paragraph>We will try to transfer funds we hold 
in escrow for our users to any legal
-                                recipient to the best of our ability within 
the limitations of the law and
-                                our implementation. However, the Services 
offered today are highly
-                                experimental and the set of recipients of 
funds is severely restricted.</paragraph>
-                        </list_item>
-                        <list_item>
-                            <paragraph>For our Services, we may charge 
transaction fees. The specific fee structure
-                                is provided based on the Taler protocol and 
should be shown to you when you
-                                withdraw electronic coins using a Taler 
Wallet. You agree and understand
-                                that the Taler protocol allows for the fee 
structure to change.</paragraph>
-                        </list_item>
-                        <list_item>
-                            <paragraph>You agree to not intentionally 
overwhelm our systems with requests and
-                                follow responsible disclosure if you find 
security issues in our services.</paragraph>
-                        </list_item>
-                        <list_item>
-                            <paragraph>We cannot be held accountable for our 
Services not being available due to
-                                circumstances beyond our control. If we modify 
or terminate our services,
-                                we will try to give you the opportunity to 
recover your funds. However,
-                                given the experimental state of the Services 
today, this may not be
-                                possible. You are strongly advised to limit 
your use of the Service
-                                to small-scale experiments expecting total 
loss of all funds.</paragraph>
-                        </list_item>
-                    </bullet_list>
-                </block_quote>
-                <paragraph>These terms outline approved uses of our Services. 
The Services and these
-                    Terms are still at an experimental stage. If you have any 
questions or
-                    comments related to this Agreement, please send us a 
message to
-                    <reference 
refuri="mailto:legal@taler-systems.com";>legal@taler-systems.com</reference>. If 
you do not agree to this Agreement, you must not
-                    use our Services.</paragraph>
-            </section>
-        </section>
-        <section ids="how-you-accept-this-policy" names="how\ you\ accept\ 
this\ policy">
-            <title>How you accept this policy</title>
-            <paragraph>By sending funds to us (to top-up your Taler Wallet), 
you acknowledge that you
-                have read, understood, and agreed to these Terms. We reserve 
the right to
-                change these Terms at any time. If you disagree with the 
change, we may in the
-                future offer you with an easy option to recover your unspent 
funds. However,
-                in the current experimental period you acknowledge that this 
feature is not
-                yet available, resulting in your funds being lost unless you 
accept the new
-                Terms. If you continue to use our Services other than to 
recover your unspent
-                funds, your continued use of our Services following any such 
change will
-                signify your acceptance to be bound by the then current Terms. 
Please check
-                the effective date above to determine if there have been any 
changes since you
-                have last reviewed these Terms.</paragraph>
-        </section>
-        <section ids="services" names="services">
-            <title>Services</title>
-            <paragraph>We will try to transfer funds that we hold in escrow 
for our users to any
-                legal recipient to the best of our ability and within the 
limitations of the
-                law and our implementation. However, the Services offered 
today are highly
-                experimental and the set of recipients of funds is severely 
restricted.  The
-                Taler Wallet can be loaded by exchanging fiat currencies 
against electronic
-                coins. We are providing this exchange service. Once your Taler 
Wallet is
-                loaded with electronic coins they can be spent for purchases 
if the seller is
-                accepting Taler as a means of payment. We are not guaranteeing 
that any seller
-                is accepting Taler at all or a particular seller.  The seller 
or recipient of
-                deposits of electronic coins must specify the target account, 
as per the
-                design of the Taler protocol. They are responsible for 
following the protocol
-                and specifying the correct bank account, and are solely liable 
for any losses
-                that may arise from specifying the wrong account. We will 
allow the government
-                to link wire transfers to the underlying contract hash. It is 
the
-                responsibility of recipients to preserve the full contracts 
and to pay
-                whatever taxes and charges may be applicable. Technical issues 
may lead to
-                situations where we are unable to make transfers at all or 
lead to incorrect
-                transfers that cannot be reversed. We will only refuse to 
execute transfers if
-                the transfers are prohibited by a competent legal authority 
and we are ordered
-                to do so.</paragraph>
-        </section>
-        <section ids="fees" names="fees">
-            <title>Fees</title>
-            <paragraph>You agree to pay the fees for exchanges and withdrawals 
completed via the
-                Taler Wallet (“Fees”) as defined by us, which we may change 
from time to
-                time. With the exception of wire transfer fees, Taler 
transaction fees are set
-                for any electronic coin at the time of withdrawal and fixed 
throughout the
-                validity period of the respective electronic coin. Your wallet 
should obtain
-                and display applicable fees when withdrawing funds. Fees for 
coins obtained as
-                change may differ from the fees applicable to the original 
coin. Wire transfer
-                fees that are independent from electronic coins may change 
annually.  You
-                authorize us to charge or deduct applicable fees owed in 
connection with
-                deposits, exchanges and withdrawals following the rules of the 
Taler protocol.
-                We reserve the right to provide different types of rewards to 
users either in
-                the form of discount for our Services or in any other form at 
our discretion
-                and without prior notice to you.</paragraph>
-        </section>
-        <section ids="eligibility" names="eligibility">
-            <title>Eligibility</title>
-            <paragraph>To be eligible to use our Services, you must be able to 
form legally binding
-                contracts or have the permission of your legal guardian. By 
using our
-                Services, you represent and warrant that you meet all 
eligibility requirements
-                that we outline in these Terms.</paragraph>
-        </section>
-        <section ids="financial-self-responsibility" names="financial\ 
self-responsibility">
-            <title>Financial self-responsibility</title>
-            <paragraph>You will be responsible for maintaining the 
availability, integrity and
-                confidentiality of the data stored in your wallet. When you 
setup a Taler
-                Wallet, you are strongly advised to follow the precautionary 
measures offered
-                by the software to minimize the chances to losse access to or 
control over
-                your Wallet data. We will not be liable for any loss or damage 
arising from
-                your failure to comply with this paragraph.</paragraph>
-        </section>
-        <section ids="copyrights-and-trademarks" names="copyrights\ and\ 
trademarks">
-            <title>Copyrights and trademarks</title>
-            <paragraph>The Taler Wallet is released under the terms of the GNU 
General Public License
-                (GNU GPL). You have the right to access, use, and share the 
Taler Wallet, in
-                modified or unmodified form. However, the GPL is a strong 
copyleft license,
-                which means that any derivative works must be distributed 
under the same
-                license terms as the original software. If you have any 
questions, you should
-                review the GNU GPL’s full terms and conditions at
-                <reference 
refuri="https://www.gnu.org/licenses/gpl-3.0.en.html";>https://www.gnu.org/licenses/gpl-3.0.en.html</reference>.
  “Taler” itself is a trademark
-                of Taler Systems SA. You are welcome to use the name in 
relation to processing
-                payments using the Taler protocol, assuming your use is 
compatible with an
-                official release from the GNU Project that is not older than 
two years.</paragraph>
-        </section>
-        <section ids="your-use-of-our-services" names="your\ use\ of\ our\ 
services">
-            <title>Your use of our services</title>
-            <paragraph>When using our Services, you agree to not take any 
action that intentionally
-                imposes an unreasonable load on our infrastructure. If you 
find security
-                problems in our Services, you agree to first report them to
-                <reference 
refuri="mailto:security@taler-systems.com";>security@taler-systems.com</reference>
 and grant us the right to publish your report. We
-                warrant that we will ourselves publicly disclose any issues 
reported within 3
-                months, and that we will not prosecute anyone reporting 
security issues if
-                they did not exploit the issue beyond a proof-of-concept, and 
followed the
-                above responsible disclosure practice.</paragraph>
-        </section>
-        <section ids="limitation-of-liability-disclaimer-of-warranties" 
names="limitation\ of\ liability\ &amp;\ disclaimer\ of\ warranties">
-            <title>Limitation of liability &amp; disclaimer of 
warranties</title>
-            <paragraph>You understand and agree that we have no control over, 
and no duty to take any
-                action regarding: Failures, disruptions, errors, or delays in 
processing that
-                you may experience while using our Services; The risk of 
failure of hardware,
-                software, and Internet connections; The risk of malicious 
software being
-                introduced or found in the software underlying the Taler 
Wallet; The risk that
-                third parties may obtain unauthorized access to information 
stored within your
-                Taler Wallet, including, but not limited to your Taler Wallet 
coins or backup
-                encryption keys.  You release us from all liability related to 
any losses,
-                damages, or claims arising from:</paragraph>
-            <enumerated_list enumtype="loweralpha" prefix="(" suffix=")">
-                <list_item>
-                    <paragraph>user error such as forgotten passwords, 
incorrectly constructed
-                        transactions;</paragraph>
-                </list_item>
-                <list_item>
-                    <paragraph>server failure or data loss;</paragraph>
-                </list_item>
-                <list_item>
-                    <paragraph>unauthorized access to the Taler Wallet 
application;</paragraph>
-                </list_item>
-                <list_item>
-                    <paragraph>bugs or other errors in the Taler Wallet 
software; and</paragraph>
-                </list_item>
-                <list_item>
-                    <paragraph>any unauthorized third party activities, 
including, but not limited to,
-                        the use of viruses, phishing, brute forcing, or other 
means of attack
-                        against the Taler Wallet. We make no representations 
concerning any
-                        Third Party Content contained in or accessed through 
our Services.</paragraph>
-                </list_item>
-            </enumerated_list>
-            <paragraph>Any other terms, conditions, warranties, or 
representations associated with
-                such content, are solely between you and such organizations 
and/or
-                individuals.</paragraph>
-        </section>
-        <section ids="limitation-of-liability" names="limitation\ of\ 
liability">
-            <title>Limitation of liability</title>
-            <paragraph>To the fullest extent permitted by applicable law, in 
no event will we or any
-                of our officers, directors, representatives, agents, servants, 
counsel,
-                employees, consultants, lawyers, and other personnel 
authorized to act,
-                acting, or purporting to act on our behalf (collectively the 
“Taler Parties”)
-                be liable to you under contract, tort, strict liability, 
negligence, or any
-                other legal or equitable theory, for:</paragraph>
-            <enumerated_list enumtype="loweralpha" prefix="(" suffix=")">
-                <list_item>
-                    <paragraph>any lost profits, data loss, cost of 
procurement of substitute goods or
-                        services, or direct, indirect, incidental, special, 
punitive, compensatory,
-                        or consequential damages of any kind whatsoever 
resulting from:</paragraph>
-                </list_item>
-            </enumerated_list>
-            <block_quote>
-                <enumerated_list enumtype="lowerroman" prefix="(" suffix=")">
-                    <list_item>
-                        <paragraph>your use of, or conduct in connection with, 
our services;</paragraph>
-                    </list_item>
-                    <list_item>
-                        <paragraph>any unauthorized use of your wallet and/or 
private key due to your
-                            failure to maintain the confidentiality of your 
wallet;</paragraph>
-                    </list_item>
-                    <list_item>
-                        <paragraph>any interruption or cessation of 
transmission to or from the services; or</paragraph>
-                    </list_item>
-                    <list_item>
-                        <paragraph>any bugs, viruses, trojan horses, or the 
like that are found in the Taler
-                            Wallet software or that may be transmitted to or 
through our services by
-                            any third party (regardless of the source of 
origination), or</paragraph>
-                    </list_item>
-                </enumerated_list>
-            </block_quote>
-            <enumerated_list enumtype="loweralpha" prefix="(" start="2" 
suffix=")">
-                <list_item>
-                    <paragraph>any direct damages.</paragraph>
-                </list_item>
-            </enumerated_list>
-            <paragraph>These limitations apply regardless of legal theory, 
whether based on tort,
-                strict liability, breach of contract, breach of warranty, or 
any other legal
-                theory, and whether or not we were advised of the possibility 
of such
-                damages. Some jurisdictions do not allow the exclusion or 
limitation of
-                liability for consequential or incidental damages, so the 
above limitation may
-                not apply to you.</paragraph>
-        </section>
-        <section ids="warranty-disclaimer" names="warranty\ disclaimer">
-            <title>Warranty disclaimer</title>
-            <paragraph>Our services are provided “as is” and without warranty 
of any kind. To the
-                maximum extent permitted by law, we disclaim all 
representations and
-                warranties, express or implied, relating to the services and 
underlying
-                software or any content on the services, whether provided or 
owned by us or by
-                any third party, including without limitation, warranties of 
merchantability,
-                fitness for a particular purpose, title, non-infringement, 
freedom from
-                computer virus, and any implied warranties arising from course 
of dealing,
-                course of performance, or usage in trade, all of which are 
expressly
-                disclaimed. In addition, we do not represent or warrant that 
the content
-                accessible via the services is accurate, complete, available, 
current, free of
-                viruses or other harmful components, or that the results of 
using the services
-                will meet your requirements. Some states do not allow the 
disclaimer of
-                implied warranties, so the foregoing disclaimers may not apply 
to you. This
-                paragraph gives you specific legal rights and you may also 
have other legal
-                rights that vary from state to state.</paragraph>
-        </section>
-        <section ids="indemnity" names="indemnity">
-            <title>Indemnity</title>
-            <paragraph>To the extent permitted by applicable law, you agree to 
defend, indemnify, and
-                hold harmless the Taler Parties from and against any and all 
claims, damages,
-                obligations, losses, liabilities, costs or debt, and expenses 
(including, but
-                not limited to, attorney’s fees) arising from: (a) your use of 
and access to
-                the Services; (b) any feedback or submissions you provide to 
us concerning the
-                Taler Wallet; (c) your violation of any term of this 
Agreement; or (d) your
-                violation of any law, rule, or regulation, or the rights of 
any third party.</paragraph>
-        </section>
-        <section ids="time-limitation-on-claims" names="time\ limitation\ on\ 
claims">
-            <title>Time limitation on claims</title>
-            <paragraph>You agree that any claim you may have arising out of or 
related to your
-                relationship with us must be filed within one year after such 
claim arises,
-                otherwise, your claim in permanently barred.</paragraph>
-        </section>
-        <section ids="governing-law" names="governing\ law">
-            <title>Governing law</title>
-            <paragraph>No matter where you’re located, the laws of Switzerland 
will govern these
-                Terms. If any provisions of these Terms are inconsistent with 
any applicable
-                law, those provisions will be superseded or modified only to 
the extent such
-                provisions are inconsistent. The parties agree to submit to 
the ordinary
-                courts in Zurich, Switzerland for exclusive jurisdiction of 
any dispute
-                arising out of or related to your use of the Services or your 
breach of these
-                Terms.</paragraph>
-        </section>
-        <section ids="termination" names="termination">
-            <title>Termination</title>
-            <paragraph>In the event of termination concerning your use of our 
Services, your
-                obligations under this Agreement will still 
continue.</paragraph>
-        </section>
-        <section ids="discontinuance-of-services" names="discontinuance\ of\ 
services">
-            <title>Discontinuance of services</title>
-            <paragraph>We may, in our sole discretion and without cost to you, 
with or without prior
-                notice, and at any time, modify or discontinue, temporarily or 
permanently,
-                any portion of our Services. We will use the Taler protocol’s 
provisions to
-                notify Wallets if our Services are to be discontinued. It is 
your
-                responsibility to ensure that the Taler Wallet is online at 
least once every
-                three months to observe these notifications. We shall not be 
held responsible
-                or liable for any loss of funds in the event that we 
discontinue or depreciate
-                the Services and your Taler Wallet fails to transfer out the 
coins within a
-                three months notification period.</paragraph>
-        </section>
-        <section ids="no-waiver" names="no\ waiver">
-            <title>No waiver</title>
-            <paragraph>Our failure to exercise or delay in exercising any 
right, power, or privilege
-                under this Agreement shall not operate as a waiver; nor shall 
any single or
-                partial exercise of any right, power, or privilege preclude 
any other or
-                further exercise thereof.</paragraph>
-        </section>
-        <section ids="severability" names="severability">
-            <title>Severability</title>
-            <paragraph>If it turns out that any part of this Agreement is 
invalid, void, or for any
-                reason unenforceable, that term will be deemed severable and 
limited or
-                eliminated to the minimum extent necessary.</paragraph>
-        </section>
-        <section ids="force-majeure" names="force\ majeure">
-            <title>Force majeure</title>
-            <paragraph>We shall not be held liable for any delays, failure in 
performance, or
-                interruptions of service which result directly or indirectly 
from any cause or
-                condition beyond our reasonable control, including but not 
limited to: any
-                delay or failure due to any act of God, act of civil or 
military authorities,
-                act of terrorism, civil disturbance, war, strike or other 
labor dispute, fire,
-                interruption in telecommunications or Internet services or 
network provider
-                services, failure of equipment and/or software, other 
catastrophe, or any
-                other occurrence which is beyond our reasonable control and 
shall not affect
-                the validity and enforceability of any remaining 
provisions.</paragraph>
-        </section>
-        <section ids="assignment" names="assignment">
-            <title>Assignment</title>
-            <paragraph>You agree that we may assign any of our rights and/or 
transfer, sub-contract,
-                or delegate any of our obligations under these 
Terms.</paragraph>
-        </section>
-        <section ids="entire-agreement" names="entire\ agreement">
-            <title>Entire agreement</title>
-            <paragraph>This Agreement sets forth the entire understanding and 
agreement as to the
-                subject matter hereof and supersedes any and all prior 
discussions,
-                agreements, and understandings of any kind (including, without 
limitation, any
-                prior versions of this Agreement) and every nature between us. 
Except as
-                provided for above, any modification to this Agreement must be 
in writing and
-                must be signed by both parties.</paragraph>
-        </section>
-        <section ids="questions-or-comments" names="questions\ or\ comments">
-            <title>Questions or comments</title>
-            <paragraph>We welcome comments, questions, concerns, or 
suggestions. Please send us a
-                message on our contact page at <reference 
refuri="mailto:legal@taler-systems.com";>legal@taler-systems.com</reference>.</paragraph>
-        </section>
-    </section>
-</document>
-`;
-
 export const NewTerms = createExample(TestedComponent, {
   knownExchanges: [
     {
@@ -805,11 +61,12 @@ export const NewTerms = createExample(TestedComponent, {
     null;
   },
   terms: {
-    value: {
+    content: {
       type: "xml",
       document: new DOMParser().parseFromString(termsXml, "text/xml"),
     },
     status: "new",
+    version: "",
   },
 });
 
@@ -842,11 +99,12 @@ export const TermsReviewingPLAIN = 
createExample(TestedComponent, {
     null;
   },
   terms: {
-    value: {
+    content: {
       type: "plain",
       content: termsPlain,
     },
     status: "new",
+    version: "",
   },
   reviewing: true,
 });
@@ -880,32 +138,18 @@ export const TermsReviewingHTML = 
createExample(TestedComponent, {
     null;
   },
   terms: {
-    value: {
+    content: {
       type: "html",
       href: new URL(
         `data:text/html;base64,${Buffer.from(termsHtml).toString("base64")}`,
       ),
     },
+    version: "",
     status: "new",
   },
   reviewing: true,
 });
 
-const termsPdf = `
-%PDF-1.2 
-9 0 obj << >> 
-stream
-BT/ 9 Tf(This is the Exchange TERMS OF SERVICE)' ET
-endstream
-endobj
-4 0 obj << /Type /Page /Parent 5 0 R /Contents 9 0 R >> endobj 
-5 0 obj << /Kids [4 0 R ] /Count 1 /Type /Pages /MediaBox [ 0 0 180 20 ] >> 
endobj
-3 0 obj << /Pages 5 0 R /Type /Catalog >> endobj
-trailer
-<< /Root 3 0 R >>
-%%EOF
-`;
-
 export const TermsReviewingPDF = createExample(TestedComponent, {
   knownExchanges: [
     {
@@ -935,13 +179,14 @@ export const TermsReviewingPDF = 
createExample(TestedComponent, {
     null;
   },
   terms: {
-    value: {
+    content: {
       type: "pdf",
       location: new URL(
         `data:text/html;base64,${Buffer.from(termsPdf).toString("base64")}`,
       ),
     },
     status: "new",
+    version: "",
   },
   reviewing: true,
 });
@@ -975,11 +220,12 @@ export const TermsReviewingXML = 
createExample(TestedComponent, {
     null;
   },
   terms: {
-    value: {
+    content: {
       type: "xml",
       document: new DOMParser().parseFromString(termsXml, "text/xml"),
     },
     status: "new",
+    version: "",
   },
   reviewing: true,
 });
@@ -1012,11 +258,12 @@ export const NewTermsAccepted = 
createExample(TestedComponent, {
     null;
   },
   terms: {
-    value: {
+    content: {
       type: "xml",
       document: new DOMParser().parseFromString(termsXml, "text/xml"),
     },
     status: "new",
+    version: "",
   },
   reviewed: true,
 });
@@ -1050,10 +297,11 @@ export const TermsShowAgainXML = 
createExample(TestedComponent, {
     null;
   },
   terms: {
-    value: {
+    content: {
       type: "xml",
       document: new DOMParser().parseFromString(termsXml, "text/xml"),
     },
+    version: "",
     status: "new",
   },
   reviewed: true,
@@ -1089,10 +337,11 @@ export const TermsChanged = 
createExample(TestedComponent, {
     null;
   },
   terms: {
-    value: {
+    content: {
       type: "xml",
       document: new DOMParser().parseFromString(termsXml, "text/xml"),
     },
+    version: "",
     status: "changed",
   },
 });
@@ -1126,7 +375,9 @@ export const TermsNotFound = 
createExample(TestedComponent, {
     null;
   },
   terms: {
+    content: undefined,
     status: "notfound",
+    version: "",
   },
 });
 
@@ -1160,6 +411,8 @@ export const TermsAlreadyAccepted = 
createExample(TestedComponent, {
   },
   terms: {
     status: "accepted",
+    content: undefined,
+    version: "",
   },
 });
 
@@ -1192,10 +445,11 @@ export const WithoutFee = createExample(TestedComponent, 
{
     null;
   },
   terms: {
-    value: {
+    content: {
       type: "xml",
       document: new DOMParser().parseFromString(termsXml, "text/xml"),
     },
     status: "accepted",
+    version: "",
   },
 });
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
index 8258717b..4ebbe11c 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
@@ -25,14 +25,11 @@ import {
   AmountJson,
   Amounts,
   ExchangeListItem,
-  GetExchangeTosResult,
   i18n,
   WithdrawUriInfoResponse,
 } from "@gnu-taler/taler-util";
-import { VNode, h, Fragment } from "preact";
+import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
-import { CheckboxOutlined } from "../components/CheckboxOutlined";
-import { ExchangeXmlTos } from "../components/ExchangeToS";
 import { LogoHeader } from "../components/LogoHeader";
 import { Part } from "../components/Part";
 import { SelectList } from "../components/SelectList";
@@ -40,19 +37,13 @@ import {
   ButtonSuccess,
   ButtonWarning,
   LinkSuccess,
-  TermsOfService,
   WalletAction,
   WarningText,
 } from "../components/styled";
 import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
-import {
-  acceptWithdrawal,
-  getExchangeTos,
-  getExchangeWithdrawalInfo,
-  getWithdrawalDetailsForUri,
-  listExchanges,
-  setExchangeTosAccepted,
-} from "../wxApi";
+import { amountToString, buildTermsOfServiceState, TermsState } from 
"../utils";
+import * as wxApi from "../wxApi";
+import { TermsOfServiceSection } from "./TermsOfServiceSection";
 
 interface Props {
   talerWithdrawUri?: string;
@@ -60,7 +51,7 @@ interface Props {
 
 export interface ViewProps {
   withdrawalFee: AmountJson;
-  exchangeBaseUrl: string;
+  exchangeBaseUrl?: string;
   amount: AmountJson;
   onSwitchExchange: (ex: string) => void;
   onWithdraw: () => Promise<void>;
@@ -69,53 +60,10 @@ export interface ViewProps {
   reviewing: boolean;
   reviewed: boolean;
   confirmed: boolean;
-  terms: {
-    value?: TermsDocument;
-    status: TermsStatus;
-  };
+  terms: TermsState;
   knownExchanges: ExchangeListItem[];
 }
 
-type TermsStatus = "new" | "accepted" | "changed" | "notfound";
-
-type TermsDocument =
-  | TermsDocumentXml
-  | TermsDocumentHtml
-  | TermsDocumentPlain
-  | TermsDocumentJson
-  | TermsDocumentPdf;
-
-interface TermsDocumentXml {
-  type: "xml";
-  document: Document;
-}
-
-interface TermsDocumentHtml {
-  type: "html";
-  href: URL;
-}
-
-interface TermsDocumentPlain {
-  type: "plain";
-  content: string;
-}
-
-interface TermsDocumentJson {
-  type: "json";
-  data: any;
-}
-
-interface TermsDocumentPdf {
-  type: "pdf";
-  location: URL;
-}
-
-function amountToString(text: AmountJson): string {
-  const aj = Amounts.jsonifyAmount(text);
-  const amount = Amounts.stringifyValue(aj);
-  return `${amount} ${aj.currency}`;
-}
-
 export function View({
   withdrawalFee,
   exchangeBaseUrl,
@@ -162,7 +110,9 @@ export function View({
             kind="negative"
           />
         )}
-        <Part title="Exchange" text={exchangeBaseUrl} kind="neutral" big />
+        {exchangeBaseUrl && (
+          <Part title="Exchange" text={exchangeBaseUrl} kind="neutral" big />
+        )}
       </section>
       {!reviewing && (
         <section>
@@ -190,13 +140,6 @@ export function View({
           )}
         </section>
       )}
-      {!reviewing && reviewed && (
-        <section>
-          <LinkSuccess upperCased onClick={() => onReview(true)}>
-            {i18n.str`Show terms of service`}
-          </LinkSuccess>
-        </section>
-      )}
       {terms.status === "notfound" && (
         <section>
           <WarningText>
@@ -204,79 +147,14 @@ export function View({
           </WarningText>
         </section>
       )}
-      {reviewing && (
-        <section>
-          {terms.status !== "accepted" &&
-            terms.value &&
-            terms.value.type === "xml" && (
-              <TermsOfService>
-                <ExchangeXmlTos doc={terms.value.document} />
-              </TermsOfService>
-            )}
-          {terms.status !== "accepted" &&
-            terms.value &&
-            terms.value.type === "plain" && (
-              <div style={{ textAlign: "left" }}>
-                <pre>{terms.value.content}</pre>
-              </div>
-            )}
-          {terms.status !== "accepted" &&
-            terms.value &&
-            terms.value.type === "html" && (
-              <iframe src={terms.value.href.toString()} />
-            )}
-          {terms.status !== "accepted" &&
-            terms.value &&
-            terms.value.type === "pdf" && (
-              <a href={terms.value.location.toString()} download="tos.pdf">
-                Download Terms of Service
-              </a>
-            )}
-        </section>
-      )}
-      {reviewing && reviewed && (
-        <section>
-          <LinkSuccess upperCased onClick={() => onReview(false)}>
-            {i18n.str`Hide terms of service`}
-          </LinkSuccess>
-        </section>
-      )}
-      {(reviewing || reviewed) && (
-        <section>
-          <CheckboxOutlined
-            name="terms"
-            enabled={reviewed}
-            label={i18n.str`I accept the exchange terms of service`}
-            onToggle={() => {
-              onAccept(!reviewed);
-              onReview(false);
-            }}
-          />
-        </section>
-      )}
-
-      {/**
-       * Main action section
-       */}
+      <TermsOfServiceSection
+        reviewed={reviewed}
+        reviewing={reviewing}
+        terms={terms}
+        onAccept={onAccept}
+        onReview={onReview}
+      />
       <section>
-        {terms.status === "new" && !reviewed && !reviewing && (
-          <ButtonSuccess
-            upperCased
-            disabled={!exchangeBaseUrl}
-            onClick={() => onReview(true)}
-          >
-            {i18n.str`Review exchange terms of service`}
-          </ButtonSuccess>
-        )}
-        {terms.status === "changed" && !reviewed && !reviewing && (
-          <ButtonWarning
-            upperCased
-            disabled={!exchangeBaseUrl}
-            onClick={() => onReview(true)}
-          >
-            {i18n.str`Review new version of terms of service`}
-          </ButtonWarning>
-        )}
         {(terms.status === "accepted" || (needsReview && reviewed)) && (
           <ButtonSuccess
             upperCased
@@ -310,15 +188,15 @@ export function WithdrawPageWithParsedURI({
   const [customExchange, setCustomExchange] = useState<string | undefined>(
     undefined,
   );
-  const [errorAccepting, setErrorAccepting] = useState<string | undefined>(
-    undefined,
-  );
+  // const [errorAccepting, setErrorAccepting] = useState<string | undefined>(
+  //   undefined,
+  // );
 
   const [reviewing, setReviewing] = useState<boolean>(false);
   const [reviewed, setReviewed] = useState<boolean>(false);
   const [confirmed, setConfirmed] = useState<boolean>(false);
 
-  const knownExchangesHook = useAsyncAsHook(() => listExchanges());
+  const knownExchangesHook = useAsyncAsHook(() => wxApi.listExchanges());
 
   const knownExchanges =
     !knownExchangesHook || knownExchangesHook.hasError
@@ -329,19 +207,25 @@ export function WithdrawPageWithParsedURI({
     (ex) => ex.currency === withdrawAmount.currency,
   );
 
-  const exchange =
-    customExchange ||
-    uriInfo.defaultExchangeBaseUrl ||
-    thisCurrencyExchanges[0]?.exchangeBaseUrl;
+  const exchange: string | undefined =
+    customExchange ??
+    uriInfo.defaultExchangeBaseUrl ??
+    (thisCurrencyExchanges[0]
+      ? thisCurrencyExchanges[0].exchangeBaseUrl
+      : undefined);
+
   const detailsHook = useAsyncAsHook(async () => {
     if (!exchange) throw Error("no default exchange");
-    const tos = await getExchangeTos(exchange, ["text/xml"]);
-    const info = await getExchangeWithdrawalInfo({
+    const tos = await wxApi.getExchangeTos(exchange, ["text/xml"]);
+
+    const tosState = buildTermsOfServiceState(tos);
+
+    const info = await wxApi.getExchangeWithdrawalInfo({
       exchangeBaseUrl: exchange,
       amount: withdrawAmount,
       tosAcceptedFormat: ["text/xml"],
     });
-    return { tos, info };
+    return { tos: tosState, info };
   });
 
   if (!detailsHook) {
@@ -364,21 +248,24 @@ export function WithdrawPageWithParsedURI({
   const details = detailsHook.response;
 
   const onAccept = async (): Promise<void> => {
+    if (!exchange) return;
     try {
-      await setExchangeTosAccepted(exchange, details.tos.currentEtag);
+      await wxApi.setExchangeTosAccepted(exchange, details.tos.version);
       setReviewed(true);
     } catch (e) {
       if (e instanceof Error) {
-        setErrorAccepting(e.message);
+        //FIXME: uncomment this and display error
+        // setErrorAccepting(e.message);
       }
     }
   };
 
   const onWithdraw = async (): Promise<void> => {
+    if (!exchange) return;
     setConfirmed(true);
     console.log("accepting exchange", exchange);
     try {
-      const res = await acceptWithdrawal(uri, exchange);
+      const res = await wxApi.acceptWithdrawal(uri, exchange);
       console.log("accept withdrawal response", res);
       if (res.confirmTransferUrl) {
         document.location.href = res.confirmTransferUrl;
@@ -388,30 +275,13 @@ export function WithdrawPageWithParsedURI({
     }
   };
 
-  const termsContent: TermsDocument | undefined = parseTermsOfServiceContent(
-    details.tos.contentType,
-    details.tos.content,
-  );
-
-  const status: TermsStatus = !termsContent
-    ? "notfound"
-    : !details.tos.acceptedEtag
-    ? "new"
-    : details.tos.acceptedEtag !== details.tos.currentEtag
-    ? "changed"
-    : "accepted";
-
   return (
     <View
       onWithdraw={onWithdraw}
-      // details={details.tos}
       amount={withdrawAmount}
       exchangeBaseUrl={exchange}
       withdrawalFee={details.info.withdrawFee} //FIXME
-      terms={{
-        status,
-        value: termsContent,
-      }}
+      terms={detailsHook.response.tos}
       onSwitchExchange={setCustomExchange}
       knownExchanges={knownExchanges}
       confirmed={confirmed}
@@ -426,7 +296,7 @@ export function WithdrawPage({ talerWithdrawUri }: Props): 
VNode {
   const uriInfoHook = useAsyncAsHook(() =>
     !talerWithdrawUri
       ? Promise.reject(undefined)
-      : getWithdrawalDetailsForUri({ talerWithdrawUri }),
+      : wxApi.getWithdrawalDetailsForUri({ talerWithdrawUri }),
   );
 
   if (!talerWithdrawUri) {
@@ -459,46 +329,3 @@ export function WithdrawPage({ talerWithdrawUri }: Props): 
VNode {
     />
   );
 }
-
-function parseTermsOfServiceContent(
-  type: string,
-  text: string,
-): TermsDocument | undefined {
-  if (type === "text/xml") {
-    try {
-      const document = new DOMParser().parseFromString(text, "text/xml");
-      return { type: "xml", document };
-    } catch (e) {
-      console.log(e);
-    }
-  } else if (type === "text/html") {
-    try {
-      const href = new URL(text);
-      return { type: "html", href };
-    } catch (e) {
-      console.log(e);
-    }
-  } else if (type === "text/json") {
-    try {
-      const data = JSON.parse(text);
-      return { type: "json", data };
-    } catch (e) {
-      console.log(e);
-    }
-  } else if (type === "text/pdf") {
-    try {
-      const location = new URL(text);
-      return { type: "pdf", location };
-    } catch (e) {
-      console.log(e);
-    }
-  } else if (type === "text/plain") {
-    try {
-      const content = text;
-      return { type: "plain", content };
-    } catch (e) {
-      console.log(e);
-    }
-  }
-  return undefined;
-}
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx 
b/packages/taler-wallet-webextension/src/cta/termsExample.ts
similarity index 83%
copy from packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
copy to packages/taler-wallet-webextension/src/cta/termsExample.ts
index 54ae19c6..d42e49c8 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/termsExample.ts
@@ -19,19 +19,7 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { amountFractionalBase } from "@gnu-taler/taler-util";
-import { createExample } from "../test-utils";
-import { View as TestedComponent } from "./Withdraw";
-
-export default {
-  title: "cta/withdraw",
-  component: TestedComponent,
-  argTypes: {
-    onSwitchExchange: { action: "onRetry" },
-  },
-};
-
-const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml"; lang="en">
+export const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml"; lang="en">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   <title>Terms Of Service &#8212; Taler Terms of Service</title>
@@ -45,7 +33,7 @@ const termsHtml = `<html xmlns="http://www.w3.org/1999/xhtml"; 
lang="en">
 </body>
 </html>
 `;
-const termsPlain = `
+export const termsPlain = `
 Terms Of Service
 ****************
 
@@ -430,7 +418,7 @@ us a message on our contact page at legal@taler-systems.com.
 
 `;
 
-const termsXml = `<?xml version="1.0" encoding="utf-8"?>
+export const termsXml = `<?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE document PUBLIC "+//IDN docutils.sourceforge.net//DTD Docutils 
Generic//EN//XML" "http://docutils.sourceforge.net/docs/ref/docutils.dtd";>
 <!-- Generated by Docutils 0.14 -->
 <document source="/home/grothoff/research/taler/exchange/contrib/tos/tos.rst">
@@ -776,122 +764,7 @@ const termsXml = `<?xml version="1.0" encoding="utf-8"?>
 </document>
 `;
 
-export const NewTerms = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    value: {
-      type: "xml",
-      document: new DOMParser().parseFromString(termsXml, "text/xml"),
-    },
-    status: "new",
-  },
-});
-
-export const TermsReviewingPLAIN = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    value: {
-      type: "plain",
-      content: termsPlain,
-    },
-    status: "new",
-  },
-  reviewing: true,
-});
-
-export const TermsReviewingHTML = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    value: {
-      type: "html",
-      href: new URL(
-        `data:text/html;base64,${Buffer.from(termsHtml).toString("base64")}`,
-      ),
-    },
-    status: "new",
-  },
-  reviewing: true,
-});
-
-const termsPdf = `
+export const termsPdf = `
 %PDF-1.2 
 9 0 obj << >> 
 stream
@@ -906,296 +779,3 @@ trailer
 %%EOF
 `;
 
-export const TermsReviewingPDF = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    value: {
-      type: "pdf",
-      location: new URL(
-        `data:text/html;base64,${Buffer.from(termsPdf).toString("base64")}`,
-      ),
-    },
-    status: "new",
-  },
-  reviewing: true,
-});
-
-export const TermsReviewingXML = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    value: {
-      type: "xml",
-      document: new DOMParser().parseFromString(termsXml, "text/xml"),
-    },
-    status: "new",
-  },
-  reviewing: true,
-});
-
-export const NewTermsAccepted = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    value: {
-      type: "xml",
-      document: new DOMParser().parseFromString(termsXml, "text/xml"),
-    },
-    status: "new",
-  },
-  reviewed: true,
-});
-
-export const TermsShowAgainXML = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    value: {
-      type: "xml",
-      document: new DOMParser().parseFromString(termsXml, "text/xml"),
-    },
-    status: "new",
-  },
-  reviewed: true,
-  reviewing: true,
-});
-
-export const TermsChanged = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    value: {
-      type: "xml",
-      document: new DOMParser().parseFromString(termsXml, "text/xml"),
-    },
-    status: "changed",
-  },
-});
-
-export const TermsNotFound = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    status: "notfound",
-  },
-});
-
-export const TermsAlreadyAccepted = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  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",
-  },
-});
-
-export const WithoutFee = createExample(TestedComponent, {
-  knownExchanges: [
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.demo.taler.net",
-      paytoUris: ["asd"],
-    },
-    {
-      currency: "USD",
-      exchangeBaseUrl: "exchange.test.taler.net",
-      paytoUris: ["asd"],
-    },
-  ],
-  exchangeBaseUrl: "exchange.demo.taler.net",
-  withdrawalFee: {
-    currency: "USD",
-    fraction: 0,
-    value: 0,
-  },
-  amount: {
-    currency: "USD",
-    value: 2,
-    fraction: 10000000,
-  },
-
-  onSwitchExchange: async () => {
-    null;
-  },
-  terms: {
-    value: {
-      type: "xml",
-      document: new DOMParser().parseFromString(termsXml, "text/xml"),
-    },
-    status: "accepted",
-  },
-});
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx 
b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index 008f30cb..33164783 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -71,6 +71,11 @@ export function BalanceView({
             <Linker pageName="/welcome">help</Linker> getting started?
           </i18n.Translate>
         </p>
+        <footer style={{ justifyContent: "space-around" }}>
+          <ButtonPrimary onClick={goToWalletManualWithdraw}>
+            Withdraw
+          </ButtonPrimary>
+        </footer>
       </Fragment>
     );
   }
diff --git a/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx 
b/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx
index ae8e54ba..06915747 100644
--- a/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Settings.stories.tsx
@@ -30,13 +30,8 @@ export default {
   },
 };
 
-export const AllOff = createExample(TestedComponent, {
-  deviceName: "this-is-the-device-name",
-  setDeviceName: () => Promise.resolve(),
-});
+export const AllOff = createExample(TestedComponent, {});
 
 export const OneChecked = createExample(TestedComponent, {
-  deviceName: "this-is-the-device-name",
   permissionsEnabled: true,
-  setDeviceName: () => Promise.resolve(),
 });
diff --git a/packages/taler-wallet-webextension/src/popup/Settings.tsx 
b/packages/taler-wallet-webextension/src/popup/Settings.tsx
index 0a3f777d..84ecea4f 100644
--- a/packages/taler-wallet-webextension/src/popup/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/popup/Settings.tsx
@@ -14,36 +14,18 @@
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */
 
-import { ExchangeListItem, i18n } from "@gnu-taler/taler-util";
+import { i18n } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
 import { Checkbox } from "../components/Checkbox";
-import { ButtonPrimary } from "../components/styled";
 import { useDevContext } from "../context/devContext";
-import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
-import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
 import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
-import { useLang } from "../hooks/useLang";
-// import { strings as messages } from "../i18n/strings";
-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}
@@ -53,36 +35,13 @@ export function SettingsPage(): VNode {
 }
 
 export interface ViewProps {
-  lang: string;
-  changeLang: (s: string) => void;
-  deviceName: string;
-  setDeviceName: (s: string) => Promise<void>;
   permissionsEnabled: boolean;
   togglePermissions: () => void;
   developerMode: boolean;
   toggleDeveloperMode: () => void;
-  knownExchanges: Array<ExchangeListItem>;
 }
 
-// type LangsNames = {
-//   [P in keyof typeof messages]: string;
-// };
-
-// const names: LangsNames = {
-//   es: "Español [es]",
-//   en: "English [en]",
-//   fr: "Français [fr]",
-//   de: "Deutsch [de]",
-//   sv: "Svenska [sv]",
-//   it: "Italiano [it]",
-// };
-
 export function SettingsView({
-  knownExchanges,
-  // lang,
-  // changeLang,
-  // deviceName,
-  // setDeviceName,
   permissionsEnabled,
   togglePermissions,
   developerMode,
@@ -91,45 +50,6 @@ export function SettingsView({
   return (
     <Fragment>
       <section>
-        <h2>
-          <i18n.Translate>Known exchanges</i18n.Translate>
-        </h2>
-        {!knownExchanges || !knownExchanges.length ? (
-          <div>No exchange yet!</div>
-        ) : (
-          <Fragment>
-            <table>
-              {knownExchanges.map((e, idx) => (
-                <tr key={idx}>
-                  <td>{e.currency}</td>
-                  <td>
-                    <a href={e.exchangeBaseUrl}>{e.exchangeBaseUrl}</a>
-                  </td>
-                </tr>
-              ))}
-            </table>
-          </Fragment>
-        )}
-        <div style={{ display: "flex", justifyContent: "space-between" }}>
-          <div />
-          <ButtonPrimary>Manage exchange</ButtonPrimary>
-        </div>
-        {/* <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)"
-        /> */}
         <h2>
           <i18n.Translate>Permissions</i18n.Translate>
         </h2>
diff --git a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx 
b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
index d0c79f6d..c6c872f2 100644
--- a/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/popupEntryPoint.tsx
@@ -38,6 +38,7 @@ import { ProviderAddPage } from "./wallet/ProviderAddPage";
 import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
 import { SettingsPage } from "./popup/Settings";
 import { TalerActionFound } from "./popup/TalerActionFound";
+import { ExchangeAddPage } from "./wallet/ExchangeAddPage";
 
 function main(): void {
   try {
@@ -127,6 +128,15 @@ function Application() {
                 route(Pages.backup);
               }}
             />
+
+            <Route
+              path={Pages.exchange_add}
+              component={ExchangeAddPage}
+              onBack={() => {
+                route(Pages.balance);
+              }}
+            />
+
             <Route default component={Redirect} to={Pages.balance} />
           </Router>
         </PopupBox>
diff --git a/packages/taler-wallet-webextension/src/utils/index.ts 
b/packages/taler-wallet-webextension/src/utils/index.ts
new file mode 100644
index 00000000..47781852
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/utils/index.ts
@@ -0,0 +1,162 @@
+/*
+ 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/>
+ */
+
+import { AmountJson, Amounts, GetExchangeTosResult } from 
"@gnu-taler/taler-util";
+
+
+function getJsonIfOk(r: Response): Promise<any> {
+  if (r.ok) {
+    return r.json();
+  }
+
+  if (r.status >= 400 && r.status < 500) {
+    throw new Error(`URL may not be right: (${r.status}) ${r.statusText}`);
+  }
+
+  throw new Error(
+    `Try another server: (${r.status}) ${r.statusText || "internal server 
error"
+    }`,
+  );
+
+}
+
+export async function queryToSlashConfig<T>(
+  url: string,
+): Promise<T> {
+  return fetch(new URL("config", url).href)
+    .catch(() => {
+      throw new Error(`Network error`);
+    })
+    .then(getJsonIfOk);
+}
+
+export async function queryToSlashKeys<T>(
+  url: string,
+): Promise<T> {
+  return fetch(new URL("keys", url).href)
+    .catch(() => {
+      throw new Error(`Network error`);
+    })
+    .then(getJsonIfOk);
+}
+
+export function buildTermsOfServiceState(tos: GetExchangeTosResult): 
TermsState {
+
+  const content: TermsDocument | undefined = parseTermsOfServiceContent(
+    tos.contentType,
+    tos.content,
+  );
+
+  const status: TermsStatus = !content
+    ? "notfound"
+    : !tos.acceptedEtag
+      ? "new"
+      : tos.acceptedEtag !== tos.currentEtag
+        ? "changed"
+        : "accepted";
+
+  return { content, status, version: tos.currentEtag }
+}
+
+function parseTermsOfServiceContent(
+  type: string,
+  text: string,
+): TermsDocument | undefined {
+  if (type === "text/xml") {
+    try {
+      const document = new DOMParser().parseFromString(text, "text/xml");
+      return { type: "xml", document };
+    } catch (e) {
+      console.log(e);
+    }
+  } else if (type === "text/html") {
+    try {
+      const href = new URL(text);
+      return { type: "html", href };
+    } catch (e) {
+      console.log(e);
+    }
+  } else if (type === "text/json") {
+    try {
+      const data = JSON.parse(text);
+      return { type: "json", data };
+    } catch (e) {
+      console.log(e);
+    }
+  } else if (type === "text/pdf") {
+    try {
+      const location = new URL(text);
+      return { type: "pdf", location };
+    } catch (e) {
+      console.log(e);
+    }
+  } else if (type === "text/plain") {
+    try {
+      const content = text;
+      return { type: "plain", content };
+    } catch (e) {
+      console.log(e);
+    }
+  }
+  return undefined;
+}
+
+export type TermsState = {
+  content: TermsDocument | undefined;
+  status: TermsStatus;
+  version: string;
+};
+
+type TermsStatus = "new" | "accepted" | "changed" | "notfound";
+
+type TermsDocument =
+  | TermsDocumentXml
+  | TermsDocumentHtml
+  | TermsDocumentPlain
+  | TermsDocumentJson
+  | TermsDocumentPdf;
+
+interface TermsDocumentXml {
+  type: "xml";
+  document: Document;
+}
+
+interface TermsDocumentHtml {
+  type: "html";
+  href: URL;
+}
+
+interface TermsDocumentPlain {
+  type: "plain";
+  content: string;
+}
+
+interface TermsDocumentJson {
+  type: "json";
+  data: any;
+}
+
+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/BalancePage.tsx 
b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
index 04d79a5e..0a891064 100644
--- a/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx
@@ -17,7 +17,7 @@
 import { BalancesResponse, i18n } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
 import { BalanceTable } from "../components/BalanceTable";
-import { ButtonPrimary, ErrorBox } from "../components/styled/index";
+import { ButtonPrimary, Centered, ErrorBox } from "../components/styled/index";
 import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook";
 import { PageLink } from "../renderHtml";
 import * as wxApi from "../wxApi";
@@ -66,10 +66,17 @@ export function BalanceView({
   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>
+        <Centered style={{ marginTop: 100 }}>
+          <i18n.Translate>
+            You have no balance to show. Need some{" "}
+            <Linker pageName="/welcome">help</Linker> getting started?
+          </i18n.Translate>
+          <div>
+            <ButtonPrimary onClick={goToWalletManualWithdraw}>
+              Withdraw
+            </ButtonPrimary>
+          </div>
+        </Centered>
       </p>
     );
   }
diff --git 
a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx 
b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
index 1bceabd2..55495279 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, i18n } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
+import { route } from "preact-router";
 import { useState } from "preact/hooks";
 import { ErrorMessage } from "../components/ErrorMessage";
 import { SelectList } from "../components/SelectList";
@@ -32,7 +33,9 @@ import {
   Input,
   InputWithLabel,
   LightText,
+  LinkPrimary,
 } from "../components/styled";
+import { Pages } from "../NavigationBar";
 
 export interface Props {
   error: string | undefined;
@@ -87,12 +90,7 @@ export function CreateManualWithdraw({
     return (
       <Centered style={{ marginTop: 100 }}>
         <BoldLight>No exchange configured</BoldLight>
-        <ButtonSuccess
-          //FIXME: add exchange feature
-          onClick={() => {
-            null;
-          }}
-        >
+        <ButtonSuccess onClick={() => route(Pages.exchange_add)}>
           <i18n.Translate>Add exchange</i18n.Translate>
         </ButtonSuccess>
       </Centered>
@@ -108,8 +106,9 @@ export function CreateManualWithdraw({
         />
         <h2>Manual Withdrawal</h2>
         <LightText>
-          Choose a exchange to create a reserve and then fill the reserve to
-          withdraw the coins
+          Choose a exchange from where the coins will be withdrawn. The 
exchange
+          will send the coins to this wallet after receiving a wire transfer
+          with the correct subject.
         </LightText>
         <p>
           <Input>
@@ -130,11 +129,14 @@ export function CreateManualWithdraw({
               onChange={changeExchange}
             />
           </Input>
-          {/* <p style={{ display: "flex", justifyContent: "right" }}>
-            <a href="" style={{ marginLeft: "auto" }}>
-              Add new exchange
-            </a>
-          </p> */}
+          <div style={{ display: "flex", justifyContent: "space-between" }}>
+            <LinkPrimary
+              onClick={() => route(Pages.exchange_add)}
+              style={{ marginLeft: "auto" }}
+            >
+              <i18n.Translate>Add exchange</i18n.Translate>
+            </LinkPrimary>
+          </div>
           {currency && (
             <InputWithLabel invalid={!!amount && !parsedAmount}>
               <label>Amount</label>
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx
new file mode 100644
index 00000000..2e034458
--- /dev/null
+++ 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.stories.tsx
@@ -0,0 +1,67 @@
+/*
+ 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 { termsXml } from "../cta/termsExample";
+import { createExample } from "../test-utils";
+import { View as TestedComponent } from "./ExchangeAddConfirm";
+
+export default {
+  title: "wallet/exchange add/confirm",
+  component: TestedComponent,
+  argTypes: {
+    onRetry: { action: "onRetry" },
+    onDelete: { action: "onDelete" },
+    onBack: { action: "onBack" },
+  },
+};
+
+export const TermsNotFound = createExample(TestedComponent, {
+  url: "https://exchange.demo.taler.net/";,
+  terms: {
+    status: "notfound",
+    version: "1",
+    content: undefined,
+  },
+  onAccept: async () => undefined,
+});
+
+export const NewTerms = createExample(TestedComponent, {
+  url: "https://exchange.demo.taler.net/";,
+  terms: {
+    status: "new",
+    version: "1",
+    content: undefined,
+  },
+  onAccept: async () => undefined,
+});
+
+export const TermsChanged = createExample(TestedComponent, {
+  url: "https://exchange.demo.taler.net/";,
+  terms: {
+    status: "changed",
+    version: "1",
+    content: {
+      type: "xml",
+      document: new DOMParser().parseFromString(termsXml, "text/xml"),
+    },
+  },
+  onAccept: async () => undefined,
+});
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx
new file mode 100644
index 00000000..5c7f94ec
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx
@@ -0,0 +1,152 @@
+import { i18n } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import {
+  Button,
+  ButtonSuccess,
+  ButtonWarning,
+  WarningBox,
+} from "../components/styled/index";
+import { TermsOfServiceSection } from "../cta/TermsOfServiceSection";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
+import { buildTermsOfServiceState, TermsState } from "../utils";
+import * as wxApi from "../wxApi";
+
+export interface Props {
+  url: string;
+  onCancel: () => void;
+  onConfirm: () => void;
+}
+
+export function ExchangeAddConfirmPage({
+  url,
+  onCancel,
+  onConfirm,
+}: Props): VNode {
+  const detailsHook = useAsyncAsHook(async () => {
+    const tos = await wxApi.getExchangeTos(url, ["text/xml"]);
+
+    const tosState = buildTermsOfServiceState(tos);
+
+    return { tos: tosState };
+  });
+
+  const termsNotFound: TermsState = {
+    status: "notfound",
+    version: "",
+    content: undefined,
+  };
+  const terms = !detailsHook
+    ? undefined
+    : detailsHook.hasError
+    ? termsNotFound
+    : detailsHook.response.tos;
+
+  // const [errorAccepting, setErrorAccepting] = useState<string | undefined>(
+  //   undefined,
+  // );
+
+  const onAccept = async (): Promise<void> => {
+    if (!terms) return;
+    try {
+      await wxApi.setExchangeTosAccepted(url, terms.version);
+    } catch (e) {
+      if (e instanceof Error) {
+        // setErrorAccepting(e.message);
+      }
+    }
+  };
+  return (
+    <View
+      url={url}
+      onAccept={onAccept}
+      onCancel={onCancel}
+      onConfirm={onConfirm}
+      terms={terms}
+    />
+  );
+}
+
+export interface ViewProps {
+  url: string;
+  terms: TermsState | undefined;
+  onAccept: (b: boolean) => Promise<void>;
+  onCancel: () => void;
+  onConfirm: () => void;
+}
+
+export function View({
+  url,
+  terms,
+  onAccept: doAccept,
+  onConfirm,
+  onCancel,
+}: ViewProps): VNode {
+  const needsReview =
+    !terms || terms.status === "changed" || terms.status === "new";
+  const [reviewed, setReviewed] = useState<boolean>(false);
+
+  return (
+    <Fragment>
+      <section>
+        <h1>Review terms of service</h1>
+        <div>
+          Exchange URL:
+          <a href={url} target="_blank" rel="noreferrer">
+            {url}
+          </a>
+        </div>
+      </section>
+      {terms && terms.status === "notfound" && (
+        <section>
+          <WarningBox>
+            {i18n.str`Exchange doesn't have terms of service`}
+          </WarningBox>
+        </section>
+      )}
+
+      {terms && (
+        <TermsOfServiceSection
+          reviewed={reviewed}
+          reviewing={true}
+          terms={terms}
+          onAccept={(value) =>
+            doAccept(value).then(() => {
+              setReviewed(value);
+            })
+          }
+        />
+      )}
+
+      <footer>
+        <Button onClick={onCancel}>
+          <i18n.Translate>Cancel</i18n.Translate>
+        </Button>
+        {!terms && (
+          <Button disabled>
+            <i18n.Translate>Loading terms..</i18n.Translate>
+          </Button>
+        )}
+        {terms && (
+          <Fragment>
+            {needsReview && !reviewed && (
+              <ButtonSuccess disabled upperCased onClick={onConfirm}>
+                {i18n.str`Add exchange`}
+              </ButtonSuccess>
+            )}
+            {(terms.status === "accepted" || (needsReview && reviewed)) && (
+              <ButtonSuccess upperCased onClick={onConfirm}>
+                {i18n.str`Add exchange`}
+              </ButtonSuccess>
+            )}
+            {terms.status === "notfound" && (
+              <ButtonWarning upperCased onClick={onConfirm}>
+                {i18n.str`Add exchange anyway`}
+              </ButtonWarning>
+            )}
+          </Fragment>
+        )}
+      </footer>
+    </Fragment>
+  );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx
new file mode 100644
index 00000000..10449c10
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeAddPage.tsx
@@ -0,0 +1,75 @@
+/*
+ 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/>
+ */
+
+import {
+  canonicalizeBaseUrl,
+  TalerConfigResponse,
+} from "@gnu-taler/taler-util";
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
+import { queryToSlashKeys } from "../utils";
+import * as wxApi from "../wxApi";
+import { ExchangeAddConfirmPage } from "./ExchangeAddConfirm";
+import { ExchangeSetUrlPage } from "./ExchangeSetUrl";
+
+interface Props {
+  currency: string;
+  onBack: () => void;
+}
+
+export function ExchangeAddPage({ onBack }: Props): VNode {
+  const [verifying, setVerifying] = useState<
+    { url: string; config: TalerConfigResponse } | undefined
+  >(undefined);
+
+  const knownExchangesResponse = useAsyncAsHook(wxApi.listExchanges);
+  const knownExchanges = !knownExchangesResponse
+    ? []
+    : knownExchangesResponse.hasError
+    ? []
+    : knownExchangesResponse.response.exchanges;
+
+  if (!verifying) {
+    return (
+      <ExchangeSetUrlPage
+        onCancel={onBack}
+        knownExchanges={knownExchanges}
+        onVerify={(url) => queryToSlashKeys(url)}
+        onConfirm={(url) =>
+          queryToSlashKeys<TalerConfigResponse>(url)
+            .then((config) => {
+              setVerifying({ url, config });
+            })
+            .catch((e) => e.message)
+        }
+      />
+    );
+  }
+  return (
+    <ExchangeAddConfirmPage
+      url={verifying.url}
+      onCancel={onBack}
+      onConfirm={async () => {
+        await wxApi.addExchange({
+          exchangeBaseUrl: canonicalizeBaseUrl(verifying.url),
+          forceUpdate: true,
+        });
+        onBack();
+      }}
+    />
+  );
+}
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx
new file mode 100644
index 00000000..bc182cb7
--- /dev/null
+++ 
b/packages/taler-wallet-webextension/src/wallet/ExchangeAddSetUrl.stories.tsx
@@ -0,0 +1,62 @@
+/*
+ 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";
+import { queryToSlashKeys } from "../utils";
+import { ExchangeSetUrlPage as TestedComponent } from "./ExchangeSetUrl";
+
+export default {
+  title: "wallet/exchange add/set url",
+  component: TestedComponent,
+  argTypes: {
+    onRetry: { action: "onRetry" },
+    onDelete: { action: "onDelete" },
+    onBack: { action: "onBack" },
+  },
+};
+
+export const ExpectedUSD = createExample(TestedComponent, {
+  expectedCurrency: "USD",
+  onVerify: queryToSlashKeys,
+  knownExchanges: [],
+});
+
+export const ExpectedKUDOS = createExample(TestedComponent, {
+  expectedCurrency: "KUDOS",
+  onVerify: queryToSlashKeys,
+  knownExchanges: [],
+});
+
+export const InitialState = createExample(TestedComponent, {
+  onVerify: queryToSlashKeys,
+  knownExchanges: [],
+});
+
+export const WithDemoAsKnownExchange = createExample(TestedComponent, {
+  knownExchanges: [
+    {
+      currency: "TESTKUDOS",
+      exchangeBaseUrl: "https://exchange.demo.taler.net/";,
+      paytoUris: [],
+    },
+  ],
+  onVerify: queryToSlashKeys,
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx
new file mode 100644
index 00000000..e87a8894
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx
@@ -0,0 +1,130 @@
+import {
+  canonicalizeBaseUrl,
+  ExchangeListItem,
+  i18n,
+  TalerConfigResponse,
+} from "@gnu-taler/taler-util";
+import { Fragment, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { ErrorMessage } from "../components/ErrorMessage";
+import {
+  Button,
+  ButtonPrimary,
+  Input,
+  WarningBox,
+} from "../components/styled/index";
+
+export interface Props {
+  initialValue?: string;
+  expectedCurrency?: string;
+  knownExchanges: ExchangeListItem[];
+  onCancel: () => void;
+  onVerify: (s: string) => Promise<TalerConfigResponse | undefined>;
+  onConfirm: (url: string) => Promise<string | undefined>;
+  withError?: string;
+}
+
+export function ExchangeSetUrlPage({
+  initialValue,
+  knownExchanges,
+  expectedCurrency,
+  onCancel,
+  onVerify,
+  onConfirm,
+  withError,
+}: Props) {
+  const [value, setValue] = useState<string>(initialValue || "");
+  const [dirty, setDirty] = useState(false);
+  const [result, setResult] = useState<TalerConfigResponse | undefined>(
+    undefined,
+  );
+  const [error, setError] = useState<string | undefined>(withError);
+
+  useEffect(() => {
+    try {
+      const url = canonicalizeBaseUrl(value);
+
+      const found =
+        knownExchanges.findIndex((e) => e.exchangeBaseUrl === url) !== -1;
+
+      if (found) {
+        setError("This exchange is already known");
+        return;
+      }
+      onVerify(url)
+        .then((r) => {
+          setResult(r);
+        })
+        .catch(() => {
+          setResult(undefined);
+        });
+      setDirty(true);
+    } catch {
+      setResult(undefined);
+    }
+  }, [value]);
+
+  return (
+    <Fragment>
+      <section>
+        {!expectedCurrency ? (
+          <h1>Add new exchange</h1>
+        ) : (
+          <h2>Add exchange for {expectedCurrency}</h2>
+        )}
+        <ErrorMessage
+          title={error && "Unable to add this exchange"}
+          description={error}
+        />
+        <p>
+          <Input invalid={dirty && !!error}>
+            <label>URL</label>
+            <input
+              type="text"
+              placeholder="https://";
+              value={value}
+              onInput={(e) => setValue(e.currentTarget.value)}
+            />
+          </Input>
+          {result && (
+            <Fragment>
+              <Input>
+                <label>Version</label>
+                <input type="text" disabled value={result.version} />
+              </Input>
+              <Input>
+                <label>Currency</label>
+                <input type="text" disabled value={result.currency} />
+              </Input>
+            </Fragment>
+          )}
+        </p>
+      </section>
+      {result && expectedCurrency && expectedCurrency !== result.currency && (
+        <WarningBox>
+          This exchange doesn't match the expected currency{" "}
+          <b>{expectedCurrency}</b>
+        </WarningBox>
+      )}
+      <footer>
+        <Button onClick={onCancel}>
+          <i18n.Translate>Cancel</i18n.Translate>
+        </Button>
+        <ButtonPrimary
+          disabled={
+            !result ||
+            !!error ||
+            (expectedCurrency !== undefined &&
+              expectedCurrency !== result.currency)
+          }
+          onClick={() => {
+            const url = canonicalizeBaseUrl(value);
+            return onConfirm(url).then((r) => (r ? setError(r) : undefined));
+          }}
+        >
+          <i18n.Translate>Next</i18n.Translate>
+        </ButtonPrimary>
+      </footer>
+    </Fragment>
+  );
+}
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx 
b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
index 41852e38..16f23967 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
@@ -31,6 +31,7 @@ import {
   LightText,
   SmallLightText,
 } from "../components/styled/index";
+import { queryToSlashConfig } from "../utils";
 import * as wxApi from "../wxApi";
 
 interface Props {
@@ -38,45 +39,19 @@ interface Props {
   onBack: () => void;
 }
 
-function getJsonIfOk(r: Response) {
-  if (r.ok) {
-    return r.json();
-  } else {
-    if (r.status >= 400 && r.status < 500) {
-      throw new Error(`URL may not be right: (${r.status}) ${r.statusText}`);
-    } else {
-      throw new Error(
-        `Try another server: (${r.status}) ${
-          r.statusText || "internal server error"
-        }`,
-      );
-    }
-  }
-}
-
 export function ProviderAddPage({ onBack }: Props): VNode {
   const [verifying, setVerifying] = useState<
     | { url: string; name: string; provider: BackupBackupProviderTerms }
     | undefined
   >(undefined);
 
-  async function getProviderInfo(
-    url: string,
-  ): Promise<BackupBackupProviderTerms> {
-    return fetch(new URL("config", url).href)
-      .catch((e) => {
-        throw new Error(`Network error`);
-      })
-      .then(getJsonIfOk);
-  }
-
   if (!verifying) {
     return (
       <SetUrlView
         onCancel={onBack}
-        onVerify={(url) => getProviderInfo(url)}
+        onVerify={(url) => queryToSlashConfig(url)}
         onConfirm={(url, name) =>
-          getProviderInfo(url)
+          queryToSlashConfig<BackupBackupProviderTerms>(url)
             .then((provider) => {
               setVerifying({ url, name, provider });
             })
diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx 
b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
index 075126dc..f009c5ad 100644
--- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.tsx
@@ -3,6 +3,7 @@ import { Fragment, h, VNode } from "preact";
 import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType";
 import { QR } from "../components/QR";
 import { ButtonDestructive, WarningBox } from "../components/styled";
+import { amountToString } from "../utils";
 export interface Props {
   reservePub: string;
   payto: string;
@@ -29,10 +30,10 @@ export function ReserveCreated({
         <h1>Exchange is ready for withdrawal!</h1>
         <p>
           To complete the process you need to wire{" "}
-          <b>{Amounts.stringify(amount)}</b> to the exchange bank account
+          <b>{amountToString(amount)}</b> to the exchange bank account
         </p>
         <BankDetailsByPaytoType
-          amount={Amounts.stringify(amount)}
+          amount={amountToString(amount)}
           exchangeBaseUrl={exchangeBaseUrl}
           payto={paytoURI}
           subject={reservePub}
diff --git a/packages/taler-wallet-webextension/src/wallet/Settings.tsx 
b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
index 586d7b53..5f1cd89d 100644
--- a/packages/taler-wallet-webextension/src/wallet/Settings.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/Settings.tsx
@@ -17,12 +17,13 @@
 import { ExchangeListItem, i18n } from "@gnu-taler/taler-util";
 import { Fragment, h, VNode } from "preact";
 import { Checkbox } from "../components/Checkbox";
-import { ButtonPrimary } from "../components/styled";
+import { LinkPrimary } from "../components/styled";
 import { useDevContext } from "../context/devContext";
 import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
 import { useBackupDeviceName } from "../hooks/useBackupDeviceName";
 import { useExtendedPermissions } from "../hooks/useExtendedPermissions";
 import { useLang } from "../hooks/useLang";
+import { Pages } from "../NavigationBar";
 // import { strings as messages } from "../i18n/strings";
 import * as wxApi from "../wxApi";
 
@@ -112,7 +113,7 @@ export function SettingsView({
         )}
         <div style={{ display: "flex", justifyContent: "space-between" }}>
           <div />
-          <ButtonPrimary>Manage exchange</ButtonPrimary>
+          <LinkPrimary href={Pages.exchange_add}>Add an exchange</LinkPrimary>
         </div>
 
         <h2>
diff --git a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx 
b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
index a17550ff..73000c9a 100644
--- a/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
+++ b/packages/taler-wallet-webextension/src/walletEntryPoint.tsx
@@ -44,6 +44,7 @@ import { ManualWithdrawPage } from 
"./wallet/ManualWithdrawPage";
 import { WalletBox } from "./components/styled";
 import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
 import { ProviderAddPage } from "./wallet/ProviderAddPage";
+import { ExchangeAddPage } from "./wallet/ExchangeAddPage";
 
 function main(): void {
   try {
@@ -131,6 +132,14 @@ function Application(): VNode {
             }}
           />
 
+          <Route
+            path={Pages.exchange_add}
+            component={withLogoAndNavBar(ExchangeAddPage)}
+            onBack={() => {
+              route(Pages.balance);
+            }}
+          />
+
           <Route
             path={Pages.manual_withdraw}
             component={withLogoAndNavBar(ManualWithdrawPage)}

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