gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 02/02: deposit test case


From: gnunet
Subject: [taler-wallet-core] 02/02: deposit test case
Date: Fri, 22 Apr 2022 21:10:58 +0200

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

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

commit c5f484d18a89bd6cda0c7a89eea5ee9d7fe4ba09
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Fri Apr 22 16:10:21 2022 -0300

    deposit test case
---
 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 +
 16 files changed, 790 insertions(+), 605 deletions(-)

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]