gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: new compose feature: sub-stat


From: gnunet
Subject: [taler-wallet-core] branch master updated: new compose feature: sub-states
Date: Tue, 20 Sep 2022 21:06:07 +0200

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 52ec740c8 new compose feature: sub-states
52ec740c8 is described below

commit 52ec740c825d4e94fd59ef0a5cd8e8b73f4dfc06
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Sep 20 16:04:51 2022 -0300

    new compose feature: sub-states
    
    implemented in withdraw page, WIP
---
 .../src/cta/InvoiceCreate/index.ts                 |   1 +
 .../src/cta/InvoiceCreate/state.ts                 |   3 +
 .../src/cta/InvoiceCreate/stories.tsx              |   3 +
 .../src/cta/InvoiceCreate/views.tsx                |   5 +-
 .../src/cta/Payment/state.ts                       |   1 +
 .../src/cta/Withdraw/index.ts                      |  18 +-
 .../src/cta/Withdraw/state.ts                      | 555 ++++++++-------------
 .../src/cta/Withdraw/stories.tsx                   |  12 +
 .../src/cta/Withdraw/test.ts                       |  29 +-
 .../src/cta/Withdraw/views.tsx                     |  14 +-
 .../src/hooks/useSelectedExchange.ts               | 125 +++++
 .../taler-wallet-webextension/src/test-utils.ts    |  23 +-
 .../taler-wallet-webextension/src/utils/index.ts   |  49 +-
 .../src/wallet/ExchangeSelection/index.ts          |  10 +-
 .../src/wallet/ExchangeSelection/state.ts          |  13 +-
 .../src/wallet/ExchangeSelection/views.tsx         |   2 +-
 16 files changed, 444 insertions(+), 419 deletions(-)

diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts 
b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
index 8beac2cb2..2bee51669 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts
@@ -48,6 +48,7 @@ export namespace State {
   }
   export interface Ready extends BaseInfo {
     status: "ready";
+    doSelectExchange: ButtonHandler;
     create: ButtonHandler;
     subject: TextFieldHandler;
     toBeReceived: AmountJson;
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts 
b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
index 6b4f54504..9b67b4414 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts
@@ -84,6 +84,9 @@ export function useComponentState(
       value: subject,
       onInput: async (e) => setSubject(e),
     },
+    doSelectExchange: {
+      //FIX
+    },
     invalid: !subject || Amounts.isZero(amount),
     exchangeUrl: selected.exchangeBaseUrl,
     create: {
diff --git 
a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
index b5a0a52e2..306d1b199 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx
@@ -37,6 +37,9 @@ export const Ready = createExample(ReadyView, {
     currency: "ARS",
     value: 1,
     fraction: 0,
+  },
+  doSelectExchange: {
+
   },
   exchangeUrl: "https://exchange.taler.ar";,
   subject: {
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx 
b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
index 209fb31e5..603392b60 100644
--- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx
@@ -54,6 +54,7 @@ export function ReadyView({
   create,
   toBeReceived,
   chosenAmount,
+  doSelectExchange,
 }: State.Ready): VNode {
   const { i18n } = useTranslationContext();
 
@@ -93,13 +94,13 @@ export function ReadyView({
               }}
             >
               <i18n.Translate>Exchange</i18n.Translate>
-              {/* <Link>
+              <Button onClick={doSelectExchange.onClick} variant="text">
                 <SvgIcon
                   title="Edit"
                   dangerouslySetInnerHTML={{ __html: editIcon }}
                   color="black"
                 />
-              </Link> */}
+              </Button>
             </div>
           }
           text={<ExchangeDetails exchange={exchangeUrl} />}
diff --git a/packages/taler-wallet-webextension/src/cta/Payment/state.ts 
b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
index e8690be39..8d388aa60 100644
--- a/packages/taler-wallet-webextension/src/cta/Payment/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Payment/state.ts
@@ -128,6 +128,7 @@ export function useComponentState(
         });
       }
       const res = await api.confirmPay(payStatus.proposalId, undefined);
+      // handle confirm pay
       if (res.type !== ConfirmPayResultType.Done) {
         throw TalerError.fromUncheckedDetail({
           code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts 
b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
index 2d9aaf828..d38c27a2f 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/index.ts
@@ -25,12 +25,17 @@ import {
   useComponentStateFromParams,
   useComponentStateFromURI,
 } from "./state.js";
+import {
+  State as SelectExchangeState
+} from "../../hooks/useSelectedExchange.js";
+
 import {
   LoadingExchangeView,
   LoadingInfoView,
   LoadingUriView,
   SuccessView,
 } from "./views.js";
+import { ExchangeSelectionPage } from 
"../../wallet/ExchangeSelection/index.js";
 
 export interface PropsFromURI {
   talerWithdrawUri: string | undefined;
@@ -49,6 +54,7 @@ export type State =
   | State.LoadingUriError
   | State.LoadingExchangeError
   | State.LoadingInfoError
+  | SelectExchangeState.Selecting
   | State.Success;
 
 export namespace State {
@@ -57,12 +63,12 @@ export namespace State {
     error: undefined;
   }
   export interface LoadingUriError {
-    status: "loading-uri";
+    status: "loading-error";
     error: HookError;
   }
   export interface LoadingExchangeError {
-    status: "loading-exchange";
-    error: HookError;
+    status: "no-exchange";
+    error: undefined,
   }
   export interface LoadingInfoError {
     status: "loading-info";
@@ -80,6 +86,7 @@ export namespace State {
     toBeReceived: AmountJson;
 
     doWithdrawal: ButtonHandler;
+    doSelectExchange: ButtonHandler;
     tosProps?: TermsOfServiceSectionProps;
     mustAcceptFirst: boolean;
 
@@ -92,9 +99,10 @@ export namespace State {
 
 const viewMapping: StateViewMap<State> = {
   loading: Loading,
-  "loading-uri": LoadingUriView,
-  "loading-exchange": LoadingExchangeView,
+  "loading-error": LoadingUriView,
+  "no-exchange": LoadingExchangeView,
   "loading-info": LoadingInfoView,
+  "selecting-exchange": ExchangeSelectionPage,
   success: SuccessView,
 };
 
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts 
b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
index 1256bf469..2e68d056e 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/state.ts
@@ -14,223 +14,58 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
+/* eslint-disable react-hooks/rules-of-hooks */
+import { AmountJson, Amounts, ExchangeListItem, parsePaytoUri } from 
"@gnu-taler/taler-util";
 import { TalerError } from "@gnu-taler/taler-wallet-core";
 import { useState } from "preact/hooks";
+import { Amount } from "../../components/Amount.js";
 import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
+import { useSelectedExchange } from "../../hooks/useSelectedExchange.js";
 import { buildTermsOfServiceState } from "../../utils/index.js";
 import * as wxApi from "../../wxApi.js";
 import { PropsFromURI, PropsFromParams, State } from "./index.js";
 
+type RecursiveState<S extends object> = S | (() => RecursiveState<S>)
+
 export function useComponentStateFromParams(
   { amount, cancel, onSuccess }: PropsFromParams,
   api: typeof wxApi,
-): State {
-  const [ageRestricted, setAgeRestricted] = useState(0);
-
-  const exchangeHook = useAsyncAsHook(api.listExchanges);
-
-  const exchangeHookDep =
-    !exchangeHook || exchangeHook.hasError || !exchangeHook.response
-      ? undefined
-      : exchangeHook.response;
-
-  const chosenAmount = Amounts.parseOrThrow(amount);
-
-  // get the first exchange with the currency as the default one
-  const exchange = exchangeHookDep
-    ? exchangeHookDep.exchanges.find(
-        (e) => e.currency === chosenAmount.currency,
-      )
-    : undefined;
-  /**
-   * For the exchange selected, bring the status of the terms of service
-   */
-  const terms = useAsyncAsHook(async () => {
-    if (!exchange) return undefined;
-
-    const exchangeTos = await api.getExchangeTos(exchange.exchangeBaseUrl, [
-      "text/xml",
-    ]);
-
-    const state = buildTermsOfServiceState(exchangeTos);
-
-    return { state };
-  }, [exchangeHookDep]);
-
-  /**
-   * With the exchange and amount, ask the wallet the information
-   * about the withdrawal
-   */
-  const amountHook = useAsyncAsHook(async () => {
-    if (!exchange) return undefined;
-
-    const info = await api.getExchangeWithdrawalInfo({
-      exchangeBaseUrl: exchange.exchangeBaseUrl,
-      amount: chosenAmount,
-      tosAcceptedFormat: ["text/xml"],
-      ageRestricted,
-    });
-
-    const withdrawAmount = {
-      raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
-      effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
-    };
-
-    return {
-      amount: withdrawAmount,
-      ageRestrictionOptions: info.ageRestrictionOptions,
-    };
-  }, [exchangeHookDep]);
-
-  const [reviewing, setReviewing] = useState<boolean>(false);
-  const [reviewed, setReviewed] = useState<boolean>(false);
+): RecursiveState<State> {
+  const uriInfoHook = useAsyncAsHook(async () => {
+    const exchanges = await api.listExchanges();
+    return { amount: Amounts.parseOrThrow(amount), exchanges };
+  });
 
-  const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
-    undefined,
-  );
-  const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
+  console.log("uri info", uriInfoHook)
 
-  if (!exchangeHook) return { status: "loading", error: undefined };
-  if (exchangeHook.hasError) {
-    return {
-      status: "loading-uri",
-      error: exchangeHook,
-    };
-  }
+  if (!uriInfoHook) return { status: "loading", error: undefined };
 
-  if (!exchange) {
+  if (uriInfoHook.hasError) {
     return {
-      status: "loading-exchange",
-      error: {
-        hasError: true,
-        operational: false,
-        message: "ERROR_NO-DEFAULT-EXCHANGE",
-      },
+      status: "loading-error",
+      error: uriInfoHook,
     };
   }
 
-  async function doWithdrawAndCheckError(): Promise<void> {
-    if (!exchange) return;
+  const chosenAmount = uriInfoHook.response.amount;
+  const exchangeList = uriInfoHook.response.exchanges.exchanges
 
-    try {
-      setDoingWithdraw(true);
-
-      const response = await wxApi.acceptManualWithdrawal(
-        exchange.exchangeBaseUrl,
-        Amounts.stringify(amount),
-      );
-
-      onSuccess(response.transactionId);
-    } catch (e) {
-      if (e instanceof TalerError) {
-        setWithdrawError(e);
-      }
-    }
-    setDoingWithdraw(false);
-  }
-
-  if (!amountHook) {
-    return { status: "loading", error: undefined };
-  }
-  if (amountHook.hasError) {
+  async function doManualWithdraw(exchange: string, ageRestricted: number | 
undefined): Promise<{ transactionId: string, confirmTransferUrl: string | 
undefined }> {
+    const res = await api.acceptManualWithdrawal(exchange, 
Amounts.stringify(chosenAmount), ageRestricted);
     return {
-      status: "loading-info",
-      error: amountHook,
+      confirmTransferUrl: undefined,
+      transactionId: res.transactionId
     };
   }
-  if (!amountHook.response) {
-    return { status: "loading", error: undefined };
-  }
 
-  const withdrawalFee = Amounts.sub(
-    amountHook.response.amount.raw,
-    amountHook.response.amount.effective,
-  ).amount;
-  const toBeReceived = amountHook.response.amount.effective;
-
-  const { state: termsState } = (!terms
-    ? undefined
-    : terms.hasError
-    ? undefined
-    : terms.response) || { state: undefined };
-
-  async function onAccept(accepted: boolean): Promise<void> {
-    if (!termsState || !exchange) return;
-
-    try {
-      await api.setExchangeTosAccepted(
-        exchange.exchangeBaseUrl,
-        accepted ? termsState.version : undefined,
-      );
-      setReviewed(accepted);
-    } catch (e) {
-      if (e instanceof Error) {
-        //FIXME: uncomment this and display error
-        // setErrorAccepting(e.message);
-      }
-    }
-  }
-
-  const mustAcceptFirst =
-    termsState !== undefined &&
-    (termsState.status === "changed" || termsState.status === "new");
+  return () => exchangeSelectionState(doManualWithdraw, cancel, onSuccess, 
undefined, chosenAmount, exchangeList, undefined, api)
 
-  const ageRestrictionOptions =
-    amountHook.response.ageRestrictionOptions?.reduce(
-      (p, c) => ({ ...p, [c]: `under ${c}` }),
-      {} as Record<string, string>,
-    );
-
-  const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
-  if (ageRestrictionEnabled) {
-    ageRestrictionOptions["0"] = "Not restricted";
-  }
-
-  //TODO: calculate based on exchange info
-  const ageRestriction = ageRestrictionEnabled
-    ? {
-        list: ageRestrictionOptions,
-        value: String(ageRestricted),
-        onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
-      }
-    : undefined;
-
-  return {
-    status: "success",
-    error: undefined,
-    exchangeUrl: exchange.exchangeBaseUrl,
-    toBeReceived,
-    withdrawalFee,
-    chosenAmount,
-    ageRestriction,
-    doWithdrawal: {
-      onClick:
-        doingWithdraw || (mustAcceptFirst && !reviewed)
-          ? undefined
-          : doWithdrawAndCheckError,
-      error: withdrawError,
-    },
-    tosProps: !termsState
-      ? undefined
-      : {
-          onAccept,
-          onReview: setReviewing,
-          reviewed: reviewed,
-          reviewing: reviewing,
-          terms: termsState,
-        },
-    mustAcceptFirst,
-    cancel,
-  };
 }
 
 export function useComponentStateFromURI(
   { talerWithdrawUri, cancel, onSuccess }: PropsFromURI,
   api: typeof wxApi,
-): State {
-  const [ageRestricted, setAgeRestricted] = useState(0);
-
+): RecursiveState<State> {
   /**
    * Ask the wallet about the withdraw URI
    */
@@ -240,207 +75,219 @@ export function useComponentStateFromURI(
     const uriInfo = await api.getWithdrawalDetailsForUri({
       talerWithdrawUri,
     });
+    const exchanges = await api.listExchanges();
     const { amount, defaultExchangeBaseUrl } = uriInfo;
-    return { amount, thisExchange: defaultExchangeBaseUrl };
+    return { talerWithdrawUri, amount: Amounts.parseOrThrow(amount), 
thisExchange: defaultExchangeBaseUrl, exchanges };
   });
 
-  /**
-   * Get the amount and select one exchange
-   */
-  const uriHookDep =
-    !uriInfoHook || uriInfoHook.hasError || !uriInfoHook.response
-      ? undefined
-      : uriInfoHook.response;
-
-  /**
-   * For the exchange selected, bring the status of the terms of service
-   */
-  const terms = useAsyncAsHook(async () => {
-    if (!uriHookDep?.thisExchange) return false;
-
-    const exchangeTos = await api.getExchangeTos(uriHookDep.thisExchange, [
-      "text/xml",
-    ]);
-
-    const state = buildTermsOfServiceState(exchangeTos);
-
-    return { state };
-  }, [uriHookDep]);
-
-  /**
-   * With the exchange and amount, ask the wallet the information
-   * about the withdrawal
-   */
-  const amountHook = useAsyncAsHook(async () => {
-    if (!uriHookDep?.thisExchange) return false;
-
-    const info = await api.getExchangeWithdrawalInfo({
-      exchangeBaseUrl: uriHookDep?.thisExchange,
-      amount: Amounts.parseOrThrow(uriHookDep.amount),
-      tosAcceptedFormat: ["text/xml"],
-      ageRestricted,
-    });
-
-    const withdrawAmount = {
-      raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
-      effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
-    };
-
-    return {
-      amount: withdrawAmount,
-      ageRestrictionOptions: info.ageRestrictionOptions,
-    };
-  }, [uriHookDep]);
-
-  const [reviewing, setReviewing] = useState<boolean>(false);
-  const [reviewed, setReviewed] = useState<boolean>(false);
-
-  const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
-    undefined,
-  );
-  const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
-
+  console.log("uri info", uriInfoHook)
   if (!uriInfoHook) return { status: "loading", error: undefined };
+
   if (uriInfoHook.hasError) {
     return {
-      status: "loading-uri",
+      status: "loading-error",
       error: uriInfoHook,
     };
   }
 
-  const { amount, thisExchange } = uriInfoHook.response;
+  const uri = uriInfoHook.response.talerWithdrawUri;
+  const chosenAmount = uriInfoHook.response.amount;
+  const defaultExchange = uriInfoHook.response.thisExchange;
+  const exchangeList = uriInfoHook.response.exchanges.exchanges
 
-  const chosenAmount = Amounts.parseOrThrow(amount);
-
-  if (!thisExchange) {
+  async function doManagedWithdraw(exchange: string, ageRestricted: number | 
undefined): Promise<{ transactionId: string, confirmTransferUrl: string | 
undefined }> {
+    const res = await api.acceptWithdrawal(uri, exchange, ageRestricted,);
     return {
-      status: "loading-exchange",
-      error: {
-        hasError: true,
-        operational: false,
-        message: "ERROR_NO-DEFAULT-EXCHANGE",
-      },
+      confirmTransferUrl: res.confirmTransferUrl,
+      transactionId: res.transactionId
     };
   }
 
-  // const selectedExchange = thisExchange;
+  return () => exchangeSelectionState(doManagedWithdraw, cancel, onSuccess, 
uri, chosenAmount, exchangeList, defaultExchange, api)
 
-  async function doWithdrawAndCheckError(): Promise<void> {
-    if (!thisExchange) return;
+}
 
-    try {
-      setDoingWithdraw(true);
-      if (!talerWithdrawUri) return;
-      const res = await api.acceptWithdrawal(
-        talerWithdrawUri,
-        thisExchange,
-        !ageRestricted ? undefined : ageRestricted,
-      );
-      if (res.confirmTransferUrl) {
-        document.location.href = res.confirmTransferUrl;
-      } else {
-        onSuccess(res.transactionId);
-      }
-    } catch (e) {
-      if (e instanceof TalerError) {
-        setWithdrawError(e);
-      }
-    }
-    setDoingWithdraw(false);
-  }
+type ManualOrManagedWithdrawFunction = (exchange: string, ageRestricted: 
number | undefined) => Promise<{ transactionId: string, confirmTransferUrl: 
string | undefined }>
 
-  if (!amountHook) {
-    return { status: "loading", error: undefined };
-  }
-  if (amountHook.hasError) {
+function exchangeSelectionState(doWithdraw: ManualOrManagedWithdrawFunction, 
cancel: () => Promise<void>, onSuccess: (txid: string) => Promise<void>, 
talerWithdrawUri: string | undefined, chosenAmount: AmountJson, exchangeList: 
ExchangeListItem[], defaultExchange: string | undefined, api: typeof wxApi,): 
RecursiveState<State> {
+
+  //FIXME: use substates here
+  const selectedExchange = useSelectedExchange({ currency: 
chosenAmount.currency, defaultExchange, list: exchangeList })
+
+  if (selectedExchange.status === 'no-exchange') {
     return {
-      status: "loading-info",
-      error: amountHook,
-    };
+      status: "no-exchange",
+      error: undefined,
+    }
   }
-  if (!amountHook.response) {
-    return { status: "loading", error: undefined };
+
+  if (selectedExchange.status === 'selecting-exchange') {
+    return selectedExchange
   }
+  console.log("exchange selected", selectedExchange.selected)
+
+  return () => {
+
+    const [ageRestricted, setAgeRestricted] = useState(0);
+    const currentExchange = selectedExchange.selected
+    /**
+     * For the exchange selected, bring the status of the terms of service
+     */
+    const terms = useAsyncAsHook(async () => {
+      const exchangeTos = await 
api.getExchangeTos(currentExchange.exchangeBaseUrl, [
+        "text/xml",
+      ]);
+
+      const state = buildTermsOfServiceState(exchangeTos);
+
+      return { state };
+    }, []);
+    console.log("terms", terms)
+    /**
+     * With the exchange and amount, ask the wallet the information
+     * about the withdrawal
+     */
+    const amountHook = useAsyncAsHook(async () => {
+
+      const info = await api.getExchangeWithdrawalInfo({
+        exchangeBaseUrl: currentExchange.exchangeBaseUrl,
+        amount: chosenAmount,
+        tosAcceptedFormat: ["text/xml"],
+        ageRestricted,
+      });
+
+      const withdrawAmount = {
+        raw: Amounts.parseOrThrow(info.withdrawalAmountRaw),
+        effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
+      };
+
+      return {
+        amount: withdrawAmount,
+        ageRestrictionOptions: info.ageRestrictionOptions,
+      };
+    }, []);
+
+    const [reviewing, setReviewing] = useState<boolean>(false);
+    const [reviewed, setReviewed] = useState<boolean>(false);
+
+    const [withdrawError, setWithdrawError] = useState<TalerError | undefined>(
+      undefined,
+    );
+    const [doingWithdraw, setDoingWithdraw] = useState<boolean>(false);
+
+
+    async function doWithdrawAndCheckError(): Promise<void> {
+
+      try {
+        setDoingWithdraw(true);
+        const res = await doWithdraw(currentExchange.exchangeBaseUrl, 
!ageRestricted ? undefined : ageRestricted)
+        if (res.confirmTransferUrl) {
+          document.location.href = res.confirmTransferUrl;
+        } else {
+          onSuccess(res.transactionId);
+        }
+      } catch (e) {
+        if (e instanceof TalerError) {
+          setWithdrawError(e);
+        }
+      }
+      setDoingWithdraw(false);
+    }
 
-  const withdrawalFee = Amounts.sub(
-    amountHook.response.amount.raw,
-    amountHook.response.amount.effective,
-  ).amount;
-  const toBeReceived = amountHook.response.amount.effective;
-
-  const { state: termsState } = (!terms
-    ? undefined
-    : terms.hasError
-    ? undefined
-    : terms.response) || { state: undefined };
-
-  async function onAccept(accepted: boolean): Promise<void> {
-    if (!termsState || !thisExchange) return;
-
-    try {
-      await api.setExchangeTosAccepted(
-        thisExchange,
-        accepted ? termsState.version : undefined,
-      );
-      setReviewed(accepted);
-    } catch (e) {
-      if (e instanceof Error) {
-        //FIXME: uncomment this and display error
-        // setErrorAccepting(e.message);
+    if (!amountHook) {
+      return { status: "loading", error: undefined };
+    }
+    if (amountHook.hasError) {
+      return {
+        status: "loading-info",
+        error: amountHook,
+      };
+    }
+    if (!amountHook.response) {
+      return { status: "loading", error: undefined };
+    }
+
+    const withdrawalFee = Amounts.sub(
+      amountHook.response.amount.raw,
+      amountHook.response.amount.effective,
+    ).amount;
+    const toBeReceived = amountHook.response.amount.effective;
+
+    const { state: termsState } = (!terms
+      ? undefined
+      : terms.hasError
+        ? undefined
+        : terms.response) || { state: undefined };
+
+    async function onAccept(accepted: boolean): Promise<void> {
+      if (!termsState) return;
+
+      try {
+        await api.setExchangeTosAccepted(
+          currentExchange.exchangeBaseUrl,
+          accepted ? termsState.version : undefined,
+        );
+        setReviewed(accepted);
+      } catch (e) {
+        if (e instanceof Error) {
+          //FIXME: uncomment this and display error
+          // setErrorAccepting(e.message);
+        }
       }
     }
-  }
 
-  const mustAcceptFirst =
-    termsState !== undefined &&
-    (termsState.status === "changed" || termsState.status === "new");
+    const mustAcceptFirst =
+      termsState !== undefined &&
+      (termsState.status === "changed" || termsState.status === "new");
 
-  const ageRestrictionOptions =
-    amountHook.response.ageRestrictionOptions?.reduce(
-      (p, c) => ({ ...p, [c]: `under ${c}` }),
-      {} as Record<string, string>,
-    );
+    const ageRestrictionOptions =
+      amountHook.response.ageRestrictionOptions?.reduce(
+        (p, c) => ({ ...p, [c]: `under ${c}` }),
+        {} as Record<string, string>,
+      );
 
-  const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
-  if (ageRestrictionEnabled) {
-    ageRestrictionOptions["0"] = "Not restricted";
-  }
+    const ageRestrictionEnabled = ageRestrictionOptions !== undefined;
+    if (ageRestrictionEnabled) {
+      ageRestrictionOptions["0"] = "Not restricted";
+    }
 
-  //TODO: calculate based on exchange info
-  const ageRestriction = ageRestrictionEnabled
-    ? {
+    //TODO: calculate based on exchange info
+    const ageRestriction = ageRestrictionEnabled
+      ? {
         list: ageRestrictionOptions,
         value: String(ageRestricted),
         onChange: async (v: string) => setAgeRestricted(parseInt(v, 10)),
       }
-    : undefined;
-
-  return {
-    status: "success",
-    error: undefined,
-    exchangeUrl: thisExchange,
-    toBeReceived,
-    withdrawalFee,
-    chosenAmount,
-    talerWithdrawUri,
-    ageRestriction,
-    doWithdrawal: {
-      onClick:
-        doingWithdraw || (mustAcceptFirst && !reviewed)
-          ? undefined
-          : doWithdrawAndCheckError,
-      error: withdrawError,
-    },
-    tosProps: !termsState
-      ? undefined
-      : {
+      : undefined;
+
+    return {
+      status: "success",
+      error: undefined,
+      doSelectExchange: selectedExchange.doSelect,
+      exchangeUrl: currentExchange.exchangeBaseUrl,
+      toBeReceived,
+      withdrawalFee,
+      chosenAmount,
+      talerWithdrawUri,
+      ageRestriction,
+      doWithdrawal: {
+        onClick:
+          doingWithdraw || (mustAcceptFirst && !reviewed)
+            ? undefined
+            : doWithdrawAndCheckError,
+        error: withdrawError,
+      },
+      tosProps: !termsState
+        ? undefined
+        : {
           onAccept,
           onReview: setReviewing,
           reviewed: reviewed,
           reviewing: reviewing,
           terms: termsState,
         },
-    mustAcceptFirst,
-    cancel,
-  };
+      mustAcceptFirst,
+      cancel,
+    };
+  }
 }
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
index 2be4437cc..a3daeb5e9 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/stories.tsx
@@ -76,6 +76,8 @@ export const TermsOfServiceNotYetLoaded = 
createExample(SuccessView, {
     fraction: 10000000,
     value: 1,
   },
+  doSelectExchange: {
+  },
   toBeReceived: {
     currency: "USD",
     fraction: 0,
@@ -104,6 +106,8 @@ export const WithSomeFee = createExample(SuccessView, {
     fraction: 0,
     value: 1,
   },
+  doSelectExchange: {
+  },
   tosProps: normalTosState,
 });
 
@@ -123,6 +127,8 @@ export const WithoutFee = createExample(SuccessView, {
     fraction: 0,
     value: 0,
   },
+  doSelectExchange: {
+  },
   toBeReceived: {
     currency: "USD",
     fraction: 0,
@@ -147,6 +153,8 @@ export const EditExchangeUntouched = 
createExample(SuccessView, {
     fraction: 0,
     value: 0,
   },
+  doSelectExchange: {
+  },
   toBeReceived: {
     currency: "USD",
     fraction: 0,
@@ -171,6 +179,8 @@ export const EditExchangeModified = 
createExample(SuccessView, {
     fraction: 0,
     value: 0,
   },
+  doSelectExchange: {
+  },
   toBeReceived: {
     currency: "USD",
     fraction: 0,
@@ -188,6 +198,8 @@ export const WithAgeRestriction = 
createExample(SuccessView, {
     value: 2,
     fraction: 10000000,
   },
+  doSelectExchange: {
+  },
   doWithdrawal: nullHandler,
   exchangeUrl: "https://exchange.demo.taler.net";,
   mustAcceptFirst: false,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts 
b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
index f614c1c8c..5c62671fe 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -29,6 +29,7 @@ import { ExchangeWithdrawDetails } from 
"@gnu-taler/taler-wallet-core";
 import { expect } from "chai";
 import { mountHook } from "../../test-utils.js";
 import { useComponentStateFromURI } from "./state.js";
+import * as wxApi from "../../wxApi.js";
 
 const exchanges: ExchangeFullDetails[] = [
   {
@@ -92,7 +93,7 @@ describe("Withdraw CTA states", () => {
     {
       const { status, error } = getLastResultOrThrow();
 
-      if (status != "loading-uri") expect.fail();
+      if (status != "loading-error") expect.fail();
       if (!error) expect.fail();
       if (!error.hasError) expect.fail();
       if (error.operational) expect.fail();
@@ -127,7 +128,7 @@ describe("Withdraw CTA states", () => {
 
     {
       const { status } = getLastResultOrThrow();
-      expect(status).equals("loading");
+      expect(status).equals("loading", "1");
     }
 
     await waitNextUpdate();
@@ -135,13 +136,9 @@ describe("Withdraw CTA states", () => {
     {
       const { status, error } = getLastResultOrThrow();
 
-      expect(status).equals("loading-exchange");
+      expect(status).equals("no-exchange", "3");
 
-      expect(error).deep.equals({
-        hasError: true,
-        operational: false,
-        message: "ERROR_NO-DEFAULT-EXCHANGE",
-      });
+      expect(error).undefined;
     }
 
     await assertNoPendingUpdate();
@@ -169,10 +166,10 @@ describe("Withdraw CTA states", () => {
             }),
             getExchangeWithdrawalInfo:
               async (): Promise<ExchangeWithdrawDetails> =>
-                ({
-                  withdrawalAmountRaw: "ARS:2",
-                  withdrawalAmountEffective: "ARS:2",
-                } as any),
+              ({
+                withdrawalAmountRaw: "ARS:2",
+                withdrawalAmountEffective: "ARS:2",
+              } as any),
             getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
               contentType: "text",
               content: "just accept",
@@ -246,10 +243,10 @@ describe("Withdraw CTA states", () => {
             }),
             getExchangeWithdrawalInfo:
               async (): Promise<ExchangeWithdrawDetails> =>
-                ({
-                  withdrawalAmountRaw: "ARS:2",
-                  withdrawalAmountEffective: "ARS:2",
-                } as any),
+              ({
+                withdrawalAmountRaw: "ARS:2",
+                withdrawalAmountEffective: "ARS:2",
+              } as any),
             getExchangeTos: async (): Promise<GetExchangeTosResult> => ({
               contentType: "text",
               content: "just accept",
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
index 60157d289..82d6090e5 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/views.tsx
@@ -38,6 +38,7 @@ import editIcon from "../../svg/edit_24px.svg";
 import { Amount } from "../../components/Amount.js";
 import { QR } from "../../components/QR.js";
 import { useState } from "preact/hooks";
+import { ErrorMessage } from "../../components/ErrorMessage.js";
 
 export function LoadingUriView({ error }: State.LoadingUriError): VNode {
   const { i18n } = useTranslationContext();
@@ -52,15 +53,12 @@ export function LoadingUriView({ error }: 
State.LoadingUriError): VNode {
   );
 }
 
-export function LoadingExchangeView({
-  error,
-}: State.LoadingExchangeError): VNode {
+export function LoadingExchangeView(p: State.LoadingExchangeError): VNode {
   const { i18n } = useTranslationContext();
 
   return (
-    <LoadingError
-      title={<i18n.Translate>Could not get exchange</i18n.Translate>}
-      error={error}
+    <ErrorMessage 
+      title={<i18n.Translate>Could not get a default exchange, please check 
configuration</i18n.Translate>}
     />
   );
 }
@@ -106,13 +104,13 @@ export function SuccessView(state: State.Success): VNode {
               }}
             >
               <i18n.Translate>Exchange</i18n.Translate>
-              {/* <Link>
+              <Button onClick={state.doSelectExchange.onClick} variant="text">
                 <SvgIcon
                   title="Edit"
                   dangerouslySetInnerHTML={{ __html: editIcon }}
                   color="black"
                 />
-              </Link> */}
+              </Button>
             </div>
           }
           text={<ExchangeDetails exchange={state.exchangeUrl} />}
diff --git 
a/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts 
b/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts
new file mode 100644
index 000000000..d9085153e
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/hooks/useSelectedExchange.ts
@@ -0,0 +1,125 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { ExchangeListItem } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
+import { ButtonHandler } from "../mui/handlers.js";
+
+type State = State.Ready | State.NoExchange | State.Selecting;
+
+export namespace State {
+  export interface NoExchange {
+    status: "no-exchange"
+    error: undefined;
+  }
+  export interface Ready {
+    status: "ready",
+    doSelect: ButtonHandler,
+    selected: ExchangeListItem;
+  }
+  export interface Selecting {
+    status: "selecting-exchange",
+    error: undefined,
+    onSelection: (url: string) => Promise<void>;
+    onCancel: () => Promise<void>;
+    list: ExchangeListItem[],
+    currency: string;
+    currentExchange: string;
+  }
+}
+
+interface Props {
+  currency: string;
+  //there is a preference for the default at the initial state
+  defaultExchange?: string,
+  //list of exchanges
+  list: ExchangeListItem[],
+}
+
+
+
+export function useSelectedExchange({ currency, defaultExchange, list }: 
Props): State {
+  const [isSelecting, setIsSelecting] = useState(false);
+  const [selectedExchange, setSelectedExchange] = useState<string | 
undefined>(undefined);
+
+  if (!list.length) {
+    return {
+      status: "no-exchange",
+      error: undefined,
+    }
+  }
+
+  const firstByCurrency = list.find((e) => e.currency === currency)
+  if (!firstByCurrency) {
+    // there should be at least one exchange for this currency
+    return {
+      status: "no-exchange",
+      error: undefined,
+    }
+  }
+
+
+  if (isSelecting) {
+    const currentExchange = selectedExchange ?? defaultExchange ?? 
firstByCurrency.exchangeBaseUrl;
+    return {
+      status: "selecting-exchange",
+      error: undefined,
+      list,
+      currency,
+      currentExchange: currentExchange,
+      onSelection: async (exchangeBaseUrl: string) => {
+        setIsSelecting(false);
+        setSelectedExchange(exchangeBaseUrl)
+      },
+      onCancel: async () => {
+        setIsSelecting(false);
+      }
+    }
+  }
+
+  {
+    const found = !selectedExchange ? undefined : list.find(
+      (e) => e.exchangeBaseUrl === selectedExchange,
+    )
+    if (found) return {
+      status: "ready",
+      doSelect: {
+        onClick: async () => setIsSelecting(true)
+      },
+      selected: found
+    };
+  }
+  {
+    const found = !defaultExchange ? undefined : list.find(
+      (e) => e.exchangeBaseUrl === defaultExchange,
+    )
+    if (found) return {
+      status: "ready",
+      doSelect: {
+        onClick: async () => setIsSelecting(true)
+      },
+      selected: found
+    };
+  }
+
+  return {
+    status: "ready",
+    doSelect: {
+      onClick: async () => setIsSelecting(true)
+    },
+    selected: firstByCurrency
+  }
+}
diff --git a/packages/taler-wallet-webextension/src/test-utils.ts 
b/packages/taler-wallet-webextension/src/test-utils.ts
index eebfa3612..7e9c5670e 100644
--- a/packages/taler-wallet-webextension/src/test-utils.ts
+++ b/packages/taler-wallet-webextension/src/test-utils.ts
@@ -82,31 +82,38 @@ export function renderNodeOrBrowser(Component: any, args: 
any): void {
     document.body.removeChild(div);
   }
 }
+type RecursiveState<S> = S | (() => RecursiveState<S>)
 
 interface Mounted<T> {
   unmount: () => void;
-  getLastResultOrThrow: () => T;
+  getLastResultOrThrow: () => Exclude<T, VoidFunction>;
   assertNoPendingUpdate: () => void;
   waitNextUpdate: (s?: string) => Promise<void>;
 }
 
 const isNode = typeof window === "undefined";
 
-export function mountHook<T>(
-  callback: () => T,
+export function mountHook<T extends object>(
+  callback: () => RecursiveState<T>,
   Context?: ({ children }: { children: any }) => VNode,
 ): Mounted<T> {
   // const result: { current: T | null } = {
   //   current: null
   // }
-  let lastResult: T | Error | null = null;
+  let lastResult: Exclude<T, VoidFunction> | Error | null = null;
 
   const listener: Array<() => void> = [];
 
   // component that's going to hold the hook
   function Component(): VNode {
     try {
-      lastResult = callback();
+      let componentOrResult = callback()
+      while (typeof componentOrResult === "function") {
+        componentOrResult = componentOrResult();
+      }
+      //typecheck fails here
+      const l: Exclude<T, () => void> = componentOrResult as any
+      lastResult = l;
     } catch (e) {
       if (e instanceof Error) {
         lastResult = e;
@@ -157,13 +164,13 @@ export function mountHook<T>(
     }
   }
 
-  function getLastResult(): T | Error | null {
-    const copy = lastResult;
+  function getLastResult(): Exclude<T | Error | null, VoidFunction> {
+    const copy: Exclude<T | Error | null, VoidFunction> = lastResult;
     lastResult = null;
     return copy;
   }
 
-  function getLastResultOrThrow(): T {
+  function getLastResultOrThrow(): Exclude<T, VoidFunction> {
     const r = getLastResult();
     if (r instanceof Error) throw r;
     if (!r) throw Error("there was no last result");
diff --git a/packages/taler-wallet-webextension/src/utils/index.ts 
b/packages/taler-wallet-webextension/src/utils/index.ts
index 8fe1f2a44..3535910cf 100644
--- a/packages/taler-wallet-webextension/src/utils/index.ts
+++ b/packages/taler-wallet-webextension/src/utils/index.ts
@@ -19,7 +19,7 @@ import {
   Amounts,
   GetExchangeTosResult,
 } from "@gnu-taler/taler-util";
-import { VNode } from "preact";
+import { VNode, createElement } from "preact";
 
 function getJsonIfOk(r: Response): Promise<any> {
   if (r.ok) {
@@ -31,8 +31,7 @@ function getJsonIfOk(r: Response): Promise<any> {
   }
 
   throw new Error(
-    `Try another server: (${r.status}) ${
-      r.statusText || "internal server error"
+    `Try another server: (${r.status}) ${r.statusText || "internal server 
error"
     }`,
   );
 }
@@ -103,10 +102,10 @@ export function buildTermsOfServiceStatus(
   return !content
     ? "notfound"
     : !acceptedVersion
-    ? "new"
-    : acceptedVersion !== currentVersion
-    ? "changed"
-    : "accepted";
+      ? "new"
+      : acceptedVersion !== currentVersion
+        ? "changed"
+        : "accepted";
 }
 
 function parseTermsOfServiceContent(
@@ -198,17 +197,35 @@ export type StateViewMap<StateType extends { status: 
string }> = {
   [S in StateType as S["status"]]: StateFunc<S>;
 };
 
+type RecursiveState<S extends object> = S | (() => RecursiveState<S>)
+
 export function compose<SType extends { status: string }, PType>(
   name: string,
-  hook: (p: PType) => SType,
-  vs: StateViewMap<SType>,
+  hook: (p: PType) => RecursiveState<SType>,
+  viewMap: StateViewMap<SType>,
 ): (p: PType) => VNode {
-  const Component = (p: PType): VNode => {
-    const state = hook(p);
-    const s = state.status as unknown as SType["status"];
-    const c = vs[s] as unknown as StateFunc<SType>;
-    return c(state);
+
+  function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
+
+    function TheComponent(): VNode {
+      const state = stateHook();
+
+      if (typeof state === "function") {
+        const subComponent = withHook(state)
+        return createElement(subComponent, {});
+      }
+
+      const statusName = state.status as unknown as SType["status"];
+      const viewComponent = viewMap[statusName] as unknown as StateFunc<SType>;
+      return createElement(viewComponent, state);
+    }
+    TheComponent.name = `${name}`;
+
+    return TheComponent;
+  }
+
+  return (p: PType) => {
+    const h = withHook(() => hook(p))
+    return h()
   };
-  Component.name = `${name}`;
-  return Component;
 }
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts 
b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
index 3b2708eff..2834028c6 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/index.ts
@@ -20,6 +20,7 @@ import {
   AbsoluteTime,
   ExchangeFullDetails,
   OperationMap,
+  ExchangeListItem,
 } from "@gnu-taler/taler-util";
 import { Loading } from "../../components/Loading.js";
 import { HookError } from "../../hooks/useAsyncAsHook.js";
@@ -29,13 +30,14 @@ import * as wxApi from "../../wxApi.js";
 import { useComponentState } from "./state.js";
 import {
   ComparingView,
-  LoadingUriView,
+  ErrorLoadingView,
   NoExchangesView,
   ReadyView,
 } from "./views.js";
 
 export interface Props {
-  currency?: string;
+  list: ExchangeListItem[],
+  currentExchange: string,
   onCancel: () => Promise<void>;
   onSelection: (exchange: string) => Promise<void>;
 }
@@ -54,7 +56,7 @@ export namespace State {
   }
 
   export interface LoadingUriError {
-    status: "loading-uri";
+    status: "error-loading";
     error: HookError;
   }
 
@@ -85,7 +87,7 @@ export namespace State {
 
 const viewMapping: StateViewMap<State> = {
   loading: Loading,
-  "loading-uri": LoadingUriView,
+  "error-loading": ErrorLoadingView,
   comparing: ComparingView,
   "no-exchanges": NoExchangesView,
   ready: ReadyView,
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts 
b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
index 8c0c21486..db6138f8e 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/state.ts
@@ -22,14 +22,17 @@ import * as wxApi from "../../wxApi.js";
 import { Props, State } from "./index.js";
 
 export function useComponentState(
-  { onCancel, onSelection, currency }: Props,
+  { onCancel, onSelection, list: exchanges, currentExchange }: Props,
   api: typeof wxApi,
 ): State {
-  const initialValue = 0;
+  const initialValue = exchanges.findIndex(e => e.exchangeBaseUrl === 
currentExchange);
+  if (initialValue === -1) {
+    throw Error(`wrong usage of ExchangeSelection component, currentExchange 
'${currentExchange}' is not in the list of exchanges`)
+  }
   const [value, setValue] = useState(String(initialValue));
 
   const hook = useAsyncAsHook(async () => {
-    const { exchanges } = await api.listExchanges();
+    // const { exchanges } = await api.listExchanges();
 
     const selectedIdx = parseInt(value, 10);
     const selectedExchange =
@@ -54,12 +57,12 @@ export function useComponentState(
   }
   if (hook.hasError) {
     return {
-      status: "loading-uri",
+      status: "error-loading",
       error: hook,
     };
   }
 
-  const { exchanges, selected, original } = hook.response;
+  const { selected, original } = hook.response;
 
   if (!selected) {
     //!selected <=> exchanges.length === 0
diff --git 
a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx 
b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
index 4cd90700f..dd85dff46 100644
--- a/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ExchangeSelection/views.tsx
@@ -101,7 +101,7 @@ const Container = styled.div`
   }
 `;
 
-export function LoadingUriView({ error }: State.LoadingUriError): VNode {
+export function ErrorLoadingView({ error }: State.LoadingUriError): VNode {
   const { i18n } = useTranslationContext();
 
   return (

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