gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated (1a06f670 -> c5f484d1)


From: gnunet
Subject: [taler-wallet-core] branch master updated (1a06f670 -> c5f484d1)
Date: Fri, 22 Apr 2022 21:10:56 +0200

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

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

    from 1a06f670 console to logger
     new 8e468ae0 fix segwit api
     new c5f484d1 deposit test case

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


Summary of changes:
 packages/taler-wallet-cli/src/index.ts             |   3 -
 packages/taler-wallet-webextension/dev.mjs         |  67 ++++
 packages/taler-wallet-webextension/package.json    |   4 +-
 .../taler-wallet-webextension/serve-esbuild.mjs    |  24 --
 .../src/cta/Deposit.stories.tsx                    | 140 +-------
 .../taler-wallet-webextension/src/cta/Deposit.tsx  | 239 ++------------
 packages/taler-wallet-webextension/src/cta/Pay.tsx |  28 +-
 .../src/cta/Withdraw.stories.tsx                   |  20 +-
 .../taler-wallet-webextension/src/cta/Withdraw.tsx |   7 +-
 .../taler-wallet-webextension/src/mui/handlers.ts  |  21 ++
 packages/taler-wallet-webextension/src/stories.tsx |  69 +++-
 .../src/wallet/CreateManualWithdraw.test.ts        |   3 +-
 .../src/wallet/CreateManualWithdraw.tsx            |  26 +-
 .../src/wallet/DepositPage.stories.tsx             |  60 +++-
 .../src/wallet/DepositPage.test.ts                 | 362 ++++++++++++++++++++-
 .../src/wallet/DepositPage.tsx                     | 308 ++++++++++--------
 pnpm-lock.yaml                                     |  17 +
 17 files changed, 790 insertions(+), 608 deletions(-)
 create mode 100755 packages/taler-wallet-webextension/dev.mjs
 delete mode 100755 packages/taler-wallet-webextension/serve-esbuild.mjs
 create mode 100644 packages/taler-wallet-webextension/src/mui/handlers.ts

diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index fca6bf67..7cff0df8 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -749,9 +749,6 @@ advancedCli
   .requiredArgument("reservePub", clk.STRING)
   .action(async (args) => {
     const p = parsePaytoUri(args.genSegwit.paytoUri);
-    if (p?.isKnown && p?.targetType === "bitcoin") {
-      p.generateSegwitAddress(args.genSegwit.reservePub);
-    }
     console.log(p);
   });
 
diff --git a/packages/taler-wallet-webextension/dev.mjs 
b/packages/taler-wallet-webextension/dev.mjs
new file mode 100755
index 00000000..6c88f8a2
--- /dev/null
+++ b/packages/taler-wallet-webextension/dev.mjs
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+/* eslint-disable no-undef */
+
+import linaria from '@linaria/esbuild'
+import esbuild from 'esbuild'
+import { buildConfig } from "./build-fast-with-linaria.mjs"
+import fs from 'fs';
+import WebSocket from "ws";
+import chokidar from "chokidar";
+import path from "path"
+
+const devServerBroadcastDelay = 500
+const devServerPort = 8002
+const wss = new WebSocket.Server({ port: devServerPort });
+const toWatch = ["./src"]
+
+function broadcast(file, event) {
+  setTimeout(() => {
+    wss.clients.forEach((client) => {
+      if (client.readyState === WebSocket.OPEN) {
+        console.log(new Date(), file)
+        client.send(JSON.stringify(event));
+      }
+    });
+  }, devServerBroadcastDelay);
+}
+wss.addListener("connection", () => {
+  console.log("new client")
+})
+
+const watcher = chokidar
+  .watch(toWatch, {
+    persistent: true,
+    ignoreInitial: true,
+    awaitWriteFinish: {
+      stabilityThreshold: 100,
+      pollInterval: 100,
+    },
+  })
+  .on("error", (error) => console.error(error))
+  .on("change", async (file) => {
+    broadcast(file, { type: "RELOAD" });
+  })
+  .on("add", async (file) => {
+    broadcast(file, { type: "RELOAD" });
+  })
+  .on("unlink", async (file) => {
+    broadcast(file, { type: "RELOAD" });
+  });
+
+
+fs.writeFileSync("dev-html/manifest.json", fs.readFileSync("manifest-v2.json"))
+fs.writeFileSync("dev-html/mocha.css", 
fs.readFileSync("node_modules/mocha/mocha.css"))
+fs.writeFileSync("dev-html/mocha.js", 
fs.readFileSync("node_modules/mocha/mocha.js"))
+fs.writeFileSync("dev-html/mocha.js.map", 
fs.readFileSync("node_modules/mocha/mocha.js.map"))
+
+const server = await esbuild
+  .serve({ servedir: 'dev-html' }, {
+    ...buildConfig, outdir: 'dev-html/dist'
+  })
+  .catch((e) => {
+    console.log(e)
+    process.exit(1)
+  });
+
+console.log("ready!", server.port);
+
diff --git a/packages/taler-wallet-webextension/package.json 
b/packages/taler-wallet-webextension/package.json
index 1293c2b2..bf586834 100644
--- a/packages/taler-wallet-webextension/package.json
+++ b/packages/taler-wallet-webextension/package.json
@@ -29,7 +29,8 @@
     "preact": "^10.6.5",
     "preact-router": "3.2.1",
     "qrcode-generator": "^1.4.4",
