gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: local error impl: errors show


From: gnunet
Subject: [taler-wallet-core] branch master updated: local error impl: errors shown fixed position that are wiped when moved from the view
Date: Mon, 30 Oct 2023 19:27:31 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 768838285 local error impl: errors shown fixed position that are wiped 
when moved from the view
768838285 is described below

commit 768838285c25cbb1b171f645e8efb37a3c14273a
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Oct 30 15:27:25 2023 -0300

    local error impl: errors shown fixed position that are wiped when moved 
from the view
---
 packages/demobank-ui/src/components/Attention.tsx  |   2 +-
 .../src/components/ShowLocalNotification.tsx       |  43 +++++++
 packages/demobank-ui/src/demobank-ui-settings.js   |   2 +-
 packages/demobank-ui/src/pages/LoginForm.tsx       |  30 ++---
 .../demobank-ui/src/pages/OperationState/index.ts  |  16 ++-
 .../demobank-ui/src/pages/OperationState/state.ts  | 142 +++++----------------
 .../demobank-ui/src/pages/OperationState/views.tsx | 138 +++++++++++++++++++-
 .../src/pages/PaytoWireTransferForm.tsx            |  27 ++--
 packages/demobank-ui/src/pages/QrCodeSection.tsx   |  19 ++-
 .../demobank-ui/src/pages/RegistrationPage.tsx     |   8 +-
 .../demobank-ui/src/pages/ShowAccountDetails.tsx   |  11 +-
 .../src/pages/UpdateAccountPassword.tsx            |   9 +-
 .../demobank-ui/src/pages/WalletWithdrawForm.tsx   |  17 +--
 .../src/pages/WithdrawalConfirmationQuestion.tsx   |  23 ++--
 packages/demobank-ui/src/pages/admin/Account.tsx   |   6 +-
 .../demobank-ui/src/pages/admin/AccountForm.tsx    |  12 +-
 .../src/pages/admin/CreateNewAccount.tsx           |  22 ++--
 .../demobank-ui/src/pages/admin/RemoveAccount.tsx  |  18 ++-
 .../src/pages/business/CreateCashout.tsx           |   9 +-
 .../src/pages/business/ShowCashoutDetails.tsx      |  11 +-
 packages/taler-harness/src/index.ts                |  11 +-
 packages/taler-util/src/amounts.ts                 |  21 ++-
 packages/taler-util/src/http-client/types.ts       |  87 ++++++-------
 packages/taler-util/src/index.node.ts              |   1 +
 packages/taler-util/src/payto.ts                   |  26 +++-
 packages/taler-util/src/taleruri.ts                |  22 ++++
 packages/web-util/src/hooks/useNotifications.ts    |  98 +++++++++++++-
 27 files changed, 556 insertions(+), 275 deletions(-)

diff --git a/packages/demobank-ui/src/components/Attention.tsx 
b/packages/demobank-ui/src/components/Attention.tsx
index 3313e5796..57d0a4199 100644
--- a/packages/demobank-ui/src/components/Attention.tsx
+++ b/packages/demobank-ui/src/components/Attention.tsx
@@ -9,7 +9,7 @@ interface Props {
   children?: ComponentChildren ,
 }
 export function Attention({ type = "info", title, children, onClose }: Props): 