-    "tslib": "^2.3.1"
+    "tslib": "^2.3.1",
+    "ws": "7.4.5"
   },
   "devDependencies": {
     "@babel/core": "7.13.16",
@@ -59,6 +60,7 @@
     "babel-loader": "^8.2.3",
     "babel-plugin-transform-react-jsx": "^6.24.1",
     "chai": "^4.3.6",
+    "chokidar": "^3.5.3",
     "mocha": "^9.2.0",
     "nyc": "^15.1.0",
     "polished": "^4.1.4",
diff --git a/packages/taler-wallet-webextension/serve-esbuild.mjs 
b/packages/taler-wallet-webextension/serve-esbuild.mjs
deleted file mode 100755
index 68dff2c2..00000000
--- a/packages/taler-wallet-webextension/serve-esbuild.mjs
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env node
-/* eslint-disable no-undef */
-
-import linaria from '@linaria/esbuild'
-import esbuild from 'esbuild'
-import { buildConfig } from "./build-fast-with-linaria.mjs"
-import fs from 'fs';
-
-fs.writeFileSync("dev-html/manifest.json", fs.readFileSync("manifest-v2.json"))
-fs.writeFileSync("dev-html/mocha.css", 
fs.readFileSync("node_modules/mocha/mocha.css"))
-fs.writeFileSync("dev-html/mocha.js", 
fs.readFileSync("node_modules/mocha/mocha.js"))
-fs.writeFileSync("dev-html/mocha.js.map", 
fs.readFileSync("node_modules/mocha/mocha.js.map"))
-
-const server = await esbuild
-  .serve({
-    servedir: 'dev-html',
-  }, { ...buildConfig, outdir: 'dev-html/dist' })
-  .catch((e) => {
-    console.log(e)
-    process.exit(1)
-  });
-
-console.log("ready!", server.port);
-
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx
index 923ea9e9..6432d532 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit.stories.tsx
@@ -21,7 +21,7 @@
 
 import { ContractTerms, PreparePayResultType } from "@gnu-taler/taler-util";
 import { createExample } from "../test-utils.js";
-import { PaymentRequestView as TestedComponent } from "./Deposit.js";
+import { View as TestedComponent } from "./Deposit.js";
 
 export default {
   title: "cta/deposit",
@@ -29,140 +29,6 @@ export default {
   argTypes: {},
 };
 
-export const NoBalance = createExample(TestedComponent, {
-  payStatus: {
-    status: PreparePayResultType.InsufficientBalance,
-    noncePriv: "",
-    proposalId: "proposal1234",
-    contractTerms: {
-      merchant: {
-        name: "someone",
-      },
-      summary: "some beers",
-      amount: "USD:10",
-    } as Partial<ContractTerms> as any,
-    amountRaw: "USD:10",
-  },
-});
-
-export const NoEnoughBalance = createExample(TestedComponent, {
-  payStatus: {
-    status: PreparePayResultType.InsufficientBalance,
-    noncePriv: "",
-    proposalId: "proposal1234",
-    contractTerms: {
-      merchant: {
-        name: "someone",
-      },
-      summary: "some beers",
-      amount: "USD:10",
-    } as Partial<ContractTerms> as any,
-    amountRaw: "USD:10",
-  },
-  balance: {
-    currency: "USD",
-    fraction: 40000000,
-    value: 9,
-  },
-});
-
-export const PaymentPossible = createExample(TestedComponent, {
-  uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
-  payStatus: {
-    status: PreparePayResultType.PaymentPossible,
-    amountEffective: "USD:10",
-    amountRaw: "USD:10",
-    noncePriv: "",
-    contractTerms: {
-      nonce: "123213123",
-      merchant: {
-        name: "someone",
-      },
-      amount: "USD:10",
-      summary: "some beers",
-    } as Partial<ContractTerms> as any,
-    contractTermsHash: "123456",
-    proposalId: "proposal1234",
-  },
-});
-
-export const PaymentPossibleWithFee = createExample(TestedComponent, {
-  uri: 
"taler://pay/merchant-backend.taler/2021.242-01G2X4275RBWG/?c=66BE594PDZR24744J6EQK52XM0",
-  payStatus: {
-    status: PreparePayResultType.PaymentPossible,
-    amountEffective: "USD:10.20",
-    amountRaw: "USD:10",
-    noncePriv: "",
-    contractTerms: {
-      nonce: "123213123",
-      merchant: {
-        name: "someone",
-      },
-      amount: "USD:10",
-      summary: "some beers",
-    } as Partial<ContractTerms> as any,
-    contractTermsHash: "123456",
-    proposalId: "proposal1234",
-  },
-});
-
-export const AlreadyConfirmedWithFullfilment = createExample(TestedComponent, {
-  payStatus: {
-    status: PreparePayResultType.AlreadyConfirmed,
-    amountEffective: "USD:10",
-    amountRaw: "USD:10",
-    contractTerms: {
-      merchant: {
-        name: "someone",
-      },
-      fulfillment_message:
-        "congratulations! you are looking at the fulfillment message! ",
-      summary: "some beers",
-      amount: "USD:10",
-    } as Partial<ContractTerms> as any,
-    contractTermsHash: "123456",
-    proposalId: "proposal1234",
-    paid: false,
-  },
-});
-
-export const AlreadyConfirmedWithoutFullfilment = createExample(
-  TestedComponent,
-  {
-    payStatus: {
-      status: PreparePayResultType.AlreadyConfirmed,
-      amountEffective: "USD:10",
-      amountRaw: "USD:10",
-      contractTerms: {
-        merchant: {
-          name: "someone",
-        },
-        summary: "some beers",
-        amount: "USD:10",
-      } as Partial<ContractTerms> as any,
-      contractTermsHash: "123456",
-      proposalId: "proposal1234",
-      paid: false,
-    },
-  },
-);
-
-export const AlreadyPaid = createExample(TestedComponent, {
-  payStatus: {
-    status: PreparePayResultType.AlreadyConfirmed,
-    amountEffective: "USD:10",
-    amountRaw: "USD:10",
-    contractTerms: {
-      merchant: {
-        name: "someone",
-      },
-      fulfillment_message:
-        "congratulations! you are looking at the fulfillment message! ",
-      summary: "some beers",
-      amount: "USD:10",
-    } as Partial<ContractTerms> as any,
-    contractTermsHash: "123456",
-    proposalId: "proposal1234",
-    paid: true,
-  },
+export const Simple = createExample(TestedComponent, {
+  state: { status: "ready" },
 });
diff --git a/packages/taler-wallet-webextension/src/cta/Deposit.tsx 
b/packages/taler-wallet-webextension/src/cta/Deposit.tsx
index 541bc733..23c557b0 100644
--- a/packages/taler-wallet-webextension/src/cta/Deposit.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Deposit.tsx
@@ -39,6 +39,8 @@ import { TalerError } from "@gnu-taler/taler-wallet-core";
 import { Fragment, h, VNode } from "preact";
 import { useEffect, useState } from "preact/hooks";
 import { ErrorTalerOperation } from "../components/ErrorTalerOperation.js";
+import { Loading } from "../components/Loading.js";
+import { LoadingError } from "../components/LoadingError.js";
 import { LogoHeader } from "../components/LogoHeader.js";
 import { Part } from "../components/Part.js";
 import {
@@ -49,157 +51,50 @@ import {
   WarningBox,
 } from "../components/styled/index.js";
 import { useTranslationContext } from "../context/translation.js";
-import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
+import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
 import * as wxApi from "../wxApi.js";
 
 interface Props {
-  talerPayUri?: string;
+  talerDepositUri?: string;
   goBack: () => void;
 }
 
-export function DepositPage({ talerPayUri, goBack }: Props): VNode {
-  const { i18n } = useTranslationContext();
-  const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(
-    undefined,
-  );
-  const [payResult, setPayResult] = useState<ConfirmPayResult | undefined>(
-    undefined,
-  );
-  const [payErrMsg, setPayErrMsg] = useState<TalerError | string | undefined>(
-    undefined,
-  );
-
-  const balance = useAsyncAsHook(wxApi.getBalance, [
-    NotificationType.CoinWithdrawn,
-  ]);
-  const balanceWithoutError = balance?.hasError
-    ? []
-    : balance?.response.balances || [];
-
-  const foundBalance = balanceWithoutError.find(
-    (b) =>
-      payStatus &&
-      Amounts.parseOrThrow(b.available).currency ===
-        Amounts.parseOrThrow(payStatus?.amountRaw).currency,
-  );
-  const foundAmount = foundBalance
-    ? Amounts.parseOrThrow(foundBalance.available)
-    : undefined;
-  // We use a string here so that dependency tracking for useEffect works 
properly
-  const foundAmountStr = foundAmount
-    ? Amounts.stringify(foundAmount)
-    : undefined;
+type State = Loading | Ready;
+interface Loading {
+  status: "loading";
+  hook: HookError | undefined;
+}
+interface Ready {
+  status: "ready";
+}
 
-  useEffect(() => {
-    if (!talerPayUri) return;
-    const doFetch = async (): Promise<void> => {
-      try {
-        const p = await wxApi.preparePay(talerPayUri);
-        setPayStatus(p);
-      } catch (e) {
-        console.log("Got error while trying to pay", e);
-        if (e instanceof TalerError) {
-          setPayErrMsg(e);
-        }
-        if (e instanceof Error) {
-          setPayErrMsg(e.message);
-        }
-      }
-    };
-    doFetch();
-  }, [talerPayUri, foundAmountStr]);
+function useComponentState(uri: string | undefined): State {
+  return {
+    status: "loading",
+    hook: undefined,
+  };
+}
 
-  if (!talerPayUri) {
-    return (
-      <span>
-        <i18n.Translate>missing pay uri</i18n.Translate>
-      </span>
-    );
-  }
+export function DepositPage({ talerDepositUri, goBack }: Props): VNode {
+  const { i18n } = useTranslationContext();
 
-  if (!payStatus) {
-    if (payErrMsg instanceof TalerError) {
-      return (
-        <WalletAction>
-          <LogoHeader />
-          <SubTitle>
-            <i18n.Translate>Digital cash payment</i18n.Translate>
-          </SubTitle>
-          <section>
-            <ErrorTalerOperation
-              title={
-                <i18n.Translate>
-                  Could not get the payment information for this order
-                </i18n.Translate>
-              }
-              error={payErrMsg?.errorDetail}
-            />
-          </section>
-        </WalletAction>
-      );
-    }
-    if (payErrMsg) {
-      return (
-        <WalletAction>
-          <LogoHeader />
-          <SubTitle>
-            <i18n.Translate>Digital cash payment</i18n.Translate>
-          </SubTitle>
-          <section>
-            <p>
-              <i18n.Translate>
-                Could not get the payment information for this order
-              </i18n.Translate>
-            </p>
-            <ErrorBox>{payErrMsg}</ErrorBox>
-          </section>
-        </WalletAction>
-      );
-    }
+  const state = useComponentState(talerDepositUri);
+  if (state.status === "loading") {
+    if (!state.hook) return <Loading />;
     return (
-      <span>
-        <i18n.Translate>Loading payment information</i18n.Translate> ...
-      </span>
+      <LoadingError
+        title={<i18n.Translate>Could not load pay status</i18n.Translate>}
+        error={state.hook}
+      />
     );
   }
-
-  const onClick = async (): Promise<void> => {
-    // try {
-    //   const res = await doPayment(payStatus);
-    //   setPayResult(res);
-    // } catch (e) {
-    //   console.error(e);
-    //   if (e instanceof Error) {
-    //     setPayErrMsg(e.message);
-    //   }
-    // }
-  };
-
-  return (
-    <PaymentRequestView
-      uri={talerPayUri}
-      payStatus={payStatus}
-      payResult={payResult}
-      onClick={onClick}
-      balance={foundAmount}
-    />
-  );
+  return <View state={state} />;
 }
 
-export interface PaymentRequestViewProps {
-  payStatus: PreparePayResult;
-  payResult?: ConfirmPayResult;
-  onClick: () => void;
-  payErrMsg?: string;
-  uri: string;
-  balance: AmountJson | undefined;
+export interface ViewProps {
+  state: State;
 }
-export function PaymentRequestView({
-  payStatus,
-  payResult,
-}: PaymentRequestViewProps): VNode {
-  const totalFees: AmountJson = Amounts.getZero(payStatus.amountRaw);
-  const contractTerms: ContractTerms = payStatus.contractTerms;
+export function View({ state }: ViewProps): VNode {
   const { i18n } = useTranslationContext();
 
   return (
@@ -209,78 +104,6 @@ export function PaymentRequestView({
       <SubTitle>
         <i18n.Translate>Digital cash deposit</i18n.Translate>
       </SubTitle>
-      {payStatus.status === PreparePayResultType.AlreadyConfirmed &&
-        (payStatus.paid ? (
-          <SuccessBox>
-            <i18n.Translate>Already paid</i18n.Translate>
-          </SuccessBox>
-        ) : (
-          <WarningBox>
-            <i18n.Translate>Already claimed</i18n.Translate>
-          </WarningBox>
-        ))}
-      {payResult && payResult.type === ConfirmPayResultType.Done && (
-        <SuccessBox>
-          <h3>
-            <i18n.Translate>Payment complete</i18n.Translate>
-          </h3>
-          <p>
-            {!payResult.contractTerms.fulfillment_message ? (
-              <i18n.Translate>
-                You will now be sent back to the merchant you came from.
-              </i18n.Translate>
-            ) : (
-              payResult.contractTerms.fulfillment_message
-            )}
-          </p>
-        </SuccessBox>
-      )}
-      <section>
-        {payStatus.status !== PreparePayResultType.InsufficientBalance &&
-          Amounts.isNonZero(totalFees) && (
-            <Part
-              big
-              title={<i18n.Translate>Total to pay</i18n.Translate>}
-              text={amountToPretty(
-                Amounts.parseOrThrow(payStatus.amountEffective),
-              )}
-              kind="negative"
-            />
-          )}
-        <Part
-          big
-          title={<i18n.Translate>Purchase amount</i18n.Translate>}
-          text={amountToPretty(Amounts.parseOrThrow(payStatus.amountRaw))}
-          kind="neutral"
-        />
-        {Amounts.isNonZero(totalFees) && (
-          <Fragment>
-            <Part
-              big
-              title={<i18n.Translate>Fee</i18n.Translate>}
-              text={amountToPretty(totalFees)}
-              kind="negative"
-            />
-          </Fragment>
-        )}
-        <Part
-          title={<i18n.Translate>Merchant</i18n.Translate>}
-          text={contractTerms.merchant.name}
-          kind="neutral"
-        />
-        <Part
-          title={<i18n.Translate>Purchase</i18n.Translate>}
-          text={contractTerms.summary}
-          kind="neutral"
-        />
-        {contractTerms.order_id && (
-          <Part
-            title={<i18n.Translate>Receipt</i18n.Translate>}
-            text={`#${contractTerms.order_id}`}
-            kind="neutral"
-          />
-        )}
-      </section>
     </WalletAction>
   );
 }
diff --git a/packages/taler-wallet-webextension/src/cta/Pay.tsx 
b/packages/taler-wallet-webextension/src/cta/Pay.tsx
index 0d5d5737..832b4879 100644
--- a/packages/taler-wallet-webextension/src/cta/Pay.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Pay.tsx
@@ -65,7 +65,7 @@ import {
   useAsyncAsHook,
   useAsyncAsHook2,
 } from "../hooks/useAsyncAsHook.js";
-import { ButtonHandler } from "../wallet/CreateManualWithdraw.js";
+import { ButtonHandler } from "../mui/handlers.js";
 import * as wxApi from "../wxApi.js";
 
 interface Props {
@@ -74,32 +74,6 @@ interface Props {
   goBack: () => void;
 }
 
-async function doPayment(
-  payStatus: PreparePayResult,
-  api: typeof wxApi,
-): Promise<ConfirmPayResultDone> {
-  if (payStatus.status !== "payment-possible") {
-    throw TalerError.fromUncheckedDetail({
-      code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
-      hint: `payment is not possible: ${payStatus.status}`,
-    });
-  }
-  const proposalId = payStatus.proposalId;
-  const res = await api.confirmPay(proposalId, undefined);
-  if (res.type !== ConfirmPayResultType.Done) {
-    throw TalerError.fromUncheckedDetail({
-      code: TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
-      hint: `could not confirm payment`,
-      payResult: res,
-    });
-  }
-  const fu = res.contractTerms.fulfillment_url;
-  if (fu) {
-    document.location.href = fu;
-  }
-  return res;
-}
-
 type State = Loading | Ready | Confirmed;
 interface Loading {
   status: "loading";
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
index 2191205c..f2bc14f7 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.stories.tsx
@@ -66,7 +66,9 @@ export const TermsOfServiceNotYetLoaded = 
createExample(TestedComponent, {
     exchange: {
       list: exchangeList,
       value: "exchange.demo.taler.net",
-      onChange: () => null,
+      onChange: async () => {
+        null;
+      },
     },
     showExchangeSelection: false,
     mustAcceptFirst: false,
@@ -99,7 +101,9 @@ export const WithSomeFee = createExample(TestedComponent, {
     exchange: {
       list: exchangeList,
       value: "exchange.demo.taler.net",
-      onChange: () => null,
+      onChange: async () => {
+        null;
+      },
     },
     showExchangeSelection: false,
     mustAcceptFirst: false,
@@ -133,7 +137,9 @@ export const WithoutFee = createExample(TestedComponent, {
     exchange: {
       list: exchangeList,
       value: "exchange.demo.taler.net",
-      onChange: () => null,
+      onChange: async () => {
+        null;
+      },
     },
     showExchangeSelection: false,
     mustAcceptFirst: false,
@@ -167,7 +173,9 @@ export const EditExchangeUntouched = 
createExample(TestedComponent, {
     exchange: {
       list: exchangeList,
       value: "exchange.demo.taler.net",
-      onChange: () => null,
+      onChange: async () => {
+        null;
+      },
     },
     showExchangeSelection: true,
     mustAcceptFirst: false,
@@ -202,7 +210,9 @@ export const EditExchangeModified = 
createExample(TestedComponent, {
       list: exchangeList,
       isDirty: true,
       value: "exchange.test.taler.net",
-      onChange: () => null,
+      onChange: async () => {
+        null;
+      },
     },
     showExchangeSelection: true,
     mustAcceptFirst: false,
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx 
b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
index 2293d650..21f98ec9 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw.tsx
@@ -42,10 +42,7 @@ import {
 import { useTranslationContext } from "../context/translation.js";
 import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
 import { buildTermsOfServiceState } from "../utils/index.js";
-import {
-  ButtonHandler,
-  SelectFieldHandler,
-} from "../wallet/CreateManualWithdraw.js";
+import { ButtonHandler, SelectFieldHandler } from "../mui/handlers.js";
 import * as wxApi from "../wxApi.js";
 import {
   Props as TermsOfServiceSectionProps,
@@ -258,7 +255,7 @@ export function useComponentState(
   }
 
   const exchangeHandler: SelectFieldHandler = {
-    onChange: setNextExchange,
+    onChange: async (e) => setNextExchange(e),
     value: nextExchange ?? thisExchange,
     list: exchanges,
     isDirty: nextExchange !== undefined,
diff --git a/packages/taler-wallet-webextension/src/mui/handlers.ts 
b/packages/taler-wallet-webextension/src/mui/handlers.ts
new file mode 100644
index 00000000..f75070c9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/handlers.ts
@@ -0,0 +1,21 @@
+import { TalerError } from "@gnu-taler/taler-wallet-core";
+
+export interface TextFieldHandler {
+  onInput: (value: string) => Promise<void>;
+  value: string;
+  error?: string;
+}
+
+export interface ButtonHandler {
+  onClick?: () => Promise<void>;
+  error?: TalerError;
+}
+
+export interface SelectFieldHandler {
+  onChange: (value: string) => Promise<void>;
+  error?: string;
+  value: string;
+  isDirty?: boolean;
+  list: Record<string, string>;
+}
+
diff --git a/packages/taler-wallet-webextension/src/stories.tsx 
b/packages/taler-wallet-webextension/src/stories.tsx
index 3f74cf11..1ad91a13 100644
--- a/packages/taler-wallet-webextension/src/stories.tsx
+++ b/packages/taler-wallet-webextension/src/stories.tsx
@@ -69,10 +69,13 @@ const SideBar = styled.div`
   & > {
     ol {
       padding: 4px;
-      div {
+      div:first-child {
         background-color: lightcoral;
         cursor: pointer;
       }
+      div[data-hide="true"] {
+        display: none;
+      }
       dd {
         margin-left: 1em;
         padding: 4px;
@@ -192,12 +195,12 @@ function ExampleList({
   selected: ExampleItem | undefined;
   onSelectStory: (i: ExampleItem, id: string) => void;
 }): VNode {
-  const [open, setOpen] = useState(true);
+  const [isOpen, setOpen] = useState(selected && selected.group === name);
   return (
     <ol>
-      <div onClick={() => setOpen(!open)}>{name}</div>
-      {open &&
-        list.map((k) => (
+      <div onClick={() => setOpen(!isOpen)}>{name}</div>
+      <div data-hide={!isOpen}>
+        {list.map((k) => (
           <li key={k.name}>
             <dl>
               <dt>{k.name}</dt>
@@ -215,6 +218,7 @@ function ExampleList({
                       href={`#${eId}`}
                       onClick={(e) => {
                         e.preventDefault();
+                        location.hash = `#${eId}`;
                         onSelectStory(r, eId);
                       }}
                     >
@@ -226,6 +230,7 @@ function ExampleList({
             </dl>
           </li>
         ))}
+      </div>
     </ol>
   );
 }
@@ -335,6 +340,7 @@ function Application(): VNode {
 
   return (
     <Page>
+      <LiveReload />
       <SideBar>
         {allExamples.map((e) => (
           <ExampleList
@@ -382,3 +388,56 @@ function main(): void {
     }
   }
 }
+
+let liveReloadMounted = false;
+function LiveReload({ port = 8002 }: { port?: number }): VNode {
+  const [isReloading, setIsReloading] = useState(false);
+  useEffect(() => {
+    if (!liveReloadMounted) {
+      setupLiveReload(port, () => {
+        setIsReloading(true);
+        window.location.reload();
+      });
+      liveReloadMounted = true;
+    }
+  });
+
+  if (isReloading) {
+    return (
+      <div
+        style={{
+          position: "absolute",
+          width: "100%",
+          height: "100%",
+          backgroundColor: "rgba(0,0,0,0.5)",
+          color: "white",
+          display: "flex",
+          justifyContent: "center",
+        }}
+      >
+        <h1 style={{ margin: "auto" }}>reloading...</h1>
+      </div>
+    );
+  }
+  return <Fragment />;
+}
+
+function setupLiveReload(port: number, onReload: () => void): void {
+  const protocol = location.protocol === "https:" ? "wss:" : "ws:";
+  const host = location.hostname;
+  const socketPath = `${protocol}//${host}:${port}/socket`;
+
+  const ws = new WebSocket(socketPath);
+  ws.onmessage = (message) => {
+    const event = JSON.parse(message.data);
+    if (event.type === "LOG") {
+      console.log(event.message);
+    }
+    if (event.type === "RELOAD") {
+      onReload();
+    }
+  };
+  ws.onerror = (error) => {
+    console.error(error);
+  };
+}
diff --git 
a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts 
b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts
index f2bb4a7d..a4b333f0 100644
--- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.test.ts
@@ -21,8 +21,9 @@
  */
 
 import { expect } from "chai";
+import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js";
 import { mountHook } from "../test-utils.js";
-import { SelectFieldHandler, TextFieldHandler, useComponentState } from 
"./CreateManualWithdraw.js";
+import { useComponentState } from "./CreateManualWithdraw.js";
 
 
 const exchangeListWithARSandUSD = {
diff --git 
a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx 
b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
index 0440c50a..11bade6f 100644
--- a/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx
@@ -37,6 +37,7 @@ import {
   SubTitle,
 } from "../components/styled/index.js";
 import { useTranslationContext } from "../context/translation.js";
+import { SelectFieldHandler, TextFieldHandler } from "../mui/handlers.js";
 import { Pages } from "../NavigationBar.js";
 
 export interface Props {
@@ -55,25 +56,6 @@ export interface State {
   exchange: SelectFieldHandler;
 }
 
-export interface TextFieldHandler {
-  onInput: (value: string) => void;
-  value: string;
-  error?: string;
-}
-
-export interface ButtonHandler {
-  onClick?: () => Promise<void>;
-  error?: TalerError;
-}
-
-export interface SelectFieldHandler {
-  onChange: (value: string) => void;
-  error?: string;
-  value: string;
-  isDirty?: boolean;
-  list: Record<string, string>;
-}
-
 export function useComponentState(
   exchangeUrlWithCurrency: Record<string, string>,
   initialAmount: string | undefined,
@@ -109,12 +91,12 @@ export function useComponentState(
   const [amount, setAmount] = useState(initialAmount || "");
   const parsedAmount = Amounts.parse(`${currency}:${amount}`);
 
-  function changeExchange(exchange: string): void {
+  async function changeExchange(exchange: string): Promise<void> {
     setExchange(exchange);
     setCurrency(exchangeUrlWithCurrency[exchange]);
   }
 
-  function changeCurrency(currency: string): void {
+  async function changeCurrency(currency: string): Promise<void> {
     setCurrency(currency);
     const found = Object.entries(exchangeUrlWithCurrency).find(
       (e) => e[1] === currency,
@@ -140,7 +122,7 @@ export function useComponentState(
     },
     amount: {
       value: amount,
-      onInput: (e: string) => setAmount(e),
+      onInput: async (e: string) => setAmount(e),
     },
     parsedAmount,
   };
diff --git 
a/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx 
b/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx
index edc2f971..5f796641 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.stories.tsx
@@ -20,10 +20,13 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { Balance, parsePaytoUri } from "@gnu-taler/taler-util";
+import { Amounts, Balance, parsePaytoUri } from "@gnu-taler/taler-util";
 import type { DepositGroupFees } from 
"@gnu-taler/taler-wallet-core/src/operations/deposits.js";
 import { createExample } from "../test-utils.js";
-import { View as TestedComponent } from "./DepositPage.js";
+import {
+  createLabelsForBankAccount,
+  View as TestedComponent,
+} from "./DepositPage.js";
 
 export default {
   title: "wallet/deposit",
@@ -41,23 +44,44 @@ async function alwaysReturnFeeToOne(): 
Promise<DepositGroupFees> {
 }
 
 export const WithEmptyAccountList = createExample(TestedComponent, {
-  accounts: [],
-  balances: [
-    {
-      available: "USD:10",
-    } as Balance,
-  ],
-  currency: "USD",
-  onCalculateFee: alwaysReturnFeeToOne,
+  state: {
+    status: "no-accounts",
+    cancelHandler: {},
+  },
+  // accounts: [],
+  // balances: [
+  //   {
+  //     available: "USD:10",
+  //   } as Balance,
+  // ],
+  // currency: "USD",
+  // onCalculateFee: alwaysReturnFeeToOne,
 });
 
+const ac = parsePaytoUri("payto://iban/ES8877998399652238")!;
+const accountMap = createLabelsForBankAccount([ac]);
+
 export const WithSomeBankAccounts = createExample(TestedComponent, {
-  accounts: [parsePaytoUri("payto://iban/ES8877998399652238")!],
-  balances: [
-    {
-      available: "USD:10",
-    } as Balance,
-  ],
-  currency: "USD",
-  onCalculateFee: alwaysReturnFeeToOne,
+  state: {
+    status: "ready",
+    account: {
+      list: accountMap,
+      value: accountMap[0],
+      onChange: async () => {
+        null;
+      },
+    },
+    currency: "USD",
+    amount: {
+      onInput: async () => {
+        null;
+      },
+      value: "10:USD",
+    },
+    cancelHandler: {},
+    depositHandler: {},
+    totalFee: Amounts.getZero("USD"),
+    totalToDeposit: Amounts.parseOrThrow("USD:10"),
+    // onCalculateFee: alwaysReturnFeeToOne,
+  },
 });
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts 
b/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts
index ac4e0ea9..c863b27d 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.test.ts
@@ -19,46 +19,390 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { Amounts, Balance } from "@gnu-taler/taler-util";
+import { Amounts, Balance, BalancesResponse, parsePaytoUri } from 
"@gnu-taler/taler-util";
 import { DepositGroupFees } from 
"@gnu-taler/taler-wallet-core/src/operations/deposits";
 import { expect } from "chai";
 import { mountHook } from "../test-utils.js";
 import { useComponentState } from "./DepositPage.js";
+import * as wxApi from "../wxApi.js";
 
 
 const currency = "EUR"
-const feeCalculator = async (): Promise<DepositGroupFees> => ({
+const withoutFee = async (): Promise<DepositGroupFees> => ({
+  coin: Amounts.parseOrThrow(`${currency}:0`),
+  wire: Amounts.parseOrThrow(`${currency}:0`),
+  refresh: Amounts.parseOrThrow(`${currency}:0`)
+})
+
+const withSomeFee = async (): Promise<DepositGroupFees> => ({
   coin: Amounts.parseOrThrow(`${currency}:1`),
   wire: Amounts.parseOrThrow(`${currency}:1`),
   refresh: Amounts.parseOrThrow(`${currency}:1`)
 })
 
+const freeJustForIBAN = async (account: string): Promise<DepositGroupFees> => 
/IBAN/i.test(account) ? withoutFee() : withSomeFee()
+
 const someBalance = [{
   available: 'EUR:10'
 } as Balance]
 
+const nullFunction: any = () => null;
+type VoidFunction = () => void;
+
 describe("DepositPage states", () => {
-  it("should have status 'no-balance' when balance is empty", () => {
-    const { getLastResultOrThrow } = mountHook(() =>
-      useComponentState(currency, [], [], feeCalculator),
+  it("should have status 'no-balance' when balance is empty", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(currency, nullFunction, nullFunction, {
+        getBalance: async () => ({
+          balances: [{ available: `${currency}:0`, }]
+        } as Partial<BalancesResponse>),
+        listKnownBankAccounts: async () => ({ accounts: [] })
+      } as Partial<typeof wxApi> as any)
     );
 
+    {
+      const { status } = getLastResultOrThrow()
+      expect(status).equal("loading")
+    }
+
+    await waitNextUpdate()
+
     {
       const { status } = getLastResultOrThrow()
       expect(status).equal("no-balance")
     }
 
+    await assertNoPendingUpdate()
+
+  });
+
+  it("should have status 'no-accounts' when balance is not empty and accounts 
is empty", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(currency, nullFunction, nullFunction, {
+        getBalance: async () => ({
+          balances: [{ available: `${currency}:1`, }]
+        } as Partial<BalancesResponse>),
+        listKnownBankAccounts: async () => ({ accounts: [] })
+      } as Partial<typeof wxApi> as any)
+    );
+
+    {
+      const { status } = getLastResultOrThrow()
+      expect(status).equal("loading")
+    }
+
+    await waitNextUpdate()
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "no-accounts") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+    }
+
+    await assertNoPendingUpdate()
+
+  });
+
+  const ibanPayto = parsePaytoUri("payto://iban/ES8877998399652238")!;
+  const talerBankPayto = 
parsePaytoUri("payto://x-taler-bank/ES8877998399652238")!;
+
+  it("should have status 'ready' but unable to deposit ", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(currency, nullFunction, nullFunction, {
+        getBalance: async () => ({
+          balances: [{ available: `${currency}:1`, }]
+        } as Partial<BalancesResponse>),
+        listKnownBankAccounts: async () => ({ accounts: [ibanPayto] })
+      } as Partial<typeof wxApi> as any)
+    );
+
+    {
+      const { status } = getLastResultOrThrow()
+      expect(status).equal("loading")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("0")
+      expect(r.depositHandler.onClick).undefined;
+    }
+
+    await assertNoPendingUpdate()
+  });
+
+  it("should not be able to deposit more than the balance ", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(currency, nullFunction, nullFunction, {
+        getBalance: async () => ({
+          balances: [{ available: `${currency}:1`, }]
+        } as Partial<BalancesResponse>),
+        listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
+        getFeeForDeposit: withoutFee
+      } as Partial<typeof wxApi> as any)
+    );
+
+    {
+      const { status } = getLastResultOrThrow()
+      expect(status).equal("loading")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("0")
+      expect(r.depositHandler.onClick).undefined;
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`))
+
+      r.amount.onInput("10")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("10")
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`))
+      expect(r.depositHandler.onClick).undefined;
+    }
+
+    await assertNoPendingUpdate()
+  });
+
+  it("should calculate the fee upon entering amount ", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(currency, nullFunction, nullFunction, {
+        getBalance: async () => ({
+          balances: [{ available: `${currency}:1`, }]
+        } as Partial<BalancesResponse>),
+        listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
+        getFeeForDeposit: withSomeFee
+      } as Partial<typeof wxApi> as any)
+    );
+
+    {
+      const { status } = getLastResultOrThrow()
+      expect(status).equal("loading")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("0")
+      expect(r.depositHandler.onClick).undefined;
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`))
+
+      r.amount.onInput("10")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("10")
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`))
+      expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`))
+      expect(r.depositHandler.onClick).undefined;
+    }
+
+    await assertNoPendingUpdate()
+  });
+
+  it("should calculate the fee upon selecting account ", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(currency, nullFunction, nullFunction, {
+        getBalance: async () => ({
+          balances: [{ available: `${currency}:1`, }]
+        } as Partial<BalancesResponse>),
+        listKnownBankAccounts: async () => ({ accounts: [ibanPayto, 
talerBankPayto] }),
+        getFeeForDeposit: freeJustForIBAN
+      } as Partial<typeof wxApi> as any)
+    );
+
+    {
+      const { status } = getLastResultOrThrow()
+      expect(status).equal("loading")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("0")
+      expect(r.depositHandler.onClick).undefined;
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`))
+
+      r.account.onChange("1")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("1")
+      expect(r.amount.value).eq("0")
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`))
+      expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:0`))
+      expect(r.depositHandler.onClick).undefined;
+
+      r.amount.onInput("10")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("1")
+      expect(r.amount.value).eq("10")
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`))
+      expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`))
+      expect(r.depositHandler.onClick).undefined;
+
+      r.account.onChange("0")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("10")
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`))
+      expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`))
+      expect(r.depositHandler.onClick).undefined;
+
+    }
+
+    await assertNoPendingUpdate()
   });
 
-  it("should have status 'no-accounts' when balance is not empty and accounts 
is empty", () => {
-    const { getLastResultOrThrow } = mountHook(() =>
-      useComponentState(currency, [], someBalance, feeCalculator),
+
+  it("should be able to deposit if has the enough balance ", async () => {
+    const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = 
mountHook(() =>
+      useComponentState(currency, nullFunction, nullFunction, {
+        getBalance: async () => ({
+          balances: [{ available: `${currency}:15`, }]
+        } as Partial<BalancesResponse>),
+        listKnownBankAccounts: async () => ({ accounts: [ibanPayto] }),
+        getFeeForDeposit: withSomeFee
+      } as Partial<typeof wxApi> as any)
     );
 
     {
       const { status } = getLastResultOrThrow()
-      expect(status).equal("no-accounts")
+      expect(status).equal("loading")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("0")
+      expect(r.depositHandler.onClick).undefined;
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:0`))
+
+      r.amount.onInput("10")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("10")
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`))
+      expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:7`))
+      expect(r.depositHandler.onClick).not.undefined;
+
+      r.amount.onInput("13")
+    }
+
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("13")
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`))
+      expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:10`))
+      expect(r.depositHandler.onClick).not.undefined;
+
+      r.amount.onInput("15")
     }
 
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("15")
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`))
+      expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:12`))
+      expect(r.depositHandler.onClick).not.undefined;
+      r.amount.onInput("17")
+    }
+    await waitNextUpdate()
+
+    {
+      const r = getLastResultOrThrow()
+      if (r.status !== "ready") expect.fail();
+      expect(r.cancelHandler.onClick).not.undefined;
+      expect(r.currency).eq(currency);
+      expect(r.account.value).eq("0")
+      expect(r.amount.value).eq("17")
+      expect(r.totalFee).deep.eq(Amounts.parseOrThrow(`${currency}:3`))
+      expect(r.totalToDeposit).deep.eq(Amounts.parseOrThrow(`${currency}:14`))
+      expect(r.depositHandler.onClick).undefined;
+    }
+    await assertNoPendingUpdate()
   });
+
 });
\ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx 
b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx
index 335dfd3c..98328ae4 100644
--- a/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/DepositPage.tsx
@@ -15,16 +15,10 @@
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */
 
-import {
-  AmountJson,
-  Amounts,
-  AmountString,
-  Balance,
-  PaytoUri,
-} from "@gnu-taler/taler-util";
+import { AmountJson, Amounts, PaytoUri } from "@gnu-taler/taler-util";
 import { DepositGroupFees } from 
"@gnu-taler/taler-wallet-core/src/operations/deposits";
 import { Fragment, h, VNode } from "preact";
-import { useEffect, useState } from "preact/hooks";
+import { useState } from "preact/hooks";
 import { Loading } from "../components/Loading.js";
 import { LoadingError } from "../components/LoadingError.js";
 import { SelectList } from "../components/SelectList.js";
@@ -38,12 +32,13 @@ import {
   WarningBox,
 } from "../components/styled/index.js";
 import { useTranslationContext } from "../context/translation.js";
-import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
-import * as wxApi from "../wxApi.js";
+import { HookError, useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
 import {
+  ButtonHandler,
   SelectFieldHandler,
   TextFieldHandler,
-} from "./CreateManualWithdraw.js";
+} from "../mui/handlers.js";
+import * as wxApi from "../wxApi.js";
 
 interface Props {
   currency: string;
@@ -51,119 +46,90 @@ interface Props {
   onSuccess: (currency: string) => void;
 }
 export function DepositPage({ currency, onCancel, onSuccess }: Props): VNode {
-  const state = useAsyncAsHook(async () => {
-    const { balances } = await wxApi.getBalance();
-    const { accounts } = await wxApi.listKnownBankAccounts(currency);
-    return { accounts, balances };
-  });
-
-  const { i18n } = useTranslationContext();
-
-  async function doSend(p: PaytoUri, a: AmountJson): Promise<void> {
-    const account = `payto://${p.targetType}/${p.targetPath}`;
-    const amount = Amounts.stringify(a);
-    await wxApi.createDepositGroup(account, amount);
-    onSuccess(currency);
-  }
-
-  async function getFeeForAmount(
-    p: PaytoUri,
-    a: AmountJson,
-  ): Promise<DepositGroupFees> {
-    const account = `payto://${p.targetType}/${p.targetPath}`;
-    const amount = Amounts.stringify(a);
-    return await wxApi.getFeeForDeposit(account, amount);
-  }
-
-  if (state === undefined) return <Loading />;
+  const state = useComponentState(currency, onCancel, onSuccess, wxApi);
 
-  if (state.hasError) {
-    return (
-      <LoadingError
-        title={<i18n.Translate>Could not load deposit balance</i18n.Translate>}
-        error={state}
-      />
-    );
-  }
-
-  return (
-    <View
-      onCancel={() => onCancel(currency)}
-      currency={currency}
-      accounts={state.response.accounts}
-      balances={state.response.balances}
-      onSend={doSend}
-      onCalculateFee={getFeeForAmount}
-    />
-  );
+  return <View state={state} />;
 }
 
 interface ViewProps {
-  accounts: Array<PaytoUri>;
-  currency: string;
-  balances: Balance[];
-  onCancel: () => void;
-  onSend: (account: PaytoUri, amount: AmountJson) => Promise<void>;
-  onCalculateFee: (
-    account: PaytoUri,
-    amount: AmountJson,
-  ) => Promise<DepositGroupFees>;
+  state: State;
 }
 
-type State = NoBalanceState | NoAccountsState | DepositState;
+type State = Loading | NoBalanceState | NoAccountsState | DepositState;
+
+interface Loading {
+  status: "loading";
+  hook: HookError | undefined;
+}
 
 interface NoBalanceState {
   status: "no-balance";
 }
 interface NoAccountsState {
   status: "no-accounts";
+  cancelHandler: ButtonHandler;
 }
 interface DepositState {
-  status: "deposit";
+  status: "ready";
+  currency: string;
   amount: TextFieldHandler;
   account: SelectFieldHandler;
   totalFee: AmountJson;
   totalToDeposit: AmountJson;
-  unableToDeposit: boolean;
-  selectedAccount: PaytoUri;
-  parsedAmount: AmountJson | undefined;
+  // currentAccount: PaytoUri;
+  // parsedAmount: AmountJson | undefined;
+  cancelHandler: ButtonHandler;
+  depositHandler: ButtonHandler;
+}
+
+async function getFeeForAmount(
+  p: PaytoUri,
+  a: AmountJson,
+  api: typeof wxApi,
+): Promise<DepositGroupFees> {
+  const account = `payto://${p.targetType}/${p.targetPath}`;
+  const amount = Amounts.stringify(a);
+  return await api.getFeeForDeposit(account, amount);
 }
 
 export function useComponentState(
   currency: string,
-  accounts: PaytoUri[],
-  balances: Balance[],
-  onCalculateFee: (
-    account: PaytoUri,
-    amount: AmountJson,
-  ) => Promise<DepositGroupFees>,
+  onCancel: (currency: string) => void,
+  onSuccess: (currency: string) => void,
+  api: typeof wxApi,
 ): State {
-  const accountMap = createLabelsForBankAccount(accounts);
+  const hook = useAsyncAsHook(async () => {
+    const { balances } = await api.getBalance();
+    const { accounts } = await api.listKnownBankAccounts(currency);
+    const defaultSelectedAccount =
+      accounts.length > 0 ? accounts[0] : undefined;
+    return { accounts, balances, defaultSelectedAccount };
+  });
+
   const [accountIdx, setAccountIdx] = useState(0);
-  const [amount, setAmount] = useState<number | undefined>(undefined);
+  const [amount, setAmount] = useState<number>(0);
+
+  const [selectedAccount, setSelectedAccount] = useState<
+    PaytoUri | undefined
+  >();
+
+  const parsedAmount = Amounts.parse(`${currency}:${amount}`);
+
   const [fee, setFee] = useState<DepositGroupFees | undefined>(undefined);
-  function updateAmount(num: number | undefined): void {
-    setAmount(num);
-    setFee(undefined);
-  }
 
-  const selectedAmountSTR: AmountString = `${currency}:${amount}`;
-  const totalFee =
-    fee !== undefined
-      ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount
-      : Amounts.getZero(currency);
+  // const hookResponse = !hook || hook.hasError ? undefined : hook.response;
 
-  const selectedAccount = accounts.length ? accounts[accountIdx] : undefined;
+  // useEffect(() => {}, [hookResponse]);
 
-  const parsedAmount =
-    amount === undefined ? undefined : Amounts.parse(selectedAmountSTR);
+  if (!hook || hook.hasError) {
+    return {
+      status: "loading",
+      hook,
+    };
+  }
 
-  useEffect(() => {
-    if (selectedAccount === undefined || parsedAmount === undefined) return;
-    onCalculateFee(selectedAccount, parsedAmount).then((result) => {
-      setFee(result);
-    });
-  }, [amount, selectedAccount, parsedAmount, onCalculateFee]);
+  const { accounts, balances, defaultSelectedAccount } = hook.response;
+  const currentAccount = selectedAccount ?? defaultSelectedAccount;
 
   const bs = balances.filter((b) => b.available.startsWith(currency));
   const balance =
@@ -171,6 +137,63 @@ export function useComponentState(
       ? Amounts.parseOrThrow(bs[0].available)
       : Amounts.getZero(currency);
 
+  if (Amounts.isZero(balance)) {
+    return {
+      status: "no-balance",
+    };
+  }
+
+  if (!currentAccount) {
+    return {
+      status: "no-accounts",
+      cancelHandler: {
+        onClick: async () => {
+          onCancel(currency);
+        },
+      },
+    };
+  }
+  const accountMap = createLabelsForBankAccount(accounts);
+
+  async function updateAccount(accountStr: string): Promise<void> {
+    const idx = parseInt(accountStr, 10);
+    const newSelected = accounts.length > idx ? accounts[idx] : undefined;
+    if (accountIdx === idx || !newSelected) return;
+
+    if (!parsedAmount) {
+      setAccountIdx(idx);
+      setSelectedAccount(newSelected);
+    } else {
+      const result = await getFeeForAmount(newSelected, parsedAmount, api);
+      setAccountIdx(idx);
+      setSelectedAccount(newSelected);
+      setFee(result);
+    }
+  }
+
+  async function updateAmount(numStr: string): Promise<void> {
+    const num = parseFloat(numStr);
+    const newAmount = Number.isNaN(num) ? 0 : num;
+    if (amount === newAmount || !currentAccount) return;
+    const parsed = Amounts.parse(`${currency}:${newAmount}`);
+    if (!parsed) {
+      setAmount(newAmount);
+    } else {
+      const result = await getFeeForAmount(currentAccount, parsed, api);
+      setAmount(newAmount);
+      setFee(result);
+    }
+  }
+
+  const totalFee =
+    fee !== undefined
+      ? Amounts.sum([fee.wire, fee.coin, fee.refresh]).amount
+      : Amounts.getZero(currency);
+
+  const totalToDeposit = parsedAmount
+    ? Amounts.sub(parsedAmount, totalFee).amount
+    : Amounts.getZero(currency);
+
   const isDirty = amount !== 0;
   const amountError = !isDirty
     ? undefined
@@ -180,65 +203,63 @@ export function useComponentState(
     ? `Too much, your current balance is ${Amounts.stringifyValue(balance)}`
     : undefined;
 
-  const totalToDeposit = parsedAmount
-    ? Amounts.sub(parsedAmount, totalFee).amount
-    : Amounts.getZero(currency);
-
   const unableToDeposit =
+    !parsedAmount ||
     Amounts.isZero(totalToDeposit) ||
     fee === undefined ||
     amountError !== undefined;
 
-  if (Amounts.isZero(balance)) {
-    return {
-      status: "no-balance",
-    };
-  }
+  async function doSend(): Promise<void> {
+    if (!currentAccount || !parsedAmount) return;
 
-  if (!accounts || !accounts.length || !selectedAccount) {
-    return {
-      status: "no-accounts",
-    };
+    const account = 
`payto://${currentAccount.targetType}/${currentAccount.targetPath}`;
+    const amount = Amounts.stringify(parsedAmount);
+    await api.createDepositGroup(account, amount);
+    onSuccess(currency);
   }
 
   return {
-    status: "deposit",
+    status: "ready",
+    currency,
     amount: {
       value: String(amount),
-      onInput: (e) => {
-        const num = parseFloat(e);
-        if (!Number.isNaN(num)) {
-          updateAmount(num);
-        } else {
-          updateAmount(undefined);
-          setFee(undefined);
-        }
-      },
+      onInput: updateAmount,
       error: amountError,
     },
     account: {
       list: accountMap,
       value: String(accountIdx),
-      onChange: (s) => setAccountIdx(parseInt(s, 10)),
+      onChange: updateAccount,
+    },
+    cancelHandler: {
+      onClick: async () => {
+        onCancel(currency);
+      },
+    },
+    depositHandler: {
+      onClick: unableToDeposit ? undefined : doSend,
     },
     totalFee,
     totalToDeposit,
-    unableToDeposit,
-    selectedAccount,
-    parsedAmount,
+    // currentAccount,
+    // parsedAmount,
   };
 }
 
-export function View({
-  onCancel,
-  currency,
-  accounts,
-  balances,
-  onSend,
-  onCalculateFee,
-}: ViewProps): VNode {
+export function View({ state }: ViewProps): VNode {
   const { i18n } = useTranslationContext();
-  const state = useComponentState(currency, accounts, balances, 
onCalculateFee);
+
+  if (state === undefined) return <Loading />;
+
+  if (state.status === "loading") {
+    if (!state.hook) return <Loading />;
+    return (
+      <LoadingError
+        title={<i18n.Translate>Could not load deposit balance</i18n.Translate>}
+        error={state.hook}
+      />
+    );
+  }
 
   if (state.status === "no-balance") {
     return (
@@ -258,7 +279,7 @@ export function View({
           </p>
         </WarningBox>
         <footer>
-          <Button onClick={onCancel}>
+          <Button onClick={state.cancelHandler.onClick}>
             <i18n.Translate>Cancel</i18n.Translate>
           </Button>
         </footer>
@@ -269,7 +290,7 @@ export function View({
   return (
     <Fragment>
       <SubTitle>
-        <i18n.Translate>Send {currency} to your account</i18n.Translate>
+        <i18n.Translate>Send {state.currency} to your account</i18n.Translate>
       </SubTitle>
       <section>
         <Input>
@@ -286,7 +307,7 @@ export function View({
             <i18n.Translate>Amount</i18n.Translate>
           </label>
           <div>
-            <span>{currency}</span>
+            <span>{state.currency}</span>
             <input
               type="number"
               value={state.amount.value}
@@ -302,7 +323,7 @@ export function View({
                 <i18n.Translate>Deposit fee</i18n.Translate>
               </label>
               <div>
-                <span>{currency}</span>
+                <span>{state.currency}</span>
                 <input
                   type="number"
                   disabled
@@ -316,7 +337,7 @@ export function View({
                 <i18n.Translate>Total deposit</i18n.Translate>
               </label>
               <div>
-                <span>{currency}</span>
+                <span>{state.currency}</span>
                 <input
                   type="number"
                   disabled
@@ -328,19 +349,18 @@ export function View({
         }
       </section>
       <footer>
-        <Button onClick={onCancel}>
+        <Button onClick={state.cancelHandler.onClick}>
           <i18n.Translate>Cancel</i18n.Translate>
         </Button>
-        {state.unableToDeposit ? (
+        {!state.depositHandler.onClick ? (
           <ButtonPrimary disabled>
             <i18n.Translate>Deposit</i18n.Translate>
           </ButtonPrimary>
         ) : (
-          <ButtonPrimary
-            onClick={() => onSend(state.selectedAccount, state.parsedAmount!)}
-          >
+          <ButtonPrimary onClick={state.depositHandler.onClick}>
             <i18n.Translate>
-              Deposit {Amounts.stringifyValue(state.totalToDeposit)} {currency}
+              Deposit {Amounts.stringifyValue(state.totalToDeposit)}{" "}
+              {state.currency}
             </i18n.Translate>
           </ButtonPrimary>
         )}
@@ -349,7 +369,9 @@ export function View({
   );
 }
 
-function createLabelsForBankAccount(knownBankAccounts: Array<PaytoUri>): {
+export function createLabelsForBankAccount(
+  knownBankAccounts: Array<PaytoUri>,
+): {
   [label: number]: string;
 } {
   if (!knownBankAccounts) return {};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1066300e..e83549f5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -349,6 +349,7 @@ importers:
       babel-loader: ^8.2.3
       babel-plugin-transform-react-jsx: ^6.24.1
       chai: ^4.3.6
+      chokidar: ^3.5.3
       date-fns: ^2.28.0
       history: 4.10.1
       mocha: ^9.2.0
@@ -367,6 +368,7 @@ importers:
       rollup-plugin-terser: ^7.0.2
       tslib: ^2.3.1
       typescript: ^4.5.5
+      ws: 7.4.5
     dependencies:
       '@gnu-taler/taler-util': link:../taler-util
       '@gnu-taler/taler-wallet-core': link:../taler-wallet-core
@@ -376,6 +378,7 @@ importers:
       preact-router: 3.2.1_preact@10.6.5
       qrcode-generator: 1.4.4
       tslib: 2.3.1
+      ws: 7.4.5
     devDependencies:
       '@babel/core': 7.13.16
       '@babel/plugin-transform-react-jsx-source': 7.14.5_@babel+core@7.13.16
@@ -404,6 +407,7 @@ importers:
       babel-loader: 8.2.3_@babel+core@7.13.16
       babel-plugin-transform-react-jsx: 6.24.1
       chai: 4.3.6
+      chokidar: 3.5.3
       mocha: 9.2.0
       nyc: 15.1.0
       polished: 4.1.4
@@ -19088,6 +19092,19 @@ packages:
       async-limiter: 1.0.1
     dev: true
 
+  /ws/7.4.5:
+    resolution: {integrity: 
sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==}
+    engines: {node: '>=8.3.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: ^5.0.2
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+    dev: false
+
   /ws/7.5.7:
     resolution: {integrity: 
sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==}
     engines: {node: '>=8.3.0'}

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