VNode {
-  return <div class={`group attention-${type} mt-2`}>
+  return <div class={`group attention-${type} mt-2 shadow-lg`}>
     <div class="rounded-md group-[.attention-info]:bg-blue-50 
group-[.attention-warning]:bg-yellow-50 group-[.attention-danger]:bg-red-50 
group-[.attention-success]:bg-green-50 p-4 shadow">
       <div class="flex">
         <div >
diff --git a/packages/demobank-ui/src/components/ShowLocalNotification.tsx 
b/packages/demobank-ui/src/components/ShowLocalNotification.tsx
new file mode 100644
index 000000000..bb62a48f0
--- /dev/null
+++ b/packages/demobank-ui/src/components/ShowLocalNotification.tsx
@@ -0,0 +1,43 @@
+import { Notification } from "@gnu-taler/web-util/browser";
+import { h, Fragment, VNode } from "preact";
+import { Attention } from "./Attention.js";
+import { useSettings } from "../hooks/settings.js";
+
+export function ShowLocalNotification({ notification }: { notification?: 
Notification }): VNode {
+  if (!notification) return <Fragment />
+  switch (notification.message.type) {
+    case "error":
+      return <div class="relative">
+        <div class="fixed top-0 left-0 right-0 z-20 w-full p-4">
+          <Attention type="danger" title={notification.message.title} 
onClose={() => {
+            notification.remove()
+          }}>
+            {notification.message.description &&
+              <div class="mt-2 text-sm text-red-700">
+                {notification.message.description}
+              </div>
+            }
+            <MaybeShowDebugInfo info={notification.message.debug} />
+          </Attention>
+        </div>
+      </div>
+    case "info":
+      return <div class="relative">
+        <div class="fixed top-0 left-0 right-0 z-20 w-full p-4">
+          <Attention type="success" title={notification.message.title} 
onClose={() => {
+            notification.remove();
+          }} /></div></div>
+  }
+}
+
+
+function MaybeShowDebugInfo({ info }: { info: any }): VNode {
+  const [settings] = useSettings()
+  if (settings.showDebugInfo) {
+    return <pre class="whitespace-break-spaces ">
+      {info}
+    </pre>
+  }
+  return <Fragment />
+}
+
diff --git a/packages/demobank-ui/src/demobank-ui-settings.js 
b/packages/demobank-ui/src/demobank-ui-settings.js
index 99c6f3873..827f207f8 100644
--- a/packages/demobank-ui/src/demobank-ui-settings.js
+++ b/packages/demobank-ui/src/demobank-ui-settings.js
@@ -4,7 +4,7 @@
  * Global settings for the demobank UI.
  */
 globalThis.talerDemobankSettings = {
-  backendBaseURL: "http://bank.taler.test/";,
+  backendBaseURL: "http://bank.taler.test:1180/";,
   allowRegistrations: true,
   showDemoNav: true,
   simplePasswordForRandomAccounts: true,
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx 
b/packages/demobank-ui/src/pages/LoginForm.tsx
index b18f29d86..f21e98343 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -15,7 +15,7 @@
  */
 
 import { TranslatedString } from "@gnu-taler/taler-util";
-import { notify, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Notification, useLocalNotification, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useEffect, useRef, useState } from "preact/hooks";
 import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
@@ -25,6 +25,8 @@ import { bankUiSettings } from "../settings.js";
 import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
 import { assertUnreachable } from "./WithdrawalOperationPage.js";
 import { doAutoFocus } from "./PaytoWireTransferForm.js";
+import { Attention } from "../components/Attention.js";
+import { ShowLocalNotification } from "../components/ShowLocalNotification.js";
 
 
 /**
@@ -37,25 +39,19 @@ export function LoginForm({ reason, onRegister }: { 
reason?: "not-found" | "forb
   const [password, setPassword] = useState<string | undefined>();
   const { i18n } = useTranslationContext();
   const { api } = useBankCoreApiContext();
-
+  const [notification, notify, handleError] = useLocalNotification()
 
   /**
    * Register form may be shown in the initialization step.
-   * If this is an error when usgin the app the registration
-   * callback is not set
+   * If no register handler then this is invoke
+   * to show a session expired or unauthorized
    */
-  const isSessionExpired = !onRegister
+  const isLogginAgain = !onRegister
 
-  // useEffect(() => {
-  //   if (backend.state.status === "loggedIn") {
-  //     backend.expired()
-  //   }
-  // },[])
   const ref = useRef<HTMLInputElement>(null);
   useEffect(function focusInput() {
-    //FIXME: show invalidate session and allow relogin
-    if (isSessionExpired) {
-      localStorage.removeItem("backend-state");
+    if (isLogginAgain && backend.state.status !== "expired") {
+      backend.expired()
       window.location.reload()
     }
     ref.current?.focus();
@@ -78,7 +74,7 @@ export function LoginForm({ reason, onRegister }: { reason?: 
"not-found" | "forb
   async function doLogin() {
     if (!username || !password) return;
     setBusy({})
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const resp = await 
api.getAuthenticationAPI(username).createAccessToken(password, {
         // scope: "readwrite" as "write", //FIX: different than merchant
         scope: "readwrite",
@@ -114,7 +110,7 @@ export function LoginForm({ reason, onRegister }: { 
reason?: "not-found" | "forb
 
   return (
     <div class="flex min-h-full flex-col justify-center">
-
+      <ShowLocalNotification notification={notification} />
       <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
         <form class="space-y-6" noValidate
           onSubmit={(e) => {
@@ -135,7 +131,7 @@ export function LoginForm({ reason, onRegister }: { 
reason?: "not-found" | "forb
                 id="username"
                 class="block w-full disabled:bg-gray-200 rounded-md border-0 
py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
                 value={username ?? ""}
-                disabled={isSessionExpired}
+                disabled={isLogginAgain}
                 enterkeyhint="next"
                 placeholder="identification"
                 autocomplete="username"
@@ -177,7 +173,7 @@ export function LoginForm({ reason, onRegister }: { 
reason?: "not-found" | "forb
             </div>
           </div>
 
-          {isSessionExpired ? <div class="flex justify-between">
+          {isLogginAgain ? <div class="flex justify-between">
             <button type="submit"
               class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold 
leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline 
focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-gray-600"
               onClick={(e) => {
diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts 
b/packages/demobank-ui/src/pages/OperationState/index.ts
index bc3555c48..b17b0d787 100644
--- a/packages/demobank-ui/src/pages/OperationState/index.ts
+++ b/packages/demobank-ui/src/pages/OperationState/index.ts
@@ -19,7 +19,7 @@ import { utils } from "@gnu-taler/web-util/browser";
 import { ErrorLoading } from "../../components/ErrorLoading.js";
 import { Loading } from "../../components/Loading.js";
 import { useComponentState } from "./state.js";
-import { AbortedView, ConfirmedView, InvalidPaytoView, InvalidReserveView, 
InvalidWithdrawalView, NeedConfirmationView, ReadyView } from "./views.js";
+import { AbortedView, ConfirmedView, FailedView, InvalidPaytoView, 
InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView, ReadyView } 
from "./views.js";
 
 export interface Props {
   currency: string;
@@ -29,6 +29,7 @@ export interface Props {
 export type State = State.Loading |
   State.LoadingError |
   State.Ready |
+  State.Failed |
   State.Aborted |
   State.Confirmed |
   State.InvalidPayto |
@@ -42,6 +43,11 @@ export namespace State {
     error: undefined;
   }
 
+  export interface Failed {
+    status: "failed";
+    error: TalerCoreBankErrorsByMethod<"createWithdrawal">;
+  }
+
   export interface LoadingError {
     status: "loading-error";
     error: TalerError;
@@ -54,8 +60,7 @@ export namespace State {
     status: "ready";
     error: undefined;
     uri: WithdrawUriResult,
-    onClose: () => void;
-    onAbort: () => void;
+    onClose: () => Promise<TalerCoreBankErrorsByMethod<"abortWithdrawalById"> 
| undefined>;
   }
 
   export interface InvalidPayto {
@@ -78,8 +83,8 @@ export namespace State {
   }
   export interface NeedConfirmation {
     status: "need-confirmation",
-    onAbort: () => void;
-    onConfirm: () => void;
+    onAbort: () => Promise<TalerCoreBankErrorsByMethod<"abortWithdrawalById"> 
| undefined>;
+    onConfirm: () => 
Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined>;
     error: undefined;
     busy: boolean,
   }
@@ -106,6 +111,7 @@ export interface Transaction {
 
 const viewMapping: utils.StateViewMap<State> = {
   loading: Loading,
+  "failed": FailedView,
   "invalid-payto": InvalidPaytoView,
   "invalid-withdrawal": InvalidWithdrawalView,
   "invalid-reserve": InvalidReserveView,
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts 
b/packages/demobank-ui/src/pages/OperationState/state.ts
index a4890d726..2d33ff78b 100644
--- a/packages/demobank-ui/src/pages/OperationState/state.ts
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -14,65 +14,40 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts, HttpStatusCode, TalerError, TranslatedString, parsePaytoUri, 
parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
-import { RequestError, notify, notifyError, notifyInfo, useTranslationContext, 
utils } from "@gnu-taler/web-util/browser";
+import { Amounts, FailCasesByMethod, TalerCoreBankErrorsByMethod, TalerError, 
TalerErrorDetail, TranslatedString, parsePaytoUri, parseWithdrawUri, 
stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { notify, notifyError, notifyInfo, useTranslationContext, utils } from 
"@gnu-taler/web-util/browser";
 import { useEffect, useState } from "preact/hooks";
+import { mutate } from "swr";
 import { useBankCoreApiContext } from "../../context/config.js";
 import { useWithdrawalDetails } from "../../hooks/access.js";
 import { useBackendState } from "../../hooks/backend.js";
 import { useSettings } from "../../hooks/settings.js";
-import { buildRequestErrorMessage, withRuntimeErrorHandling } from 
"../../utils.js";
-import { Props, State } from "./index.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
-import { mutate } from "swr";
+import { Props, State } from "./index.js";
 
 export function useComponentState({ currency, onClose }: Props): 
utils.RecursiveState<State> {
-  const { i18n } = useTranslationContext();
   const [settings, updateSettings] = useSettings()
   const { state: credentials } = useBackendState()
   const creds = credentials.status !== "loggedIn" ? undefined : credentials
   const { api } = useBankCoreApiContext()
-  // const { createWithdrawal } = useAccessAPI();
-  // const { abortWithdrawal, confirmWithdrawal } = useAccessAnonAPI();
-  const [busy, setBusy] = useState<Record<string, undefined>>()
 
+  const [busy, setBusy] = useState<Record<string, undefined>>()
+  const [failure, setFailure] = 
useState<TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined>()
   const amount = settings.maxWithdrawalAmount
 
   async function doSilentStart() {
     //FIXME: if amount is not enough use balance
     const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
     if (!creds) return;
-    await withRuntimeErrorHandling(i18n, async () => {
-      const resp = await api.createWithdrawal(creds, {
-        amount: Amounts.stringify(parsedAmount),
-      });
-      if (resp.type === "fail") {
-        switch (resp.case) {
-          case "insufficient-funds": return notify({
-            type: "error",
-            title: i18n.str`The operation was rejected due to insufficient 
funds.`,
-            description: resp.detail.hint as TranslatedString,
-            debug: resp.detail,
-          });
-          case "unauthorized": return notify({
-            type: "error",
-            title: i18n.str`Unauthorized to make the opeartion, maybe the 
session has expired or the password changed.`,
-            description: resp.detail.hint as TranslatedString,
-            debug: resp.detail,
-          });
-          default: assertUnreachable(resp)
-        }
-      }
+    const resp = await api.createWithdrawal(creds, {
+      amount: Amounts.stringify(parsedAmount),
+    });
+    if (resp.type === "fail") {
+      setFailure(resp)
+      return;
+    }
+    updateSettings("currentWithdrawalOperationId", resp.body.withdrawal_id)
 
-      const uri = parseWithdrawUri(resp.body.taler_withdraw_uri);
-      if (!uri) {
-        return notifyError(
-          i18n.str`Server responded with an invalid withdraw URI`,
-          i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`);
-      } else {
-        updateSettings("currentWithdrawalOperationId", 
uri.withdrawalOperationId)
-      }
-    })
   }
 
   const withdrawalOperationId = settings.currentWithdrawalOperationId
@@ -82,6 +57,13 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
     }
   }, [settings.fastWithdrawal, amount])
 
+  if (failure) {
+    return {
+      status: "failed",
+      error: failure
+    }
+  }
+
   if (!withdrawalOperationId) {
     return {
       status: "loading",
@@ -92,77 +74,24 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
   const wid = withdrawalOperationId
 
   async function doAbort() {
-    await withRuntimeErrorHandling(i18n, async () => {
-      const resp = await api.abortWithdrawalById(wid);
-      if (resp.type === "ok") {
-        updateSettings("currentWithdrawalOperationId", undefined)
-        onClose();
-      } else {
-        switch (resp.case) {
-          case "previously-confirmed": return notify({
-            type: "error",
-            title: i18n.str`The reserve operation has been confirmed 
previously and can't be aborted`,
-            description: resp.detail.hint as TranslatedString,
-            debug: resp.detail,
-          })
-          case "invalid-id": return notify({
-            type: "error",
-            title: i18n.str`The operation id is invalid.`,
-            description: resp.detail.hint as TranslatedString,
-            debug: resp.detail,
-          });
-          case "not-found": return notify({
-            type: "error",
-            title: i18n.str`The operation was not found.`,
-            description: resp.detail.hint as TranslatedString,
-            debug: resp.detail,
-          });
-          default: assertUnreachable(resp)
-        }
-      }
-    })
+    const resp = await api.abortWithdrawalById(wid);
+    if (resp.type === "ok") {
+      updateSettings("currentWithdrawalOperationId", undefined)
+      onClose();
+    } else {
+      return resp;
+    }
   }
 
-  async function doConfirm() {
+  async function doConfirm(): 
Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined> {
     setBusy({})
-    await withRuntimeErrorHandling(i18n, async () => {
-      const resp = await api.confirmWithdrawalById(wid);
-      if (resp.type === "ok") {
-        mutate(() => true)//clean withdrawal state
-        if (!settings.showWithdrawalSuccess) {
-          notifyInfo(i18n.str`Wire transfer completed!`)
-        }
-      } else {
-        switch (resp.case) {
-          case "previously-aborted": return notify({
-            type: "error",
-            title: i18n.str`The withdrawal has been aborted previously and 
can't be confirmed`,
-            description: resp.detail.hint as TranslatedString,
-            debug: resp.detail,
-          })
-          case "no-exchange-or-reserve-selected": return notify({
-            type: "error",
-            title: i18n.str`The withdraw operation cannot be confirmed because 
no exchange and reserve public key selection happened before`,
-            description: resp.detail.hint as TranslatedString,
-            debug: resp.detail,
-          })
-          case "invalid-id": return notify({
-            type: "error",
-            title: i18n.str`The operation id is invalid.`,
-            description: resp.detail.hint as TranslatedString,
-            debug: resp.detail,
-          });
-          case "not-found": return notify({
-            type: "error",
-            title: i18n.str`The operation was not found.`,
-            description: resp.detail.hint as TranslatedString,
-            debug: resp.detail,
-          });
-          default: assertUnreachable(resp)
-        }
-      }
-    })
+    const resp = await api.confirmWithdrawalById(wid);
     setBusy(undefined)
+    if (resp.type === "ok") {
+      mutate(() => true)//clean withdrawal state
+    } else {
+      return resp;
+    }
   }
 
   const uri = stringifyWithdrawUri({
@@ -261,7 +190,6 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
         error: undefined,
         uri: parsedUri,
         onClose: doAbort,
-        onAbort: doAbort,
       }
     }
 
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx 
b/packages/demobank-ui/src/pages/OperationState/views.tsx
index 2cb7385db..b7d7e5520 100644
--- a/packages/demobank-ui/src/pages/OperationState/views.tsx
+++ b/packages/demobank-ui/src/pages/OperationState/views.tsx
@@ -14,8 +14,8 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { stringifyWithdrawUri } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { TranslatedString, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { notifyInfo, useLocalNotification, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useEffect, useMemo, useState } from "preact/hooks";
 import { QR } from "../../components/QR.js";
@@ -23,6 +23,10 @@ import { ShowInputErrorLabel } from 
"../../components/ShowInputErrorLabel.js";
 import { useSettings } from "../../hooks/settings.js";
 import { undefinedIfEmpty } from "../../utils.js";
 import { State } from "./index.js";
+import { ShowLocalNotification } from 
"../../components/ShowLocalNotification.js";
+import { ErrorLoading } from "../../components/ErrorLoading.js";
+import { Attention } from "../../components/Attention.js";
+import { assertUnreachable } from "../WithdrawalOperationPage.js";
 
 export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) {
   return (
@@ -40,8 +44,10 @@ export function InvalidReserveView({ reserve, onClose }: 
State.InvalidReserve) {
   );
 }
 
-export function NeedConfirmationView({ error, onAbort, onConfirm, busy }: 
State.NeedConfirmation) {
+export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: 
doConfirm, busy }: State.NeedConfirmation) {
   const { i18n } = useTranslationContext()
+  const [settings] = useSettings()
+  const [notification, notify, errorHandler] = useLocalNotification()
 
   const captchaNumbers = useMemo(() => {
     return {
@@ -61,8 +67,76 @@ export function NeedConfirmationView({ error, onAbort, 
onConfirm, busy }: State.
           : undefined,
   }) ?? (busy ? {} as Record<string, undefined> : undefined);
 
+  async function onCancel() {
+    errorHandler(async () => {
+      const resp = await doAbort()
+      if (!resp) return;
+      switch (resp.case) {
+        case "previously-confirmed": return notify({
+          type: "error",
+          title: i18n.str`The reserve operation has been confirmed previously 
and can't be aborted`,
+          description: resp.detail.hint as TranslatedString,
+          debug: resp.detail,
+        })
+        case "invalid-id": return notify({
+          type: "error",
+          title: i18n.str`The operation id is invalid.`,
+          description: resp.detail.hint as TranslatedString,
+          debug: resp.detail,
+        });
+        case "not-found": return notify({
+          type: "error",
+          title: i18n.str`The operation was not found.`,
+          description: resp.detail.hint as TranslatedString,
+          debug: resp.detail,
+        });
+        default: assertUnreachable(resp)
+      }
+    })
+  }
+
+  async function onConfirm() {
+    errorHandler(async () => {
+      const hasError = await doConfirm()
+      if (!hasError) {
+        if (!settings.showWithdrawalSuccess) {
+          notifyInfo(i18n.str`Wire transfer completed!`)
+        }
+        return
+      }
+      switch (hasError.case) {
+      case "previously-aborted": return notify({
+        type: "error",
+        title: i18n.str`The withdrawal has been aborted previously and can't 
be confirmed`,
+        description: hasError.detail.hint as TranslatedString,
+        debug: hasError.detail,
+      })
+      case "no-exchange-or-reserve-selected": return notify({
+        type: "error",
+        title: i18n.str`The withdraw operation cannot be confirmed because no 
exchange and reserve public key selection happened before`,
+        description: hasError.detail.hint as TranslatedString,
+        debug: hasError.detail,
+      })
+      case "invalid-id": return notify({
+        type: "error",
+        title: i18n.str`The operation id is invalid.`,
+        description: hasError.detail.hint as TranslatedString,
+        debug: hasError.detail,
+      });
+      case "not-found": return notify({
+        type: "error",
+        title: i18n.str`The operation was not found.`,
+        description: hasError.detail.hint as TranslatedString,
+        debug: hasError.detail,
+      });
+        default: assertUnreachable(hasError)
+      }
+    })
+  }
+
   return (
     <div class="bg-white shadow sm:rounded-lg">
+      <ShowLocalNotification notification={notification} />
       <div class="px-4 py-5 sm:p-6">
         <h3 class="text-base font-semibold text-gray-900">
           <i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
@@ -161,7 +235,10 @@ export function NeedConfirmationView({ error, onAbort, 
onConfirm, busy }: State.
             </div>
             <div class="flex items-center justify-between gap-x-6 border-t 
border-gray-900/10 px-4 py-4 sm:px-8">
               <button type="button" class="text-sm font-semibold leading-6 
text-gray-900"
-                onClick={onAbort}
+                onClick={(e) => {
+                  e.preventDefault()
+                  onCancel()
+                }}
               >
                 <i18n.Translate>Cancel</i18n.Translate></button>
               <button type="submit"
@@ -246,6 +323,25 @@ export function NeedConfirmationView({ error, onAbort, 
onConfirm, busy }: State.
 
   );
 }
+export function FailedView({ error }: State.Failed) {
+  const { i18n } = useTranslationContext();
+  switch (error.case) {
+    case "unauthorized": return <Attention type="danger"
+      title={i18n.str`Unauthorized to make the operation, maybe the session 
has expired or the password changed.`}>
+      <div class="mt-2 text-sm text-red-700">
+        {error.detail.hint}
+      </div>
+    </Attention>
+    case "insufficient-funds": return <Attention type="danger"
+      title={i18n.str`The operation was rejected due to insufficient funds.`}>
+      <div class="mt-2 text-sm text-red-700">
+        {error.detail.hint}
+      </div>
+    </Attention>
+    default: assertUnreachable(error)
+  }
+}
+
 export function AbortedView({ error, onClose }: State.Aborted) {
   return (
     <div>aborted</div>
@@ -308,8 +404,9 @@ export function ConfirmedView({ error, onClose }: 
State.Confirmed) {
   );
 }
 
-export function ReadyView({ uri, onClose }: State.Ready): VNode<{}> {
+export function ReadyView({ uri, onClose: doClose }: State.Ready): VNode<{}> {
   const { i18n } = useTranslationContext();
+  const [notification, notify, errorHandler] = useLocalNotification()
 
   useEffect(() => {
     //Taler Wallet WebExtension is listening to headers response and tab 
updates.
@@ -320,7 +417,38 @@ export function ReadyView({ uri, onClose }: State.Ready): 
VNode<{}> {
     document.title = `${document.title} ${uri.withdrawalOperationId}`;
   }, []);
   const talerWithdrawUri = stringifyWithdrawUri(uri);
+
+  async function onClose() {
+    errorHandler(async () => {
+      const hasError = await doClose()
+      if (!hasError) return;
+      switch (hasError.case) {
+        case "previously-confirmed": return notify({
+          type: "error",
+          title: i18n.str`The reserve operation has been confirmed previously 
and can't be aborted`,
+          description: hasError.detail.hint as TranslatedString,
+          debug: hasError.detail,
+        })
+        case "invalid-id": return notify({
+          type: "error",
+          title: i18n.str`The operation id is invalid.`,
+          description: hasError.detail.hint as TranslatedString,
+          debug: hasError.detail,
+        });
+        case "not-found": return notify({
+          type: "error",
+          title: i18n.str`The operation was not found.`,
+          description: hasError.detail.hint as TranslatedString,
+          debug: hasError.detail,
+        });
+        default: assertUnreachable(hasError)
+      }
+    })
+  }
+
   return <Fragment>
+      <ShowLocalNotification notification={notification} />
+
     <div class="flex justify-end mt-4">
       <button type="button"
         class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 
text-sm font-semibold text-white shadow-sm hover:bg-red-500 
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-red-500"
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 63cb3e865..6649d224e 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -18,33 +18,30 @@ import {
   AmountJson,
   AmountString,
   Amounts,
-  HttpStatusCode,
   Logger,
-  TalerError,
+  PaytoString,
   TranslatedString,
   buildPayto,
   parsePaytoUri,
   stringifyPaytoUri
 } from "@gnu-taler/taler-util";
 import {
-  RequestError,
-  notify,
-  notifyError,
-  useTranslationContext,
+  useLocalNotification,
+  useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { Fragment, Ref, VNode, h } from "preact";
-import { useEffect, useRef, useState } from "preact/hooks";
+import { useState } from "preact/hooks";
+import { mutate } from "swr";
 import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
+import { useBankCoreApiContext } from "../context/config.js";
+import { useBackendState } from "../hooks/backend.js";
 import {
-  buildRequestErrorMessage,
   undefinedIfEmpty,
   validateIBAN,
-  withRuntimeErrorHandling,
+  withRuntimeErrorHandling
 } from "../utils.js";
-import { useBankCoreApiContext } from "../context/config.js";
-import { useBackendState } from "../hooks/backend.js";
 import { assertUnreachable } from "./WithdrawalOperationPage.js";
-import { mutate } from "swr";
+import { ShowLocalNotification } from "../components/ShowLocalNotification.js";
 
 const logger = new Logger("PaytoWireTransferForm");
 
@@ -82,6 +79,7 @@ export function PaytoWireTransferForm({
   const trimmedAmountStr = amount?.trim();
   const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
   const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
+  const [notification, notify, handleError] = useLocalNotification()
 
   const errorsWire = undefinedIfEmpty({
     iban: !iban
@@ -122,7 +120,7 @@ export function PaytoWireTransferForm({
   });
 
   async function doSend() {
-    let payto_uri: string | undefined;
+    let payto_uri: PaytoString | undefined;
     let sendingAmount: AmountString | undefined;
     if (credentials.status !== "loggedIn") return;
     if (rawPaytoInput) {
@@ -141,7 +139,7 @@ export function PaytoWireTransferForm({
     }
     const puri = payto_uri;
 
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const res = await api.createTransaction(credentials, {
         payto_uri: puri,
         amount: sendingAmount,
@@ -367,6 +365,7 @@ export function PaytoWireTransferForm({
           <i18n.Translate>Send</i18n.Translate>
         </button>
       </div>
+      <ShowLocalNotification notification={notification} />
     </form>
   </div >
   )
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx 
b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index 9ae1cf268..ca2a89f48 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -15,24 +15,21 @@
  */
 
 import {
-  HttpStatusCode,
   stringifyWithdrawUri,
-  TalerError,
   TranslatedString,
-  WithdrawUriResult,
+  WithdrawUriResult
 } from "@gnu-taler/taler-util";
 import {
-  notify,
-  notifyError,
-  RequestError,
-  useTranslationContext,
+  useLocalNotification,
+  useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { Fragment, h, VNode } from "preact";
 import { useEffect } from "preact/hooks";
 import { QR } from "../components/QR.js";
-import { buildRequestErrorMessage, withRuntimeErrorHandling } from 
"../utils.js";
 import { useBankCoreApiContext } from "../context/config.js";
+import { withRuntimeErrorHandling } from "../utils.js";
 import { assertUnreachable } from "./WithdrawalOperationPage.js";
+import { ShowLocalNotification } from "../components/ShowLocalNotification.js";
 
 export function QrCodeSection({
   withdrawUri,
@@ -51,18 +48,19 @@ export function QrCodeSection({
     document.title = `${document.title} ${withdrawUri.withdrawalOperationId}`;
   }, []);
   const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
+  const [notification, notify, handleError] = useLocalNotification()
 
   const { api } = useBankCoreApiContext()
 
   async function doAbort() {
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const resp = await 
api.abortWithdrawalById(withdrawUri.withdrawalOperationId);
       if (resp.type === "ok") {
         onAborted();
       } else {
         switch (resp.case) {
           case "previously-confirmed": return notify({
-            type: "info",
+            type: "error",
             title: i18n.str`The reserve operation has been confirmed 
previously and can't be aborted`
           })
           case "invalid-id": return notify({
@@ -87,6 +85,7 @@ export function QrCodeSection({
 
   return (
     <Fragment>
+      <ShowLocalNotification notification={notification} />
       <div class="bg-white shadow-xl sm:rounded-lg">
         <div class="px-4 py-5 sm:p-6">
           <h3 class="text-base font-semibold leading-6 text-gray-900">
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx 
b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index 3520405c5..fdf2c0e9d 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -15,7 +15,7 @@
  */
 import { AccessToken, Logger, TranslatedString } from "@gnu-taler/taler-util";
 import {
-  notify,
+  useLocalNotification,
   useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
@@ -26,6 +26,7 @@ import { useBackendState } from "../hooks/backend.js";
 import { bankUiSettings } from "../settings.js";
 import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
 import { getRandomPassword, getRandomUsername } from "./rnd.js";
+import { ShowLocalNotification } from "../components/ShowLocalNotification.js";
 
 const logger = new Logger("RegistrationPage");
 
@@ -60,6 +61,7 @@ function RegistrationForm({ onComplete, onCancel }: { 
onComplete: () => void, on
   const [phone, setPhone] = useState<string | undefined>();
   const [email, setEmail] = useState<string | undefined>();
   const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
+  const [notification, notify, handleError] = useLocalNotification()
 
   const { api } = useBankCoreApiContext()
   // const { register } = useTestingAPI();
@@ -93,7 +95,7 @@ function RegistrationForm({ onComplete, onCancel }: { 
onComplete: () => void, on
   });
 
   async function doRegistrationAndLogin(name: string | undefined, username: 
string, password: string) {
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const creationResponse = await api.createAccount("" as AccessToken, { 
name: name ?? "", username, password });
       if (creationResponse.type === "fail") {
         switch (creationResponse.case) {
@@ -171,7 +173,7 @@ function RegistrationForm({ onComplete, onCancel }: { 
onComplete: () => void, on
 
   return (
     <Fragment>
-      <h1 class="nav"></h1>
+      <ShowLocalNotification notification={notification} />
 
       <div class="flex min-h-full flex-col justify-center">
         <div class="sm:mx-auto sm:w-full sm:max-w-sm">
diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx 
b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
index b109441a6..eb8ea8f20 100644
--- a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
@@ -1,5 +1,5 @@
 import { TalerCorebankApi, TalerError, TranslatedString } from 
"@gnu-taler/taler-util";
-import { notify, notifyInfo, useTranslationContext } from 
"@gnu-taler/web-util/browser";
+import { notifyInfo, useLocalNotification, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
 import { ErrorLoading } from "../components/ErrorLoading.js";
@@ -8,10 +8,11 @@ import { useBankCoreApiContext } from "../context/config.js";
 import { useAccountDetails } from "../hooks/access.js";
 import { useBackendState } from "../hooks/backend.js";
 import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
 import { LoginForm } from "./LoginForm.js";
-import { AccountForm } from "./admin/AccountForm.js";
 import { ProfileNavigation } from "./ProfileNavigation.js";
+import { assertUnreachable } from "./WithdrawalOperationPage.js";
+import { AccountForm } from "./admin/AccountForm.js";
+import { ShowLocalNotification } from "../components/ShowLocalNotification.js";
 
 export function ShowAccountDetails({
   account,
@@ -31,6 +32,7 @@ export function ShowAccountDetails({
 
   const [update, setUpdate] = useState(false);
   const [submitAccount, setSubmitAccount] = 
useState<TalerCorebankApi.AccountData | undefined>();
+  const [notification, notify, handleError] = useLocalNotification()
 
   const result = useAccountDetails(account);
   if (!result) {
@@ -50,7 +52,7 @@ export function ShowAccountDetails({
 
   async function doUpdate() {
     if (!update || !submitAccount || !creds) return;
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const resp = await api.updateAccount(creds, {
         cashout_address: submitAccount.cashout_payto_uri,
         challenge_contact_data: undefinedIfEmpty({
@@ -93,6 +95,7 @@ export function ShowAccountDetails({
 
   return (
     <Fragment>
+          <ShowLocalNotification notification={notification} />
       {accountIsTheCurrentUser ?
         <ProfileNavigation current="details" />
         :
diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx 
b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
index b14c6d90b..d30216f3f 100644
--- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
@@ -1,13 +1,14 @@
-import { notify, notifyInfo, useTranslationContext } from 
"@gnu-taler/web-util/browser";
+import { notifyInfo, useLocalNotification, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
 import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
 import { useBankCoreApiContext } from "../context/config.js";
 import { useBackendState } from "../hooks/backend.js";
 import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
 import { doAutoFocus } from "./PaytoWireTransferForm.js";
 import { ProfileNavigation } from "./ProfileNavigation.js";
+import { assertUnreachable } from "./WithdrawalOperationPage.js";
+import { ShowLocalNotification } from "../components/ShowLocalNotification.js";
 
 export function UpdateAccountPassword({
   account: accountName,
@@ -41,11 +42,12 @@ export function UpdateAccountPassword({
         ? i18n.str`password doesn't match`
         : undefined,
   });
+  const [notification, notify, handleError] = useLocalNotification()
 
 
   async function doChangePassword() {
     if (!!errors || !password || !token) return;
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const resp = await api.updatePassword({ username: accountName, token }, {
         old_password: current,
         new_password: password,
@@ -77,6 +79,7 @@ export function UpdateAccountPassword({
 
   return (
     <Fragment>
+          <ShowLocalNotification notification={notification} />
       {accountIsTheCurrentUser ?
         <ProfileNavigation current="credentials" /> :
         <h1 class="text-base font-semibold leading-6 text-gray-900">
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx 
b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 0637a8af4..abdebf9bf 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -17,17 +17,14 @@
 import {
   AmountJson,
   Amounts,
-  HttpStatusCode,
   Logger,
-  TalerError,
   TranslatedString,
   parseWithdrawUri
 } from "@gnu-taler/taler-util";
 import {
-  RequestError,
-  notify,
   notifyError,
-  useTranslationContext,
+  useLocalNotification,
+  useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { forwardRef } from "preact/compat";
@@ -36,10 +33,11 @@ import { Attention } from "../components/Attention.js";
 import { useBankCoreApiContext } from "../context/config.js";
 import { useBackendState } from "../hooks/backend.js";
 import { useSettings } from "../hooks/settings.js";
-import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling 
} from "../utils.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
+import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
 import { OperationState } from "./OperationState/index.js";
 import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js";
+import { assertUnreachable } from "./WithdrawalOperationPage.js";
+import { ShowLocalNotification } from "../components/ShowLocalNotification.js";
 
 const logger = new Logger("WalletWithdrawForm");
 const RefAmount = forwardRef(InputAmount);
@@ -59,6 +57,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, 
onCancel, focus }: {
 
   const { api } = useBankCoreApiContext()
   const [amountStr, setAmountStr] = useState<string | 
undefined>(`${settings.maxWithdrawalAmount}`);
+  const [notification, notify, handleError] = useLocalNotification()
 
   if (!!settings.currentWithdrawalOperationId) {
     return <Attention type="warning" title={i18n.str`There is an operation 
already`}>
@@ -88,7 +87,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, 
onCancel, focus }: {
 
   async function doStart() {
     if (!parsedAmount || !creds) return;
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const resp = await api.createWithdrawal(creds, {
         amount: Amounts.stringify(parsedAmount),
       });
@@ -136,6 +135,8 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, 
onCancel, focus }: {
       e.preventDefault()
     }}
   >
+              <ShowLocalNotification notification={notification} />
+
     <div class="px-4 py-6 ">
       <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
         <div class="sm:col-span-5">
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx 
b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 5e0fa322f..89538e305 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -16,32 +16,28 @@
 
 import {
   AmountJson,
-  Amounts,
-  HttpStatusCode,
   Logger,
   PaytoUri,
   PaytoUriIBAN,
   PaytoUriTalerBank,
-  TalerError,
   TranslatedString,
   WithdrawUriResult
 } from "@gnu-taler/taler-util";
 import {
-  RequestError,
-  notify,
-  notifyError,
   notifyInfo,
-  useTranslationContext,
+  useLocalNotification,
+  useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useMemo, useState } from "preact/hooks";
+import { mutate } from "swr";
 import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling 
} from "../utils.js";
+import { useBankCoreApiContext } from "../context/config.js";
 import { useSettings } from "../hooks/settings.js";
+import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
 import { RenderAmount } from "./PaytoWireTransferForm.js";
-import { useBankCoreApiContext } from "../context/config.js";
 import { assertUnreachable } from "./WithdrawalOperationPage.js";
-import { mutate } from "swr";
+import { ShowLocalNotification } from "../components/ShowLocalNotification.js";
 
 const logger = new Logger("WithdrawalConfirmationQuestion");
 
@@ -72,6 +68,7 @@ export function WithdrawalConfirmationQuestion({
       b: Math.floor(Math.random() * 10),
     };
   }, []);
+  const [notification, notify, handleError] = useLocalNotification()
 
   const { api } = useBankCoreApiContext()
   const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
@@ -89,7 +86,7 @@ export function WithdrawalConfirmationQuestion({
 
   async function doTransfer() {
     setBusy({})
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const resp = await 
api.confirmWithdrawalById(withdrawUri.withdrawalOperationId);
       if (resp.type === "ok") {
         mutate(() => true)// clean any info that we have
@@ -131,7 +128,7 @@ export function WithdrawalConfirmationQuestion({
 
   async function doCancel() {
     setBusy({})
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const resp = await 
api.abortWithdrawalById(withdrawUri.withdrawalOperationId);
       if (resp.type === "ok") {
         onAborted();
@@ -164,6 +161,8 @@ export function WithdrawalConfirmationQuestion({
 
   return (
     <Fragment>
+          <ShowLocalNotification notification={notification} />
+
       <div class="bg-white shadow sm:rounded-lg">
         <div class="px-4 py-5 sm:p-6">
           <h3 class="text-base font-semibold text-gray-900">
diff --git a/packages/demobank-ui/src/pages/admin/Account.tsx 
b/packages/demobank-ui/src/pages/admin/Account.tsx
index 1818de655..7109b082f 100644
--- a/packages/demobank-ui/src/pages/admin/Account.tsx
+++ b/packages/demobank-ui/src/pages/admin/Account.tsx
@@ -23,9 +23,9 @@ export function WireTransfer({ toAccount, onRegister, 
onCancel, onSuccess }: { o
   }
   if (result.type === "fail") {
     switch (result.case) {
-      case "unauthorized": return <LoginForm reason="forbidden" 
onRegister={onRegister} />
-      case "not-found": return <LoginForm reason="not-found" 
onRegister={onRegister} />
-      case "no-rights": return <LoginForm reason="not-found" 
onRegister={onRegister} />
+      case "unauthorized": return <LoginForm reason="forbidden"  />
+      case "not-found": return <LoginForm reason="not-found" />
+      case "no-rights": return <LoginForm reason="not-found" />
       default: assertUnreachable(result)
     }
   }
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx 
b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 410683dcb..fa3a28057 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -3,7 +3,7 @@ import { ShowInputErrorLabel } from 
"../../components/ShowInputErrorLabel.js";
 import { PartialButDefined, RecursivePartial, WithIntermediate, 
undefinedIfEmpty, validateIBAN } from "../../utils.js";
 import { useEffect, useRef, useState } from "preact/hooks";
 import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { TalerCorebankApi, buildPayto, parsePaytoUri } from 
"@gnu-taler/taler-util";
+import { PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri } from 
"@gnu-taler/taler-util";
 import { doAutoFocus } from "../PaytoWireTransferForm.js";
 import { CopyButton } from "../../components/CopyButton.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
@@ -52,7 +52,7 @@ export function AccountForm({
       : buildPayto("iban", newForm.cashout_payto_uri, undefined);;
 
     const errors = undefinedIfEmpty<RecursivePartial<typeof initial>>({
-      cashout_payto_uri: !newForm.cashout_payto_uri
+      cashout_payto_uri: (!newForm.cashout_payto_uri
         ? i18n.str`required`
         : !parsed
           ? i18n.str`does not follow the pattern`
@@ -60,7 +60,7 @@ export function AccountForm({
             ? i18n.str`only "IBAN" target are supported`
             : !IBAN_REGEX.test(parsed.iban)
               ? i18n.str`IBAN should have just uppercased letters and numbers`
-              : validateIBAN(parsed.iban, i18n),
+              : validateIBAN(parsed.iban, i18n)) as PaytoString,
       contact_data: undefinedIfEmpty({
         email: !newForm.contact_data?.email
           ? i18n.str`required`
@@ -165,7 +165,7 @@ export function AccountForm({
           </div>
 
 
-          {purpose !== "create" && (<RenderPaytoDisabledField 
paytoURI={form.payto_uri} />)}
+          {purpose !== "create" && (<RenderPaytoDisabledField 
paytoURI={form.payto_uri as PaytoString} />)}
 
           <div class="sm:col-span-5">
             <label
@@ -252,7 +252,7 @@ export function AccountForm({
                 disabled={purpose === "show"}
                 value={form.cashout_payto_uri ?? ""}
                 onChange={(e) => {
-                  form.cashout_payto_uri = e.currentTarget.value;
+                  form.cashout_payto_uri = e.currentTarget.value as 
PaytoString;
                   updateForm(structuredClone(form));
                 }}
                 autocomplete="off"
@@ -303,7 +303,7 @@ function initializeFromTemplate(
 }
 
 
-function RenderPaytoDisabledField({ paytoURI }: { paytoURI: string | undefined 
}): VNode {
+function RenderPaytoDisabledField({ paytoURI }: { paytoURI: PaytoString | 
undefined }): VNode {
   const { i18n } = useTranslationContext()
   const payto = parsePaytoUri(paytoURI ?? "");
   if (payto?.isKnown) {
diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx 
b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
index ea40001c0..f2c1d5456 100644
--- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -1,15 +1,16 @@
-import { HttpStatusCode, TalerCorebankApi, TalerError, TranslatedString } from 
"@gnu-taler/taler-util";
-import { RequestError, notify, notifyError, notifyInfo, useTranslationContext 
} from "@gnu-taler/web-util/browser";
+import { TalerCorebankApi, TranslatedString } from "@gnu-taler/taler-util";
+import { notifyInfo, useLocalNotification, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
-import { buildRequestErrorMessage, withRuntimeErrorHandling } from 
"../../utils.js";
-import { getRandomPassword } from "../rnd.js";
-import { AccountForm, AccountFormData } from "./AccountForm.js";
-import { useBackendState } from "../../hooks/backend.js";
-import { useBankCoreApiContext } from "../../context/config.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
 import { mutate } from "swr";
 import { Attention } from "../../components/Attention.js";
+import { useBankCoreApiContext } from "../../context/config.js";
+import { useBackendState } from "../../hooks/backend.js";
+import { withRuntimeErrorHandling } from "../../utils.js";
+import { assertUnreachable } from "../WithdrawalOperationPage.js";
+import { getRandomPassword } from "../rnd.js";
+import { AccountForm, AccountFormData } from "./AccountForm.js";
+import { ShowLocalNotification } from 
"../../components/ShowLocalNotification.js";
 
 export function CreateNewAccount({
   onCancel,
@@ -25,10 +26,11 @@ export function CreateNewAccount({
   const { api } = useBankCoreApiContext();
 
   const [submitAccount, setSubmitAccount] = useState<AccountFormData | 
undefined>();
+  const [notification, notify, handleError] = useLocalNotification()
 
   async function doCreate() {
     if (!submitAccount || !token) return;
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const account: TalerCorebankApi.RegisterAccountRequest = {
         cashout_payto_uri: submitAccount.cashout_payto_uri,
         challenge_contact_data: submitAccount.contact_data,
@@ -85,6 +87,8 @@ export function CreateNewAccount({
 
   return (
     <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 
bg-gray-100 my-4 px-4 pb-4 rounded-lg">
+      <ShowLocalNotification notification={notification} />
+
       <div class="px-4 sm:px-0">
         <h2 class="text-base font-semibold leading-7 text-gray-900">
           <i18n.Translate>New business account</i18n.Translate>
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx 
b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index 89f634080..1a5255595 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -1,18 +1,19 @@
-import { Amounts, HttpStatusCode, TalerError, TranslatedString } from 
"@gnu-taler/taler-util";
-import { HttpResponsePaginated, RequestError, notify, notifyError, notifyInfo, 
useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import { notifyInfo, useLocalNotification, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
 import { Attention } from "../../components/Attention.js";
 import { ErrorLoading } from "../../components/ErrorLoading.js";
 import { Loading } from "../../components/Loading.js";
 import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
+import { useBankCoreApiContext } from "../../context/config.js";
 import { useAccountDetails } from "../../hooks/access.js";
-import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling 
} from "../../utils.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
+import { useBackendState } from "../../hooks/backend.js";
+import { undefinedIfEmpty } from "../../utils.js";
 import { LoginForm } from "../LoginForm.js";
 import { doAutoFocus } from "../PaytoWireTransferForm.js";
-import { useBankCoreApiContext } from "../../context/config.js";
-import { useBackendState } from "../../hooks/backend.js";
+import { assertUnreachable } from "../WithdrawalOperationPage.js";
+import { ShowLocalNotification } from 
"../../components/ShowLocalNotification.js";
 
 export function RemoveAccount({
   account,
@@ -32,6 +33,7 @@ export function RemoveAccount({
   const { state } = useBackendState();
   const token = state.status !== "loggedIn" ? undefined : state.token
   const { api } = useBankCoreApiContext()
+  const [notification, notify, handleError] = useLocalNotification()
 
   if (!result) {
     return <Loading />
@@ -61,7 +63,7 @@ export function RemoveAccount({
 
   async function doRemove() {
     if (!token) return;
-    await withRuntimeErrorHandling(i18n, async () => {
+    await handleError(async () => {
       const resp = await api.deleteAccount({ username: account, token });
       if (resp.type === "ok") {
         notifyInfo(i18n.str`Account removed`);
@@ -111,6 +113,8 @@ export function RemoveAccount({
 
   return (
     <div>
+      <ShowLocalNotification notification={notification} />
+
       <Attention type="warning" title={i18n.str`You are going to remove the 
account`}>
         <i18n.Translate>This step can't be undone.</i18n.Translate>
       </Attention>
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx 
b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index a71915622..8d90e9205 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -19,7 +19,7 @@ import {
   TranslatedString
 } from "@gnu-taler/taler-util";
 import {
-  notify,
+  useLocalNotification,
   useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
@@ -43,6 +43,7 @@ import {
 import { LoginForm } from "../LoginForm.js";
 import { InputAmount } from "../PaytoWireTransferForm.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
+import { ShowLocalNotification } from 
"../../components/ShowLocalNotification.js";
 
 interface Props {
   account: string;
@@ -76,6 +77,7 @@ export function CreateCashout({
   const creds = state.status !== "loggedIn" ? undefined : state
   const { api, config } = useBankCoreApiContext()
   const [form, setForm] = useState<Partial<FormType>>({ isDebit: true });
+  const [notification, notify, handleError] = useLocalNotification()
 
   if (!config.have_cashout) {
     return <Attention type="warning" title={i18n.str`Unable to create a 
cashout`} onClose={onCancel}>
@@ -144,7 +146,7 @@ export function CreateCashout({
 
   useEffect(() => {
     async function doAsync() {
-      await withRuntimeErrorHandling(i18n, async () => {
+      await handleError(async () => {
         const resp = await (form.isDebit ?
           calculateFromDebit(amount, sellFee, safeSellRate) :
           calculateFromCredit(amount, sellFee, safeSellRate));
@@ -176,6 +178,7 @@ export function CreateCashout({
 
   return (
     <div>
+          <ShowLocalNotification notification={notification} />
       <h1>New cashout</h1>
       <form class="pure-form">
         <fieldset>
@@ -360,7 +363,7 @@ export function CreateCashout({
               e.preventDefault();
 
               if (errors || !creds) return;
-              await withRuntimeErrorHandling(i18n, async () => {
+              await handleError(async () => {
                 const resp = await api.createCashout(creds, {
                   amount_credit: Amounts.stringify(calc.credit),
                   amount_debit: Amounts.stringify(calc.debit),
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx 
b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
index b8e566348..7e7ed21cb 100644
--- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -18,13 +18,14 @@ import {
   TranslatedString
 } from "@gnu-taler/taler-util";
 import {
-  notify,
+  useLocalNotification,
   useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { format } from "date-fns";
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
 import { mutate } from "swr";
+import { Attention } from "../../components/Attention.js";
 import { ErrorLoading } from "../../components/ErrorLoading.js";
 import { Loading } from "../../components/Loading.js";
 import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
@@ -38,7 +39,7 @@ import {
   withRuntimeErrorHandling
 } from "../../utils.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
-import { Attention } from "../../components/Attention.js";
+import { ShowLocalNotification } from 
"../../components/ShowLocalNotification.js";
 
 interface Props {
   id: string;
@@ -54,6 +55,7 @@ export function ShowCashoutDetails({
   const { api } = useBankCoreApiContext()
   const result = useCashoutDetails(id);
   const [code, setCode] = useState<string | undefined>(undefined);
+  const [notification, notify, handleError] = useLocalNotification()
 
   if (!result) {
     return <Loading />
@@ -76,6 +78,7 @@ export function ShowCashoutDetails({
   const isPending = String(result.body.status).toUpperCase() === "PENDING";
   return (
     <div>
+      <ShowLocalNotification notification={notification} />
       <h1>Cashout details {id}</h1>
       <form class="pure-form">
         <fieldset>
@@ -161,7 +164,7 @@ export function ShowCashoutDetails({
               onClick={async (e) => {
                 e.preventDefault();
                 if (!creds) return;
-                await withRuntimeErrorHandling(i18n, async () => {
+                await handleError(async () => {
                   const resp = await api.abortCashoutById(creds, id);
                   if (resp.type === "ok") {
                     onCancel();
@@ -203,7 +206,7 @@ export function ShowCashoutDetails({
               onClick={async (e) => {
                 e.preventDefault();
                 if (!creds || !code) return;
-                await withRuntimeErrorHandling(i18n, async () => {
+                await handleError(async () => {
                   const resp = await api.confirmCashoutById(creds, id, {
                     tan: code,
                   });
diff --git a/packages/taler-harness/src/index.ts 
b/packages/taler-harness/src/index.ts
index c83457be4..717aee57d 100644
--- a/packages/taler-harness/src/index.ts
+++ b/packages/taler-harness/src/index.ts
@@ -18,13 +18,11 @@
  * Imports.
  */
 import {
-  AccessToken,
   AmountString,
   Amounts,
   Configuration,
   Duration,
   HttpStatusCode,
-  LibtoolVersion,
   Logger,
   MerchantApiClient,
   MerchantInstanceConfig,
@@ -34,12 +32,11 @@ import {
   TalerError,
   addPaytoQueryParams,
   decodeCrock,
-  encodeCrock,
   generateIban,
-  getRandomBytes,
   j2s,
   rsaBlind,
   setGlobalLogLevelFromString,
+  setPrintHttpRequestAsCurl,
 } from "@gnu-taler/taler-util";
 import { clk } from "@gnu-taler/taler-util/clk";
 import {
@@ -54,6 +51,7 @@ import {
 } from "@gnu-taler/taler-wallet-core";
 import { deepStrictEqual } from "assert";
 import fs from "fs";
+import { BankCoreSmokeTest } from "http-client/bank-core.js";
 import os from "os";
 import path from "path";
 import { runBench1 } from "./bench1.js";
@@ -68,7 +66,6 @@ import {
 } from "./harness/harness.js";
 import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
 import { lintExchangeDeployment } from "./lint.js";
-import { BankCoreSmokeTest } from "http-client/bank-core.js";
 
 const logger = new Logger("taler-harness:index.ts");
 
@@ -665,11 +662,15 @@ deploymentCli
   })
   .requiredArgument("corebankApiBaseUrl", clk.STRING)
   .maybeOption("adminPwd", ["--admin-password"], clk.STRING)
+  .flag("showCurl", ["--show-curl"])
   .action(async (args) => {
     const httpLib = createPlatformHttpLib();
     const api = new 
TalerCoreBankHttpClient(args.testBankAPI.corebankApiBaseUrl, httpLib);
 
     const tester = new BankCoreSmokeTest(api)
+    if (args.testBankAPI.showCurl) {
+      setPrintHttpRequestAsCurl(true)
+    }
     try {
       process.stdout.write("config: ");
       const config = await tester.testConfig()
diff --git a/packages/taler-util/src/amounts.ts 
b/packages/taler-util/src/amounts.ts
index 082a8168e..5c6444b00 100644
--- a/packages/taler-util/src/amounts.ts
+++ b/packages/taler-util/src/amounts.ts
@@ -26,6 +26,9 @@ import {
   codecForString,
   codecForNumber,
   Codec,
+  Context,
+  DecodingError,
+  renderContext,
 } from "./codec.js";
 import { AmountString } from "./taler-types.js";
 
@@ -74,7 +77,23 @@ export const codecForAmountJson = (): Codec<AmountJson> =>
     .property("fraction", codecForNumber())
     .build("AmountJson");
 
-export const codecForAmountString = (): Codec<AmountString> => 
codecForString() as Codec<AmountString>;
+export function codecForAmountString(): Codec<AmountString> {
+  return {
+    decode(x: any, c?: Context): AmountString {
+      if (typeof x !== "string") {
+        throw new DecodingError(
+          `expected string at ${renderContext(c)} but got ${typeof x}`,
+        );
+      }
+      if (Amounts.parse(x) === undefined) {
+        throw new DecodingError(
+          `invalid amount at ${renderContext(c)} got "${x}"`,
+        );
+      }
+      return x as AmountString;
+    },
+  };
+}
 
 /**
  * Result of a possibly overflowing operation.
diff --git a/packages/taler-util/src/http-client/types.ts 
b/packages/taler-util/src/http-client/types.ts
index b9a5032d1..1bb8f99c1 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -1,6 +1,8 @@
 import { codecForAmountString } from "../amounts.js";
 import { Codec, buildCodecForObject, buildCodecForUnion, codecForBoolean, 
codecForConstString, codecForEither, codecForList, codecForMap, codecForNumber, 
codecForString, codecOptional } from "../codec.js";
+import { PaytoString, PaytoUri, codecForPaytoString } from "../payto.js";
 import { AmountString } from "../taler-types.js";
+import { TalerActionString, WithdrawUriResult, codecForTalerActionString } 
from "../taleruri.js";
 import { codecForTimestamp } from "../time.js";
 import { TalerErrorDetail } from "../wallet-types.js";
 
@@ -255,7 +257,7 @@ const codecForPublicAccount = (): 
Codec<TalerCorebankApi.PublicAccount> =>
   buildCodecForObject<TalerCorebankApi.PublicAccount>()
     .property("account_name", codecForString())
     .property("balance", codecForBalance())
-    .property("payto_uri", codecForPaytoURI())
+    .property("payto_uri", codecForPaytoString())
     .build("TalerCorebankApi.PublicAccount")
 
 export const codecForPublicAccountsResponse =
@@ -285,10 +287,10 @@ export const codecForAccountData =
     buildCodecForObject<TalerCorebankApi.AccountData>()
       .property("name", codecForString())
       .property("balance", codecForBalance())
-      .property("payto_uri", codecForPaytoURI())
+      .property("payto_uri", codecForPaytoString())
       .property("debit_threshold", codecForAmountString())
       .property("contact_data", codecOptional(codecForChallengeContactData()))
-      .property("cashout_payto_uri", codecOptional(codecForPaytoURI()))
+      .property("cashout_payto_uri", codecOptional(codecForPaytoString()))
       .build("TalerCorebankApi.AccountData")
 
 
@@ -309,9 +311,9 @@ export const codecForBankAccountTransactionInfo =
   (): Codec<TalerCorebankApi.BankAccountTransactionInfo> =>
     buildCodecForObject<TalerCorebankApi.BankAccountTransactionInfo>()
       .property("amount", codecForAmountString())
-      .property("creditor_payto_uri", codecForPaytoURI())
+      .property("creditor_payto_uri", codecForPaytoString())
       .property("date", codecForTimestamp)
-      .property("debtor_payto_uri", codecForPaytoURI())
+      .property("debtor_payto_uri", codecForPaytoString())
       .property("direction", codecForEither(codecForConstString("debit"), 
codecForConstString("credit")))
       .property("row_id", codecForNumber())
       .property("subject", codecForString())
@@ -320,7 +322,7 @@ export const codecForBankAccountTransactionInfo =
 export const codecForBankAccountCreateWithdrawalResponse =
   (): Codec<TalerCorebankApi.BankAccountCreateWithdrawalResponse> =>
     buildCodecForObject<TalerCorebankApi.BankAccountCreateWithdrawalResponse>()
-      .property("taler_withdraw_uri", codecForTalerWithdrawalURI())
+      .property("taler_withdraw_uri", codecForTalerActionString())
       .property("withdrawal_id", codecForString())
       .build("TalerCorebankApi.BankAccountCreateWithdrawalResponse");
 
@@ -330,7 +332,7 @@ export const codecForBankAccountGetWithdrawalResponse =
       .property("aborted", codecForBoolean())
       .property("amount", codecForAmountString())
       .property("confirmation_done", codecForBoolean())
-      .property("selected_exchange_account", codecOptional(codecForString()))
+      .property("selected_exchange_account", 
codecOptional(codecForPaytoString()))
       .property("selected_reserve_pub", codecOptional(codecForString()))
       .property("selection_done", (codecForBoolean()))
       .build("TalerCorebankApi.BankAccountGetWithdrawalResponse");
@@ -382,7 +384,7 @@ export const codecForCashoutStatusResponse =
       .property("amount_debit", codecForAmountString())
       .property("confirmation_time", codecForTimestamp)
       .property("creation_time", codecForTimestamp)
-      .property("credit_payto_uri", codecForPaytoURI())
+      .property("credit_payto_uri", codecForPaytoString())
       .property("status", codecForEither(codecForConstString("pending"), 
codecForConstString("confirmed")))
       .property("subject", codecForString())
       .build("TalerCorebankApi.CashoutStatusResponse");
@@ -423,7 +425,7 @@ export const codecForBankWithdrawalOperationStatus =
       .property("amount", codecForAmountString())
       .property("confirm_transfer_url", codecOptional(codecForURL()))
       .property("selection_done", codecForBoolean())
-      .property("sender_wire", codecForPaytoURI())
+      .property("sender_wire", codecForPaytoString())
       .property("suggested_exchange", codecOptional(codecForString()))
       .property("transfer_done", codecForBoolean())
       .property("wire_types", codecForList(codecForString()))
@@ -439,7 +441,7 @@ export const codecForBankWithdrawalOperationPostResponse =
 export const codecForMerchantIncomingHistory =
   (): Codec<TalerRevenueApi.MerchantIncomingHistory> =>
     buildCodecForObject<TalerRevenueApi.MerchantIncomingHistory>()
-      .property("credit_account", codecForPaytoURI())
+      .property("credit_account", codecForPaytoString())
       .property("incoming_transactions", 
codecForList(codecForMerchantIncomingBankTransaction()))
       .build("TalerRevenueApi.MerchantIncomingHistory");
 
@@ -448,7 +450,7 @@ export const codecForMerchantIncomingBankTransaction =
     buildCodecForObject<TalerRevenueApi.MerchantIncomingBankTransaction>()
       .property("amount", codecForAmountString())
       .property("date", codecForTimestamp)
-      .property("debit_account", codecForPaytoURI())
+      .property("debit_account", codecForPaytoString())
       .property("exchange_url", codecForURL())
       .property("row_id", codecForNumber())
       .property("wtid", codecForString())
@@ -464,7 +466,7 @@ export const codecForTransferResponse =
 export const codecForIncomingHistory =
   (): Codec<TalerWireGatewayApi.IncomingHistory> =>
     buildCodecForObject<TalerWireGatewayApi.IncomingHistory>()
-      .property("credit_account", codecForString())
+      .property("credit_account", codecForPaytoString())
       .property("incoming_transactions", 
codecForList(codecForIncomingBankTransaction()))
       .build("TalerWireGatewayApi.IncomingHistory");
 
@@ -479,7 +481,7 @@ export const codecForIncomingReserveTransaction =
     buildCodecForObject<TalerWireGatewayApi.IncomingReserveTransaction>()
       .property("amount", codecForAmountString())
       .property("date", codecForTimestamp)
-      .property("debit_account", codecForPaytoURI())
+      .property("debit_account", codecForPaytoString())
       .property("reserve_pub", codecForString())
       .property("row_id", codecForNumber())
       .property("type", codecForConstString("RESERVE"))
@@ -489,9 +491,9 @@ export const codecForIncomingWadTransaction =
   (): Codec<TalerWireGatewayApi.IncomingWadTransaction> =>
     buildCodecForObject<TalerWireGatewayApi.IncomingWadTransaction>()
       .property("amount", codecForAmountString())
-      .property("credit_account", codecForPaytoURI())
+      .property("credit_account", codecForPaytoString())
       .property("date", codecForTimestamp)
-      .property("debit_account", codecForPaytoURI())
+      .property("debit_account", codecForPaytoString())
       .property("origin_exchange_url", codecForURL())
       .property("row_id", codecForNumber())
       .property("type", codecForConstString("WAD"))
@@ -501,7 +503,7 @@ export const codecForIncomingWadTransaction =
 export const codecForOutgoingHistory =
   (): Codec<TalerWireGatewayApi.OutgoingHistory> =>
     buildCodecForObject<TalerWireGatewayApi.OutgoingHistory>()
-      .property("debit_account", codecForString())
+      .property("debit_account", codecForPaytoString())
       .property("outgoing_transactions", 
codecForList(codecForOutgoingBankTransaction()))
       .build("TalerWireGatewayApi.OutgoingHistory");
 
@@ -509,7 +511,7 @@ export const codecForOutgoingBankTransaction =
   (): Codec<TalerWireGatewayApi.OutgoingBankTransaction> =>
     buildCodecForObject<TalerWireGatewayApi.OutgoingBankTransaction>()
       .property("amount", codecForAmountString())
-      .property("credit_account", codecForPaytoURI())
+      .property("credit_account", codecForPaytoString())
       .property("date", codecForTimestamp)
       .property("exchange_base_url", codecForURL())
       .property("row_id", codecForNumber())
@@ -537,7 +539,6 @@ type DecimalNumber = number;
 const codecForURL = codecForString
 const codecForLibtoolVersion = codecForString
 const codecForCurrencyName = codecForString
-const codecForPaytoURI = codecForString
 const codecForTalerWithdrawalURI = codecForString
 const codecForDecimalNumber = codecForNumber
 
@@ -583,7 +584,7 @@ export namespace TalerWireGatewayApi {
     wtid: ShortHashCode;
 
     // The recipient's account identifier as a payto URI.
-    credit_account: string;
+    credit_account: PaytoString;
   }
 
   export interface IncomingHistory {
@@ -595,7 +596,7 @@ export namespace TalerWireGatewayApi {
     // This must be one of the exchange's bank accounts.
     // Credit account is shared by all incoming transactions
     // as per the nature of the request.
-    credit_account: string;
+    credit_account: PaytoString;
 
   }
 
@@ -617,7 +618,7 @@ export namespace TalerWireGatewayApi {
     amount: AmountString;
 
     // Payto URI to identify the sender of funds.
-    debit_account: string;
+    debit_account: PaytoString;
 
     // The reserve public key extracted from the transaction details.
     reserve_pub: EddsaPublicKey;
@@ -638,10 +639,10 @@ export namespace TalerWireGatewayApi {
 
     // Payto URI to identify the receiver of funds.
     // This must be one of the exchange's bank accounts.
-    credit_account: string;
+    credit_account: PaytoString;
 
     // Payto URI to identify the sender of funds.
-    debit_account: string;
+    debit_account: PaytoString;
 
     // Base URL of the exchange that originated the wad.
     origin_exchange_url: string;
@@ -660,7 +661,7 @@ export namespace TalerWireGatewayApi {
     // This must be one of the exchange's bank accounts.
     // Credit account is shared by all incoming transactions
     // as per the nature of the request.
-    debit_account: string;
+    debit_account: PaytoString;
 
   }
 
@@ -676,7 +677,7 @@ export namespace TalerWireGatewayApi {
     amount: AmountString;
 
     // Payto URI to identify the receiver of funds.
-    credit_account: string;
+    credit_account: PaytoString;
 
     // The wire transfer ID in the outgoing transaction.
     wtid: ShortHashCode;
@@ -697,7 +698,7 @@ export namespace TalerWireGatewayApi {
     // Usually this account must be created by the test harness before this 
API is
     // used.  An exception is the "exchange-fakebank", where any debit account 
can be
     // specified, as it is automatically created.
-    debit_account: string;
+    debit_account: PaytoString;
   }
 
   export interface AddIncomingResponse {
@@ -729,7 +730,7 @@ export namespace TalerRevenueApi {
     // This must be one of the merchant's bank accounts.
     // Credit account is shared by all incoming transactions
     // as per the nature of the request.
-    credit_account: string;
+    credit_account: PaytoString;
 
   }
 
@@ -745,7 +746,7 @@ export namespace TalerRevenueApi {
     amount: AmountString;
 
     // Payto URI to identify the sender of funds.
-    debit_account: string;
+    debit_account: PaytoString;
 
     // Base URL of the exchange where the transfer originated form.
     exchange_url: string;
@@ -791,7 +792,7 @@ export namespace TalerBankIntegrationApi {
 
     // Bank account of the customer that is withdrawing, as a
     // payto URI.
-    sender_wire?: string;
+    sender_wire?: PaytoString;
 
     // Suggestion for an exchange given by the bank.
     suggested_exchange?: string;
@@ -811,7 +812,7 @@ export namespace TalerBankIntegrationApi {
     reserve_pub: string;
 
     // Payto address of the exchange selected for the withdrawal.
-    selected_exchange: string;
+    selected_exchange: PaytoString;
   }
 
   export interface BankWithdrawalOperationPostResponse {
@@ -867,7 +868,7 @@ export namespace TalerCorebankApi {
     withdrawal_id: string;
 
     // URI that can be passed to the wallet to initiate the withdrawal.
-    taler_withdraw_uri: string;
+    taler_withdraw_uri: TalerActionString;
   }
   export interface BankAccountGetWithdrawalResponse {
     // Amount that will be withdrawn with this withdrawal operation.
@@ -891,7 +892,7 @@ export namespace TalerCorebankApi {
     // Exchange account selected by the wallet, or by the bank
     // (with the default exchange) in case the wallet did not provide one
     // through the Integration API.
-    selected_exchange_account: string | undefined;
+    selected_exchange_account: PaytoString | undefined;
   }
 
   export interface BankAccountTransactionsResponse {
@@ -899,8 +900,8 @@ export namespace TalerCorebankApi {
   }
 
   export interface BankAccountTransactionInfo {
-    creditor_payto_uri: string;
-    debtor_payto_uri: string;
+    creditor_payto_uri: PaytoString;
+    debtor_payto_uri: PaytoString;
 
     amount: AmountString;
     direction: "debit" | "credit";
@@ -916,13 +917,13 @@ export namespace TalerCorebankApi {
   export interface CreateBankAccountTransactionCreate {
     // Address in the Payto format of the wire transfer receiver.
     // It needs at least the 'message' query string parameter.
-    payto_uri: string;
+    payto_uri: PaytoString;
 
     // Transaction amount (in the $currency:x.y format), optional.
     // However, when not given, its value must occupy the 'amount'
     // query string parameter of the 'payto' field.  In case it
     // is given in both places, the paytoUri's takes the precedence.
-    amount?: string;
+    amount?: AmountString;
   }
 
   export interface RegisterAccountRequest {
@@ -958,11 +959,11 @@ export namespace TalerCorebankApi {
     // Payments will be sent to this bank account
     // when the user wants to convert the local currency
     // back to fiat currency outside libeufin-bank.
-    cashout_payto_uri?: string;
+    cashout_payto_uri?: PaytoString;
 
     // Internal payto URI of this bank account.
     // Used mostly for testing.
-    internal_payto_uri?: string;
+    internal_payto_uri?: PaytoString;
   }
   export interface ChallengeContactData {
 
@@ -987,7 +988,7 @@ export namespace TalerCorebankApi {
     // Payments will be sent to this bank account
     // when the user wants to convert the local currency
     // back to fiat currency outside libeufin-bank.
-    cashout_address?: string;
+    cashout_address?: PaytoString;
 
     // Legal name associated with $username.
     // When missing, the old name is kept.
@@ -1011,7 +1012,7 @@ export namespace TalerCorebankApi {
     public_accounts: PublicAccount[];
   }
   export interface PublicAccount {
-    payto_uri: string;
+    payto_uri: PaytoString;
 
     balance: Balance;
 
@@ -1049,7 +1050,7 @@ export namespace TalerCorebankApi {
     balance: Balance;
 
     // payto://-URI of the account.
-    payto_uri: string;
+    payto_uri: PaytoString;
 
     // Number indicating the max debit allowed for the requesting user.
     debit_threshold: AmountString;
@@ -1062,7 +1063,7 @@ export namespace TalerCorebankApi {
     // in the merchants' circuit.  One example is the exchange:
     // that never cashouts.  Registering these accounts can
     // be done via the access API.
-    cashout_payto_uri?: string;
+    cashout_payto_uri?: PaytoString;
   }
 
 
@@ -1151,7 +1152,7 @@ export namespace TalerCorebankApi {
 
     // Fiat bank account that will receive the cashed out amount.
     // Specified as a payto URI.
-    credit_payto_uri: string;
+    credit_payto_uri: PaytoString;
 
     // Time when the cashout was created.
     creation_time: Timestamp;
diff --git a/packages/taler-util/src/index.node.ts 
b/packages/taler-util/src/index.node.ts
index 018b4767f..619da0127 100644
--- a/packages/taler-util/src/index.node.ts
+++ b/packages/taler-util/src/index.node.ts
@@ -21,3 +21,4 @@ initNodePrng();
 export * from "./index.js";
 export * from "./talerconfig.js";
 export * from "./globbing/minimatch.js";
+export { setPrintHttpRequestAsCurl } from "./http-impl.node.js";
\ No newline at end of file
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index 85870afcd..3df174944 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -15,6 +15,7 @@
  */
 
 import { generateFakeSegwitAddress } from "./bitcoin.js";
+import { Codec, Context, DecodingError, renderContext } from "./codec.js";
 import { URLSearchParams } from "./url.js";
 
 export type PaytoUri =
@@ -23,6 +24,27 @@ export type PaytoUri =
   | PaytoUriTalerBank
   | PaytoUriBitcoin;
 
+declare const __payto_str: unique symbol;
+export type PaytoString = string & { [__payto_str]: true };
+
+export function codecForPaytoString(): Codec<PaytoString> {
+  return {
+    decode(x: any, c?: Context): PaytoString {
+      if (typeof x !== "string") {
+        throw new DecodingError(
+          `expected string at ${renderContext(c)} but got ${typeof x}`,
+        );
+      }
+      if (!x.startsWith(paytoPfx)) {
+        throw new DecodingError(
+          `expected start with payto at ${renderContext(c)} but got "${x}"`,
+        );
+      }
+      return x as PaytoString;
+    },
+  };
+}
+
 export interface PaytoUriGeneric {
   targetType: PaytoType | string;
   targetPath: string;
@@ -143,13 +165,13 @@ export function addPaytoQueryParams(
  * @param p
  * @returns
  */
-export function stringifyPaytoUri(p: PaytoUri): string {
+export function stringifyPaytoUri(p: PaytoUri): PaytoString {
   const url = new URL(`${paytoPfx}${p.targetType}/${p.targetPath}`);
   const paramList = !p.params ? [] : Object.entries(p.params);
   paramList.forEach(([key, value]) => {
     url.searchParams.set(key, value);
   });
-  return url.href;
+  return url.href as PaytoString;
 }
 
 /**
diff --git a/packages/taler-util/src/taleruri.ts 
b/packages/taler-util/src/taleruri.ts
index 9568636b8..cf5d3f413 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -14,6 +14,7 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
+import { Codec, Context, DecodingError, renderContext } from "./codec.js";
 import { canonicalizeBaseUrl } from "./helpers.js";
 import { AmountString } from "./taler-types.js";
 import { URLSearchParams, URL } from "./url.js";
@@ -32,6 +33,27 @@ export type TalerUri =
   | WithdrawExchangeUri
   | AuditorUri;
 
+declare const __action_str: unique symbol;
+export type TalerActionString = string & { [__action_str]: true };
+
+export function codecForTalerActionString(): Codec<TalerActionString> {
+  return {
+    decode(x: any, c?: Context): TalerActionString {
+      if (typeof x !== "string") {
+        throw new DecodingError(
+          `expected string at ${renderContext(c)} but got ${typeof x}`,
+        );
+      }
+      if (parseTalerUri(x) === undefined) {
+        throw new DecodingError(
+          `invalid taler action at ${renderContext(c)} but got "${x}"`,
+        );
+      }
+      return x as TalerActionString;
+    },
+  };
+}
+
 export interface PayUriResult {
   type: TalerUriAction.Pay;
   merchantBaseUrl: string;
diff --git a/packages/web-util/src/hooks/useNotifications.ts 
b/packages/web-util/src/hooks/useNotifications.ts
index 8f9e0e835..ca67c5b9b 100644
--- a/packages/web-util/src/hooks/useNotifications.ts
+++ b/packages/web-util/src/hooks/useNotifications.ts
@@ -1,6 +1,6 @@
-import { TranslatedString } from "@gnu-taler/taler-util";
+import { TalerError, TalerErrorCode, TranslatedString } from 
"@gnu-taler/taler-util";
 import { useEffect, useState } from "preact/hooks";
-import { memoryMap } from "../index.browser.js";
+import { memoryMap, useTranslationContext } from "../index.browser.js";
 
 export type NotificationMessage = ErrorNotification | InfoNotification;
 
@@ -105,3 +105,97 @@ function hash(msg: NotificationMessage): string {
   }
   return hashCode(str);
 }
+
+export function useLocalNotification(): [Notification | undefined, (n: 
NotificationMessage) => void, (cb: () => Promise<void>) => Promise<void>] {
+  const {i18n} = useTranslationContext();
+
+  const [value, setter] = useState<NotificationMessage>();
+  const notif = !value ? undefined : {
+    message: value,
+    remove: () => {
+      setter(undefined);
+    },
+  }
+
+  async function errorHandling(cb: () => Promise<void>) {
+    try {
+      return await cb()
+    } catch (error: unknown) {
+      if (error instanceof TalerError) {
+        notify(buildRequestErrorMessage(i18n, error))
+      } else {
+        notifyError(
+          i18n.str`Operation failed, please report`,
+          (error instanceof Error
+            ? error.message
+            : JSON.stringify(error)) as TranslatedString
+        )
+      }
+
+    }
+  }
+  return [notif, setter, errorHandling]
+}
+
+type Translator = ReturnType<typeof useTranslationContext>["i18n"]
+
+function buildRequestErrorMessage( i18n: Translator, cause: TalerError): 
ErrorNotification {
+  let result: ErrorNotification;
+  switch (cause.errorDetail.code) {
+    case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+      result = {
+        type: "error",
+        title: i18n.str`Request timeout`,
+        description: cause.message as TranslatedString,
+        debug: JSON.stringify(cause.errorDetail, undefined, 2),
+      };
+      break;
+    }
+    case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+      result = {
+        type: "error",
+        title: i18n.str`Request throttled`,
+        description: cause.message as TranslatedString,
+        debug: JSON.stringify(cause.errorDetail, undefined, 2),
+      };
+      break;
+    }
+    case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+      result = {
+        type: "error",
+        title: i18n.str`Malformed response`,
+        description: cause.message as TranslatedString,
+        debug: JSON.stringify(cause.errorDetail, undefined, 2),
+      };
+      break;
+    }
+    case TalerErrorCode.WALLET_NETWORK_ERROR: {
+      result = {
+        type: "error",
+        title: i18n.str`Network error`,
+        description: cause.message as TranslatedString,
+        debug: JSON.stringify(cause.errorDetail, undefined, 2),
+      };
+      break;
+    }
+    case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+      result = {
+        type: "error",
+        title: i18n.str`Unexpected request error`,
+        description: cause.message as TranslatedString,
+        debug: JSON.stringify(cause.errorDetail, undefined, 2),
+      };
+      break;
+    }
+    default: {
+      result = {
+        type: "error",
+        title: i18n.str`Unexpected error`,
+        description: cause.message as TranslatedString,
+        debug: JSON.stringify(cause.errorDetail, undefined, 2),
+      };
+      break;
+    }
+  }
+  return result;
+}

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