gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 02/02: some ui fixes


From: gnunet
Subject: [taler-wallet-core] 02/02: some ui fixes
Date: Thu, 19 Oct 2023 20:11:26 +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 7582855e2723e11de25f10b6d5ba8a0757616765
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Oct 19 15:10:18 2023 -0300

    some ui fixes
---
 .../src/components/Transactions/views.tsx          |  16 +-
 packages/demobank-ui/src/hooks/access.ts           |  22 ++-
 packages/demobank-ui/src/hooks/circuit.ts          |  15 +-
 packages/demobank-ui/src/i18n/strings.ts           |   2 +-
 packages/demobank-ui/src/pages/LoginForm.tsx       |  69 ++++-----
 .../demobank-ui/src/pages/OperationState/state.ts  |  58 ++------
 packages/demobank-ui/src/pages/PaymentOptions.tsx  |  52 +++----
 .../src/pages/PaytoWireTransferForm.tsx            |  54 +++----
 packages/demobank-ui/src/pages/QrCodeSection.tsx   |  28 +---
 .../demobank-ui/src/pages/RegistrationPage.tsx     | 163 ++++++++-------------
 .../demobank-ui/src/pages/ShowAccountDetails.tsx   |  77 +++++-----
 .../src/pages/UpdateAccountPassword.tsx            |  36 ++---
 .../demobank-ui/src/pages/WalletWithdrawForm.tsx   |  18 +--
 .../src/pages/WithdrawalConfirmationQuestion.tsx   |  47 ++----
 .../demobank-ui/src/pages/WithdrawalQRCode.tsx     |  16 +-
 .../src/pages/admin/CreateNewAccount.tsx           |  17 +--
 .../demobank-ui/src/pages/admin/RemoveAccount.tsx  |  15 +-
 packages/demobank-ui/src/pages/business/Home.tsx   |  93 +++---------
 packages/demobank-ui/src/utils.ts                  |  27 +++-
 19 files changed, 306 insertions(+), 519 deletions(-)

diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx 
b/packages/demobank-ui/src/components/Transactions/views.tsx
index 404e25619..5cdb47a0c 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Transactions/views.tsx
@@ -17,7 +17,7 @@
 import { useTranslationContext } from "@gnu-taler/web-util/browser";
 import { format } from "date-fns";
 import { Fragment, h, VNode } from "preact";
-import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
+import { doAutoFocus, RenderAmount } from 
"../../pages/PaytoWireTransferForm.js";
 import { State } from "./index.js";
 
 export function LoadingUriView({ error }: State.LoadingUriError): VNode {
@@ -57,7 +57,7 @@ export function ReadyView({ transactions, onNext, onPrev }: 
State.Ready): VNode
               <th scope="col" class="pl-2 py-3.5 text-left text-sm 
font-semibold text-gray-900 ">{i18n.str`Date`}</th>
               <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 
text-left text-sm font-semibold text-gray-900 ">{i18n.str`Amount`}</th>
               <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 
text-left text-sm font-semibold text-gray-900 ">{i18n.str`Counterpart`}</th>
-              <th scope="col" class="pl-2 py-3.5 text-left text-sm 
font-semibold text-gray-900 ">{i18n.str`Subject`}</th>
+              <th scope="col" class="hidden sm:table-cell pl-2 py-3.5 
text-left text-sm font-semibold text-gray-900 ">{i18n.str`Subject`}</th>
             </tr>
           </thead>
           <tbody>
@@ -70,10 +70,7 @@ export function ReadyView({ transactions, onNext, onPrev }: 
State.Ready): VNode
                 </tr>
                 {txs.map(item => {
                   const time = item.when.t_ms === "never" ? "" : 
format(item.when.t_ms, "HH:mm:ss")
-                  const amount = <Fragment>
-                    { }
-                  </Fragment>
-                  return (<tr key={idx}>
+                  return (<tr key={idx} class="border-b border-gray-200 
last:border-none">
                     <td class="relative py-2 pl-2 pr-2 text-sm ">
                       <div class="font-medium text-gray-900">{time}</div>
                       <dl class="font-normal sm:hidden">
@@ -91,6 +88,11 @@ export function ReadyView({ transactions, onNext, onPrev }: 
State.Ready): VNode
                         <dd class="mt-1 truncate text-gray-500 sm:hidden">
                           {item.negative ? i18n.str`to` : i18n.str`from`} 
{item.counterpart}
                         </dd>
+                        <dd class="mt-1 text-gray-500 sm:hidden" >
+                          <pre class="break-words w-56 whitespace-break-spaces 
p-2 rounded-md mx-auto my-2 bg-gray-100">
+                          {item.subject}
+                          </pre>
+                        </dd>
                       </dl>
                     </td>
                     <td data-negative={item.negative ? "true" : "false"}
@@ -101,7 +103,7 @@ export function ReadyView({ transactions, onNext, onPrev }: 
State.Ready): VNode
                       )}
                     </td>
                     <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-gray-500">{item.counterpart}</td>
-                    <td class="px-3 py-3.5 text-sm text-gray-500 break-all 
min-w-md">{item.subject}</td>
+                    <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-gray-500 break-all min-w-md">{item.subject}</td>
                   </tr>)
                 })}
               </Fragment>
diff --git a/packages/demobank-ui/src/hooks/access.ts 
b/packages/demobank-ui/src/hooks/access.ts
index 2533d32fe..7023b8803 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -38,7 +38,8 @@ export function useAccountDetails(account: string) {
     return await api.getAccount({ username, token })
   }
   const token = credentials.status !== "loggedIn" ? undefined : 
credentials.token
-  const { data, error } = useSWR<TalerCoreBankResultByMethod<"getAccount">, 
TalerHttpError>([account, token], fetcher, {
+  const { data, error } = useSWR<TalerCoreBankResultByMethod<"getAccount">, 
TalerHttpError>(
+    [account, token, "getAccount"], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -65,7 +66,7 @@ export function useWithdrawalDetails(wid: string) {
   }
 
   const { data, error } = 
useSWR<TalerCoreBankResultByMethod<"getWithdrawalById">, TalerHttpError>(
-    [wid], fetcher, {
+    [wid, "getWithdrawalById"], fetcher, {
     refreshInterval: 1000,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -92,7 +93,7 @@ export function useTransactionDetails(account: string, tid: 
number) {
   }
 
   const { data, error } = 
useSWR<TalerCoreBankResultByMethod<"getTransactionById">, TalerHttpError>(
-    [account, token, tid], fetcher, {
+    [account, token, tid, "getTransactionById"], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -121,7 +122,18 @@ export function usePublicAccounts(initial?: number) {
     })
   }
 
-  const { data, error } = 
useSWR<TalerCoreBankResultByMethod<"getPublicAccounts">, 
TalerHttpError>([offset], fetcher);
+  const { data, error } = 
useSWR<TalerCoreBankResultByMethod<"getPublicAccounts">, TalerHttpError>(
+    [offset, "getPublicAccounts"], fetcher, {
+      refreshInterval: 0,
+      refreshWhenHidden: false,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+      refreshWhenOffline: false,
+      errorRetryCount: 0,
+      errorRetryInterval: 1,
+      shouldRetryOnError: false,
+      keepPreviousData: true,  
+    });
 
   const isLastPage =
     data && data.body.public_accounts.length < PAGE_SIZE;
@@ -174,7 +186,7 @@ export function useTransactions(account: string, initial?: 
number) {
   }
 
   const { data, error } = 
useSWR<TalerCoreBankResultByMethod<"getTransactions">, TalerHttpError>(
-    [account, token, offset], fetcher, {
+    [account, token, offset, "getTransactions"], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     refreshWhenOffline: false,
diff --git a/packages/demobank-ui/src/hooks/circuit.ts 
b/packages/demobank-ui/src/hooks/circuit.ts
index 208663f8b..0f7af5fe5 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -106,9 +106,8 @@ export function useRatiosAndFeeConfig() {
     return api.getConversionRates()
   }
 
-  const { data, error } = useSWR<
-    TalerCoreBankResultByMethod<"getConversionRates">, TalerHttpError
-  >([], fetcher, {
+  const { data, error } = 
useSWR<TalerCoreBankResultByMethod<"getConversionRates">, TalerHttpError>(
+    [, "getConversionRates"], fetcher, {
     refreshInterval: 60 * 1000,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -138,7 +137,7 @@ export function useBusinessAccounts() {
 
   const [offset, setOffset] = useState<string | undefined>();
 
-  function fetcher(token: AccessToken, offset?: string) {
+  function fetcher([token, offset]: [AccessToken, string]) {
     return api.getAccounts(token, {
       limit: MAX_RESULT_SIZE,
       offset,
@@ -147,7 +146,7 @@ export function useBusinessAccounts() {
   }
 
   const { data, error } = useSWR<TalerCoreBankResultByMethod<"getAccounts">, 
TalerHttpError>(
-    [token, offset], fetcher, {
+    [token, offset, "getAccounts"], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -192,8 +191,8 @@ function notUndefined(c: CashoutWithId | undefined): c is 
CashoutWithId {
 }
 export function useCashouts(account: string) {
   const { state: credentials } = useBackendState();
-  const token = credentials.status !== "loggedIn" ? undefined : 
credentials.token
   const { api } = useBankCoreApiContext();
+  const token = credentials.status !== "loggedIn" ? undefined : 
credentials.token
 
   async function fetcher([username, token]: [string, AccessToken]) {
     const list = await api.getAccountCashouts({ username, token })
@@ -212,7 +211,7 @@ export function useCashouts(account: string) {
   }
 
   const { data, error } = useSWR<OperationOk<{ cashouts: CashoutWithId[] }>, 
TalerHttpError>(
-    [account, token], fetcher, {
+    [account, token, "getAccountCashouts"], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -239,7 +238,7 @@ export function useCashoutDetails(cashoutId: string) {
   }
 
   const { data, error } = 
useSWR<TalerCoreBankResultByMethod<"getCashoutById">, TalerHttpError>(
-    [creds?.username, creds?.token, cashoutId], fetcher, {
+    [creds?.username, creds?.token, cashoutId, "getCashoutById"], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
diff --git a/packages/demobank-ui/src/i18n/strings.ts 
b/packages/demobank-ui/src/i18n/strings.ts
index 1b19f7df7..86d1fff5b 100644
--- a/packages/demobank-ui/src/i18n/strings.ts
+++ b/packages/demobank-ui/src/i18n/strings.ts
@@ -167,7 +167,7 @@ strings["en"] = {
       "Transfer creation gave response error": [""],
       "Wire transfer created!": [""],
       "Amount to withdraw:": ["Amount to withdraw"],
-      Withdraw: ["Confirm withdrawal"],
+      Withdraw: ["Withdraw"],
       "No credentials given.": [""],
       "Could not create withdrawal operation": [""],
       "Withdrawal creation gave response error": [""],
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx 
b/packages/demobank-ui/src/pages/LoginForm.tsx
index a8167cca5..981b0f880 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -21,7 +21,7 @@ import { useEffect, useRef, useState } from "preact/hooks";
 import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
 import { useBackendContext } from "../context/backend.js";
 import { bankUiSettings } from "../settings.js";
-import { undefinedIfEmpty } from "../utils.js";
+import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
 import { doAutoFocus } from "./PaytoWireTransferForm.js";
 import { useBankCoreApiContext } from "../context/config.js";
 import { assertUnreachable } from "./HomePage.js";
@@ -78,43 +78,36 @@ export function LoginForm({ reason, onRegister }: { 
reason?: "not-found" | "forb
   async function doLogin() {
     if (!username || !password) return;
     setBusy({})
-    const data: TalerAuthentication.TokenRequest = {
-      // scope: "readwrite" as "write", //FIX: different than merchant
-      scope: "readwrite",
-      duration: {
-        d_us: "forever" //FIX: should return shortest
-        // d_us: 60 * 60 * 24 * 7 * 1000 * 1000
-      },
-      refreshable: true,
-    }
-    const resp = await 
api.getAuthenticationAPI(username).createAccessToken(password, {
-      // scope: "readwrite" as "write", //FIX: different than merchant
-      scope: "readwrite",
-      duration: {
-        d_us: "forever" //FIX: should return shortest
-        // d_us: 60 * 60 * 24 * 7 * 1000 * 1000
-      },
-      refreshable: true,
-    })
-    if (resp.type === "ok") {
-      backend.logIn({ username, token: resp.body.access_token });
-    } else {
-      switch (resp.case) {
-        case "wrong-credentials": return notify({
-          type: "error",
-          title: i18n.str`Wrong credentials for "${username}"`,
-          description: resp.detail.hint as TranslatedString,
-          debug: resp.detail,
-        })
-        case "not-found": return notify({
-          type: "error",
-          title: i18n.str`Account not found`,
-          description: resp.detail.hint as TranslatedString,
-          debug: resp.detail,
-        })
-        default: assertUnreachable(resp)
+    await withRuntimeErrorHandling(i18n, async () => {
+      const resp = await 
api.getAuthenticationAPI(username).createAccessToken(password, {
+        // scope: "readwrite" as "write", //FIX: different than merchant
+        scope: "readwrite",
+        duration: {
+          d_us: "forever" //FIX: should return shortest
+          // d_us: 60 * 60 * 24 * 7 * 1000 * 1000
+        },
+        refreshable: true,
+      })
+      if (resp.type === "ok") {
+        backend.logIn({ username, token: resp.body.access_token });
+      } else {
+        switch (resp.case) {
+          case "wrong-credentials": return notify({
+            type: "error",
+            title: i18n.str`Wrong credentials for "${username}"`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          })
+          case "not-found": return notify({
+            type: "error",
+            title: i18n.str`Account not found`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          })
+          default: assertUnreachable(resp)
+        }
       }
-    }
+    })
     setPassword(undefined);
     setBusy(undefined)
   }
@@ -198,7 +191,7 @@ export function LoginForm({ reason, onRegister }: { 
reason?: "not-found" | "forb
             <button type="submit"
               class="rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 
text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-indigo-600"
               disabled={!!errors}
-              onClick={(e) => {
+              onClick={async (e) => {
                 e.preventDefault()
                 doLogin()
               }}
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts 
b/packages/demobank-ui/src/pages/OperationState/state.ts
index 148571ec9..c9c1fa238 100644
--- a/packages/demobank-ui/src/pages/OperationState/state.ts
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -21,7 +21,7 @@ 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 } from "../../utils.js";
+import { buildRequestErrorMessage, withRuntimeErrorHandling } from 
"../../utils.js";
 import { Props, State } from "./index.js";
 import { assertUnreachable } from "../HomePage.js";
 import { mutate } from "swr";
@@ -41,9 +41,8 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
   async function doSilentStart() {
     //FIXME: if amount is not enough use balance
     const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
-
-    try {
-      if (!creds) return;
+    if (!creds) return;
+    await withRuntimeErrorHandling(i18n, async () => {
       const resp = await api.createWithdrawal(creds, {
         amount: Amounts.stringify(parsedAmount),
       });
@@ -67,18 +66,7 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
       } else {
         updateSettings("currentWithdrawalOperationId", 
uri.withdrawalOperationId)
       }
-    } catch (error) {
-      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
-        )
-      }
-    }
+    })
   }
 
   const withdrawalOperationId = settings.currentWithdrawalOperationId
@@ -98,10 +86,9 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
   const wid = withdrawalOperationId
 
   async function doAbort() {
-    try {
-      setBusy({})
+    setBusy({})
+    await withRuntimeErrorHandling(i18n, async () => {
       const resp = await api.abortWithdrawalById(wid);
-      setBusy(undefined)
       if (resp.type === "ok") {
         onClose();
       } else {
@@ -115,30 +102,19 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
           default: assertUnreachable(resp.case)
         }
       }
-    } catch (error) {
-      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
-        )
-      }
-    }
+    })
+    setBusy(undefined)
   }
 
   async function doConfirm() {
-    try {
-      setBusy({})
+    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!`)
         }
-        setBusy(undefined)
       } else {
         switch (resp.case) {
           case "previously-aborted": return notify({
@@ -156,18 +132,8 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
           default: assertUnreachable(resp)
         }
       }
-    } catch (error) {
-      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
-        )
-      }
-    }
+    })
+    setBusy(undefined)
   }
 
   const uri = stringifyWithdrawUri({
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx 
b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index f60ba3270..c36d58691 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -18,7 +18,7 @@ import { AmountJson } from "@gnu-taler/taler-util";
 import { notifyInfo, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { h, VNode } from "preact";
 import { useState } from "preact/hooks";
-import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
+import { PaytoWireTransferForm, doAutoFocus } from 
"./PaytoWireTransferForm.js";
 import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
 import { useSettings } from "../hooks/settings.js";
 
@@ -30,7 +30,7 @@ export function PaymentOptions({ limit, goToConfirmOperation 
}: { limit: AmountJ
   const { i18n } = useTranslationContext();
   const [settings] = useSettings();
 
-  const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | 
undefined>();
+  const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | 
undefined>("wire-transfer");
 
   return (
     <div class="mt-2">
@@ -46,28 +46,28 @@ export function PaymentOptions({ limit, 
goToConfirmOperation }: { limit: AmountJ
             <input type="radio" name="project-type" value="Newsletter" 
class="sr-only" aria-labelledby="project-type-0-label" 
aria-describedby="project-type-0-description-0 project-type-0-description-1" 
onClick={() => {
               setTab("charge-wallet")
             }} />
-            <span class="flex flex-1">
+            <div class="flex flex-col">
+              <span class="flex">
               <div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
-              <span class="flex flex-col">
-                <span id="project-type-0-label" class="block text-sm 
font-medium text-gray-900">
-                  <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
-                </span>
-                <span id="project-type-0-description-0" class="mt-1 flex 
items-center text-sm text-gray-500">
-                  <i18n.Translate>Withdraw digital money into your mobile 
wallet or browser extension</i18n.Translate>
+                <span class="grow self-center text-lg text-gray-900 
align-middle text-center">
+                <i18n.Translate>a <b>Taler</b> wallet</i18n.Translate>
                 </span>
+                <svg class="self-center flex-none h-5 w-5 text-indigo-600" 
style={{ visibility: tab === "charge-wallet" ? "visible" : "hidden" }} 
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+                  <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 
16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 
1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+                </svg>
+              </span>
+              <div class="mt-1 flex items-center text-sm text-gray-500">
+                <i18n.Translate>Withdraw digital money into your mobile wallet 
or browser extension</i18n.Translate>
+              </div>
                 {!!settings.currentWithdrawalOperationId &&
-                  <span class="inline-flex items-center gap-x-1.5 w-fit 
rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">
+                  <span class="flex items-center gap-x-1.5 w-fit rounded-md 
bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
                     <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" 
aria-hidden="true">
                       <circle cx="3" cy="3" r="3" />
                     </svg>
                     <i18n.Translate>operation ready</i18n.Translate>
                   </span>
                 }
-              </span>
-            </span>
-            <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === 
"charge-wallet" ? "visible" : "hidden" }} viewBox="0 0 20 20" 
fill="currentColor" aria-hidden="true">
-              <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 
16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 
1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
-            </svg>
+            </div>
           </label>
 
 
@@ -75,20 +75,20 @@ export function PaymentOptions({ limit, 
goToConfirmOperation }: { limit: AmountJ
             <input type="radio" name="project-type" value="Existing Customers" 
class="sr-only" aria-labelledby="project-type-1-label" 
aria-describedby="project-type-1-description-0 project-type-1-description-1" 
onClick={() => {
               setTab("wire-transfer")
             }} />
-            <span class="flex flex-1">
-              <div class="text-4xl mr-4 my-auto">&#x2194;</div>
-              <span class="flex flex-col">
-                <span id="project-type-1-label" class="block text-sm 
font-medium text-gray-900">
+            <div class="flex flex-col">
+              <span class="flex">
+                <div class="text-4xl mr-4 my-auto">&#x2194;</div>
+                <span class="grow self-center text-lg font-medium 
text-gray-900 align-middle text-center">
                   <i18n.Translate>another bank account</i18n.Translate>
                 </span>
-                <span id="project-type-1-description-0" class="mt-1 flex 
items-center text-sm text-gray-500">
-                  <i18n.Translate>Make a wire transfer to an account which you 
know the bank account number</i18n.Translate>
-                </span>
+                <svg class="self-center flex-none h-5 w-5 text-indigo-600" 
style={{ visibility: tab === "wire-transfer" ? "visible" : "hidden" }} 
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
+                  <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 
16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 
1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+                </svg>
               </span>
-            </span>
-            <svg class="h-5 w-5 text-indigo-600" style={{ visibility: tab === 
"wire-transfer" ? "visible" : "hidden" }} viewBox="0 0 20 20" 
fill="currentColor" aria-hidden="true">
-              <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 
16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 
1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
-            </svg>
+              <div class="mt-1 flex items-center text-sm text-gray-500">
+                <i18n.Translate>Make a wire transfer to an account which you 
know the bank account number</i18n.Translate>
+              </div>
+            </div>
           </label>
         </div>
         {tab === "charge-wallet" && (
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 7861bb0b3..e713324c5 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -39,6 +39,7 @@ import {
   buildRequestErrorMessage,
   undefinedIfEmpty,
   validateIBAN,
+  withRuntimeErrorHandling,
 } from "../utils.js";
 import { useBankCoreApiContext } from "../context/config.js";
 import { useBackendState } from "../hooks/backend.js";
@@ -60,8 +61,7 @@ export function PaytoWireTransferForm({
   onCancel: (() => void) | undefined;
   limit: AmountJson;
 }): VNode {
-  const [isRawPayto, setIsRawPayto] = useState(false);
-  // FIXME: remove this
+  const [isRawPayto, setIsRawPayto] = useState(true);
   const { state: credentials } = useBackendState()
   const { api } = useBankCoreApiContext();
   const [iban, setIban] = useState<string | undefined>();
@@ -73,10 +73,6 @@ export function PaytoWireTransferForm({
   );
   const { i18n } = useTranslationContext();
   const ibanRegex = "^[A-Z][A-Z][0-9]+$";
-  const ref = useRef<HTMLInputElement>(null);
-  useEffect(() => {
-    if (focus) ref.current?.focus();
-  }, [focus, isRawPayto]);
 
   const trimmedAmountStr = amount?.trim();
   const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
@@ -100,8 +96,6 @@ export function PaytoWireTransferForm({
             : undefined,
   });
 
-  // const { createTransaction } = useAccessAPI();
-
   const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
 
   const errorsPayto = undefinedIfEmpty({
@@ -125,12 +119,13 @@ export function PaytoWireTransferForm({
   async function doSend() {
     let payto_uri: string | undefined;
     let sendingAmount: AmountString | undefined;
+    if (credentials.status !== "loggedIn") return;
     if (rawPaytoInput) {
       const p = parsePaytoUri(rawPaytoInput)
       if (!p) return;
       sendingAmount = p.params.amount
       delete p.params.amount
-      //it should have message
+      //if this payto is valid then it already have message
       payto_uri = stringifyPaytoUri(p)
     } else {
       if (!iban || !subject) return;
@@ -139,11 +134,11 @@ export function PaytoWireTransferForm({
       payto_uri = stringifyPaytoUri(ibanPayto);
       sendingAmount = `${limit.currency}:${trimmedAmountStr}`
     }
+    const puri = payto_uri;
 
-    try {
-      if (credentials.status !== "loggedIn") return;
+    await withRuntimeErrorHandling(i18n, async () => {
       const res = await api.createTransaction(credentials, {
-        payto_uri,
+        payto_uri: puri,
         amount: sendingAmount,
       });
       mutate(() => true)
@@ -168,32 +163,20 @@ export function PaytoWireTransferForm({
       setAmount(undefined);
       setIban(undefined);
       setSubject(undefined);
-      rawPaytoInputSetter(undefined)
-    } catch (error) {
-      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
-        )
-      }
-    }
-
+      rawPaytoInputSetter(undefined)      
+    })
   }
 
   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">
     {/**
      * FIXME: Scan a qr code
      */}
-    <div class="px-4 sm:px-0">
+    <div class="">
       <h2 class="text-base font-semibold leading-7 text-gray-900">
         {title}
       </h2>
       <div>
-        <div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-1 
sm:gap-x-4">
+        <div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
           <label class={"relative flex cursor-pointer rounded-lg border 
bg-white p-4 shadow-sm focus:outline-none" + (!isRawPayto ? "border-indigo-600 
ring-2 ring-indigo-600" : "border-gray-300")}>
             <input type="radio" name="project-type" value="Newsletter" 
class="sr-only" aria-labelledby="project-type-0-label" 
aria-describedby="project-type-0-description-0 project-type-0-description-1" 
onChange={() => {
               if (parsed && parsed.isKnown && parsed.targetType === "iban") {
@@ -207,8 +190,6 @@ export function PaytoWireTransferForm({
                   setSubject(subject)
                 }
               }
-              
//payto://iban/DE9714548806481?amount=LOCAL%3A2&message=011Y8V8KDCPFDEKPDZTHS7KZ14GHX7BVWKRDDPZ1N75TJ90166T0
-              
//payto://iban/DE9714548806481?receiver-name=Exchanger&amount=LOCAL%3A2&message=011Y8V8KDCPFDEKPDZTHS7KZ14GHX7BVWKRDDPZ1N75TJ90166T0
               setIsRawPayto(false)
             }} />
             <span class="flex flex-1">
@@ -236,7 +217,7 @@ export function PaytoWireTransferForm({
             }} />
             <span class="flex flex-1">
               <span class="flex flex-col">
-                <span class="block text-sm  font-medium text-gray-900">
+                <span class="block text-sm font-medium text-gray-900">
                   <i18n.Translate>Import payto:// URI</i18n.Translate>
                 </span>
               </span>
@@ -247,17 +228,16 @@ export function PaytoWireTransferForm({
     </div>
 
     <form
-      class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl 
md:col-span-2 w-fit mx-auto"
+      class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md 
sm:rounded-xl md:col-span-2 w-fit mx-auto"
       autoCapitalize="none"
       autoCorrect="off"
       onSubmit={e => {
         e.preventDefault()
       }}
     >
-      <div class="px-4 py-6 sm:p-8">
+      <div class="p-4 sm:p-8">
         {!isRawPayto ?
           <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">
               <label for="iban" class="block text-sm font-medium leading-6 
text-gray-900">{i18n.str`Recipient`}</label>
               <div class="mt-2">
@@ -338,8 +318,8 @@ export function PaytoWireTransferForm({
                   name="address"
                   id="address"
                   type="textarea"
-                  rows={3}
-                  class="block overflow-hidden w-64 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"
+                  rows={5}
+                  class="block overflow-hidden w-44 sm:w-96 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={rawPaytoInput ?? ""}
                   required
                   
placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`}
@@ -393,7 +373,7 @@ export function doAutoFocus(element: HTMLElement | null) {
       element.scrollIntoView({
         behavior: "smooth",
         block: "center",
-        inline: "center"
+        inline: "center",
       })
     }, 100)
   }
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx 
b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index a37de383d..64f9ec5ab 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -30,7 +30,7 @@ import {
 import { Fragment, h, VNode } from "preact";
 import { useEffect } from "preact/hooks";
 import { QR } from "../components/QR.js";
-import { buildRequestErrorMessage } from "../utils.js";
+import { buildRequestErrorMessage, withRuntimeErrorHandling } from 
"../utils.js";
 import { useBankCoreApiContext } from "../context/config.js";
 import { assertUnreachable } from "./HomePage.js";
 
@@ -55,36 +55,22 @@ export function QrCodeSection({
   const { api } = useBankCoreApiContext()
 
   async function doAbort() {
-    try {
+    await withRuntimeErrorHandling(i18n, async () => {
       const result = await 
api.abortWithdrawalById(withdrawUri.withdrawalOperationId);
       if (result.type === "ok") {
         onAborted();
       } else {
         switch (result.case) {
-          case "previously-confirmed": {
-            notify({
-              type: "info",
-              title: i18n.str`The reserve operation has been confirmed 
previously and can't be aborted`
-            })
-            break;
-          }
+          case "previously-confirmed": return notify({
+            type: "info",
+            title: i18n.str`The reserve operation has been confirmed 
previously and can't be aborted`
+          })
           default: {
             assertUnreachable(result.case)
           }
         }
       }
-    } catch (error) {
-      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 (
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx 
b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index fda2d904d..ce38a9fb8 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -24,7 +24,7 @@ import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
 import { useBackendContext } from "../context/backend.js";
 import { bankUiSettings } from "../settings.js";
-import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling 
} from "../utils.js";
 import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
 import { getRandomPassword, getRandomUsername } from "./rnd.js";
 import { useBankCoreApiContext } from "../context/config.js";
@@ -95,118 +95,80 @@ function RegistrationForm({ onComplete, onCancel }: { 
onComplete: () => void, on
   });
 
   async function doRegistrationAndLogin(name: string | undefined, username: 
string, password: string) {
-    const creationResponse = await api.createAccount("" as AccessToken, { 
name: name ?? "", username, password });
-    if (creationResponse.type === "fail") {
-      switch (creationResponse.case) {
-        case "invalid-input": return notify({
-          type: "error",
-          title: i18n.str`Some of the input fields are invalid.`,
-          description: creationResponse.detail.hint as TranslatedString,
-          debug: creationResponse.detail,
-        })
-        case "unable-to-create": return notify({
-          type: "error",
-          title: i18n.str`Unable to create that account.`,
-          description: creationResponse.detail.hint as TranslatedString,
-          debug: creationResponse.detail,
-        })
-        case "unauthorized": return notify({
-          type: "error",
-          title: i18n.str`No enough permission to create that account.`,
-          description: creationResponse.detail.hint as TranslatedString,
-          debug: creationResponse.detail,
-        })
-        case "already-exist": return notify({
-          type: "error",
-          title: i18n.str`That username is already taken`,
-          description: creationResponse.detail.hint as TranslatedString,
-          debug: creationResponse.detail,
-        })
-        default: assertUnreachable(creationResponse)
+    await withRuntimeErrorHandling(i18n, async () => {
+      const creationResponse = await api.createAccount("" as AccessToken, { 
name: name ?? "", username, password });
+      if (creationResponse.type === "fail") {
+        switch (creationResponse.case) {
+          case "invalid-input": return notify({
+            type: "error",
+            title: i18n.str`Some of the input fields are invalid.`,
+            description: creationResponse.detail.hint as TranslatedString,
+            debug: creationResponse.detail,
+          })
+          case "unable-to-create": return notify({
+            type: "error",
+            title: i18n.str`Unable to create that account.`,
+            description: creationResponse.detail.hint as TranslatedString,
+            debug: creationResponse.detail,
+          })
+          case "unauthorized": return notify({
+            type: "error",
+            title: i18n.str`No enough permission to create that account.`,
+            description: creationResponse.detail.hint as TranslatedString,
+            debug: creationResponse.detail,
+          })
+          case "already-exist": return notify({
+            type: "error",
+            title: i18n.str`That username is already taken`,
+            description: creationResponse.detail.hint as TranslatedString,
+            debug: creationResponse.detail,
+          })
+          default: assertUnreachable(creationResponse)
+        }
       }
-    }
-    const resp = await 
api.getAuthenticationAPI(username).createAccessToken(password, {
-      // scope: "readwrite" as "write", //FIX: different than merchant
-      scope: "readwrite",
-      duration: {
-        d_us: "forever" //FIX: should return shortest
-        // d_us: 60 * 60 * 24 * 7 * 1000 * 1000
-      },
-      refreshable: true,
-    })
+      const resp = await 
api.getAuthenticationAPI(username).createAccessToken(password, {
+        scope: "readwrite",
+        duration: { d_us: "forever" },
+        refreshable: true,
+      })
 
-    if (resp.type === "ok") {
-      backend.logIn({ username, token: resp.body.access_token });
-    } else {
-      switch (resp.case) {
-        case "wrong-credentials": return notify({
-          type: "error",
-          title: i18n.str`Wrong credentials for "${username}"`,
-          description: resp.detail.hint as TranslatedString,
-          debug: resp.detail,
-        })
-        case "not-found": return notify({
-          type: "error",
-          title: i18n.str`Account not found`,
-          description: resp.detail.hint as TranslatedString,
-          debug: resp.detail,
-        })
-        default: assertUnreachable(resp)
+      if (resp.type === "ok") {
+        backend.logIn({ username, token: resp.body.access_token });
+      } else {
+        switch (resp.case) {
+          case "wrong-credentials": return notify({
+            type: "error",
+            title: i18n.str`Wrong credentials for "${username}"`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          })
+          case "not-found": return notify({
+            type: "error",
+            title: i18n.str`Account not found`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          })
+          default: assertUnreachable(resp)
+        }
       }
-    }
+    })
   }
 
   async function doRegistrationStep() {
     if (!username || !password) return;
-    try {
-      await doRegistrationAndLogin(name, username, password)
-      setUsername(undefined);
-      onComplete();
-    } catch (error) {
-      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
-        )
-      }
-    }
+    await doRegistrationAndLogin(name, username, password)
+    setUsername(undefined);
     setPassword(undefined);
     setRepeatPassword(undefined);
+    onComplete();
   }
 
-  async function delay(ms: number): Promise<void> {
-    return new Promise((resolve) => {
-      setTimeout(() => {
-        resolve(undefined);
-      }, ms)
-    })
-  }
   async function doRandomRegistration(tries: number = 3) {
     const user = getRandomUsername();
     const pass = getRandomPassword();
-    try {
-      setUsername(undefined);
-      setPassword(undefined);
-      setRepeatPassword(undefined);
-      const username = `_${user.first}-${user.second}_`
-      await doRegistrationAndLogin(name, username, pass)
-      onComplete();
-    } catch (error) {
-      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
-        )
-      }
-    }
+    const username = `_${user.first}-${user.second}_`
+    await doRegistrationAndLogin(name, username, pass)
+    onComplete();
   }
 
   return (
@@ -403,8 +365,9 @@ function RegistrationForm({ onComplete, onCancel }: { 
onComplete: () => void, on
               <button type="submit"
                 class=" rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 
py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-indigo-600"
                 disabled={!!errors}
-                onClick={(e) => {
+                onClick={async (e) => {
                   e.preventDefault()
+
                   doRegistrationStep()
                 }}
               >
diff --git a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx 
b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
index 3534f9733..c65b90503 100644
--- a/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/ShowAccountDetails.tsx
@@ -7,7 +7,7 @@ import { Loading } from "../components/Loading.js";
 import { useBankCoreApiContext } from "../context/config.js";
 import { useAccountDetails } from "../hooks/access.js";
 import { useBackendState } from "../hooks/backend.js";
-import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling 
} from "../utils.js";
 import { assertUnreachable } from "./HomePage.js";
 import { LoginForm } from "./LoginForm.js";
 import { AccountForm } from "./admin/AccountForm.js";
@@ -49,50 +49,41 @@ export function ShowAccountDetails({
   async function doUpdate() {
     if (!update) {
       setUpdate(true);
-    } else {
-      if (!submitAccount || !creds) return;
-      try {
-        const resp = await api.updateAccount(creds, {
-          cashout_address: submitAccount.cashout_payto_uri,
-          challenge_contact_data: undefinedIfEmpty({
-            email: submitAccount.contact_data?.email,
-            phone: submitAccount.contact_data?.phone,
-          }),
-          is_exchange: false,
-          name: submitAccount.name,
-        });
-        if (resp.type === "ok") {
-          onUpdateSuccess();
-        } else {
-          switch (resp.case) {
-            case "unauthorized": return notify({
-              type: "error",
-              title: i18n.str`The rights to change the account are not 
sufficient`,
-              description: resp.detail.hint as TranslatedString,
-              debug: resp.detail,
-            })
-            case "not-found": return notify({
-              type: "error",
-              title: i18n.str`The username was not found`,
-              description: resp.detail.hint as TranslatedString,
-              debug: resp.detail,
-            })
-            default: assertUnreachable(resp)
-          }
-        }
-      } catch (error) {
-        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;
+    }
+    if (!submitAccount || !creds) return;
+    await withRuntimeErrorHandling(i18n, async () => {
+      const resp = await api.updateAccount(creds, {
+        cashout_address: submitAccount.cashout_payto_uri,
+        challenge_contact_data: undefinedIfEmpty({
+          email: submitAccount.contact_data?.email,
+          phone: submitAccount.contact_data?.phone,
+        }),
+        is_exchange: false,
+        name: submitAccount.name,
+      });
+      
+      if (resp.type === "ok") {
+        onUpdateSuccess();
+      } else {
+        switch (resp.case) {
+          case "unauthorized": return notify({
+            type: "error",
+            title: i18n.str`The rights to change the account are not 
sufficient`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          })
+          case "not-found": return notify({
+            type: "error",
+            title: i18n.str`The username was not found`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          })
+          default: assertUnreachable(resp)
         }
       }
-    }
+    })
+
   }
 
   return (
diff --git a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx 
b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
index ac6e9fa9b..d82dac4b1 100644
--- a/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/UpdateAccountPassword.tsx
@@ -3,7 +3,7 @@ import { HttpResponsePaginated, RequestError, notify, 
notifyError, useTranslatio
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
 import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling 
} from "../utils.js";
 import { doAutoFocus } from "./PaytoWireTransferForm.js";
 import { useBankCoreApiContext } from "../context/config.js";
 import { assertUnreachable } from "./HomePage.js";
@@ -39,7 +39,7 @@ export function UpdateAccountPassword({
 
   async function doChangePassword() {
     if (!!errors || !password || !creds) return;
-    try {
+    await withRuntimeErrorHandling(i18n, async () => {
       const resp = await api.updatePassword(creds, {
         new_password: password,
       });
@@ -47,32 +47,18 @@ export function UpdateAccountPassword({
         onUpdateSuccess();
       } else {
         switch (resp.case) {
-          case "unauthorized": {
-            notify({
-              type: "error",
-              title: i18n.str`Not authorized to change the password, maybe the 
session is invalid.`
-            })
-            break;
-          }
-          case "not-found": {
-            notify({
-              type: "error",
-              title: i18n.str`Account not found`
-            })
-            break;
-          }
+          case "unauthorized": return notify({
+            type: "error",
+            title: i18n.str`Not authorized to change the password, maybe the 
session is invalid.`
+          })
+          case "not-found": return notify({
+            type: "error",
+            title: i18n.str`Account not found`
+          })
           default: assertUnreachable(resp)
         }
       }
-    } catch (error) {
-      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 (
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx 
b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 2d80bad1f..28d5d7749 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -36,7 +36,7 @@ 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 } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling 
} from "../utils.js";
 import { assertUnreachable } from "./HomePage.js";
 import { OperationState } from "./OperationState/index.js";
 import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js";
@@ -62,6 +62,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, 
onCancel, focus }: {
 
   if (!!settings.currentWithdrawalOperationId) {
     return <Attention type="warning" title={i18n.str`There is an operation 
already`}>
+      <span ref={focus ? doAutoFocus : undefined}/>
       <i18n.Translate>
         To complete or cancel the operation click <a class="font-semibold 
text-yellow-700 hover:text-yellow-600" 
href={`#/operation/${settings.currentWithdrawalOperationId}`}>here</a>
       </i18n.Translate>
@@ -87,7 +88,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, 
onCancel, focus }: {
 
   async function doStart() {
     if (!parsedAmount || !creds) return;
-    try {
+    await withRuntimeErrorHandling(i18n, async () => {
       const result = await api.createWithdrawal(creds, {
         amount: Amounts.stringify(parsedAmount),
       });
@@ -110,18 +111,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, 
onCancel, focus }: {
           default: assertUnreachable(result.case)
         }
       }
-    } catch (error) {
-      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 <form
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx 
b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 602ec9bd8..87637f7ef 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -36,7 +36,7 @@ import {
 import { Fragment, VNode, h } from "preact";
 import { useMemo, useState } from "preact/hooks";
 import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
-import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling 
} from "../utils.js";
 import { useSettings } from "../hooks/settings.js";
 import { RenderAmount } from "./PaytoWireTransferForm.js";
 import { useBankCoreApiContext } from "../context/config.js";
@@ -88,8 +88,8 @@ export function WithdrawalConfirmationQuestion({
   }) ?? busy;
 
   async function doTransfer() {
-    try {
-      setBusy({})
+    setBusy({})
+    await withRuntimeErrorHandling(i18n, async () => {
       const resp = await 
api.confirmWithdrawalById(withdrawUri.withdrawalOperationId);
       if (resp.type === "ok") {
         mutate(() => true)// clean any info that we have
@@ -113,53 +113,28 @@ export function WithdrawalConfirmationQuestion({
           default: assertUnreachable(resp)
         }
       }
-    } catch (error) {
-      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
-        )
-      }
-    }
+    })
     setBusy(undefined)
   }
 
   async function doCancel() {
-    try {
-      setBusy({})
+    setBusy({})
+    await withRuntimeErrorHandling(i18n, async () => {
       const resp = await 
api.abortWithdrawalById(withdrawUri.withdrawalOperationId);
       if (resp.type === "ok") {
         onAborted();
       } else {
         switch (resp.case) {
-          case "previously-confirmed": {
-            notify({
-              type: "error",
-              title: i18n.str`The reserve operation has been confirmed 
previously and can't be aborted`
-            });
-            break;
-          }
+          case "previously-confirmed": return notify({
+            type: "error",
+            title: i18n.str`The reserve operation has been confirmed 
previously and can't be aborted`
+          });
           default: {
             assertUnreachable(resp.case)
           }
         }
       }
-    } catch (error) {
-      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
-        )
-      }
-    }
+    })
     setBusy(undefined)
   }
 
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx 
b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 15910201e..7266e4de4 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -29,6 +29,7 @@ import { useWithdrawalDetails } from "../hooks/access.js";
 import { assertUnreachable } from "./HomePage.js";
 import { QrCodeSection } from "./QrCodeSection.js";
 import { WithdrawalConfirmationQuestion } from 
"./WithdrawalConfirmationQuestion.js";
+import { Attention } from "../components/Attention.js";
 
 const logger = new Logger("WithdrawalQRCode");
 
@@ -139,18 +140,21 @@ export function WithdrawalQRCode({
   }
 
   if (!data.selected_reserve_pub) {
-    return <div>
-      the exchange is selcted but no reserve pub
-    </div>
+    return <Attention type="danger"
+      title={i18n.str`The operation is incomplete or some step in the 
withdrawal failed`} >
+      <i18n.Translate>The exchange is selected but no reserve public key 
found.</i18n.Translate>
+    </Attention>
   }
 
   const account = !data.selected_exchange_account ? undefined : 
parsePaytoUri(data.selected_exchange_account)
 
   if (!account) {
-    return <div>
-      the exchange is selcted but no account
-    </div>
+    return <Attention type="danger"
+      title={i18n.str`The operation is incomplete or some step in the 
withdrawal failed`} >
+      <i18n.Translate>The exchange is selected but the exchange payto URI is 
missing or invalid.</i18n.Translate>
+    </Attention>
   }
+
   return (
     <WithdrawalConfirmationQuestion
       withdrawUri={withdrawUri}
diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx 
b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
index f6176e772..e10c3ad41 100644
--- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -2,7 +2,7 @@ import { HttpStatusCode, TalerCorebankApi, TalerError, 
TranslatedString } from "
 import { RequestError, notify, notifyError, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
-import { buildRequestErrorMessage } from "../../utils.js";
+import { buildRequestErrorMessage, withRuntimeErrorHandling } from 
"../../utils.js";
 import { getRandomPassword } from "../rnd.js";
 import { AccountForm } from "./AccountForm.js";
 import { useBackendState } from "../../hooks/backend.js";
@@ -29,7 +29,7 @@ export function CreateNewAccount({
 
   async function doCreate() {
     if (!submitAccount || !token) return;
-    try {
+    await withRuntimeErrorHandling(i18n, async () => {
       const account: TalerCorebankApi.RegisterAccountRequest = {
         cashout_payto_uri: submitAccount.cashout_payto_uri,
         challenge_contact_data: submitAccount.contact_data,
@@ -72,18 +72,7 @@ export function CreateNewAccount({
           default: assertUnreachable(resp)
         }
       }
-    } catch (error) {
-      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 (
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx 
b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index ce8a53ca1..9a212ebd0 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -7,7 +7,7 @@ import { ErrorLoading } from "../../components/ErrorLoading.js";
 import { Loading } from "../../components/Loading.js";
 import { ShowInputErrorLabel } from "../../components/ShowInputErrorLabel.js";
 import { useAccountDetails } from "../../hooks/access.js";
-import { buildRequestErrorMessage, undefinedIfEmpty } from "../../utils.js";
+import { buildRequestErrorMessage, undefinedIfEmpty, withRuntimeErrorHandling 
} from "../../utils.js";
 import { assertUnreachable } from "../HomePage.js";
 import { LoginForm } from "../LoginForm.js";
 import { doAutoFocus } from "../PaytoWireTransferForm.js";
@@ -60,7 +60,7 @@ export function RemoveAccount({
 
   async function doRemove() {
     if (!token) return;
-    try {
+    await withRuntimeErrorHandling(i18n, async () => {
       const resp = await api.deleteAccount({ username: account, token });
       if (resp.type === "ok") {
         onUpdateSuccess();
@@ -95,16 +95,7 @@ export function RemoveAccount({
           }
         }
       }
-    } catch (error) {
-      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);
-      }
-    }
+    })
   }
 
   const errors = undefinedIfEmpty({
diff --git a/packages/demobank-ui/src/pages/business/Home.tsx 
b/packages/demobank-ui/src/pages/business/Home.tsx
index 03d7895e3..d7beda01d 100644
--- a/packages/demobank-ui/src/pages/business/Home.tsx
+++ b/packages/demobank-ui/src/pages/business/Home.tsx
@@ -45,6 +45,7 @@ import {
   TanChannel,
   buildRequestErrorMessage,
   undefinedIfEmpty,
+  withRuntimeErrorHandling,
 } from "../../utils.js";
 import { LoginForm } from "../LoginForm.js";
 import { InputAmount } from "../PaytoWireTransferForm.js";
@@ -241,41 +242,15 @@ function CreateCashout({
   );
 
   useEffect(() => {
-    if (form.isDebit) {
-      calculateFromDebit(amount, sellFee, sellRate)
-        .then((r) => {
-          setCalc(r);
-        })
-        .catch((error) => {
-          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
-            )
-          }
-        });
-    } else {
-      calculateFromCredit(amount, sellFee, sellRate)
-        .then((r) => {
-          setCalc(r);
-        })
-        .catch((error) => {
-          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
-            )
-          }
-        });
+    async function doAsync() {
+      await withRuntimeErrorHandling(i18n, async () => {
+        const resp = await (form.isDebit ?
+          calculateFromDebit(amount, sellFee, sellRate) :
+          calculateFromCredit(amount, sellFee, sellRate));
+        setCalc(resp)
+      })
     }
+    doAsync()
   }, [form.amount, form.isDebit]);
 
   const balanceAfter = Amounts.sub(account.balance, calc.debit).amount;
@@ -484,7 +459,7 @@ function CreateCashout({
               e.preventDefault();
 
               if (errors || !creds) return;
-              try {
+              await withRuntimeErrorHandling(i18n, async () => {
                 const resp = await api.createCashout(creds, {
                   amount_credit: Amounts.stringify(calc.credit),
                   amount_debit: Amounts.stringify(calc.debit),
@@ -529,18 +504,7 @@ function CreateCashout({
                     default: assertUnreachable(resp)
                   }
                 }
-              } catch (error) {
-                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
-                  )
-                }
-              }
+              })
             }}
           >
             {i18n.str`Create`}
@@ -669,7 +633,7 @@ export function ShowCashoutDetails({
               onClick={async (e) => {
                 e.preventDefault();
                 if (!creds) return;
-                try {
+                await withRuntimeErrorHandling(i18n, async () => {
                   const resp = await api.abortCashoutById(creds, id);
                   if (resp.type === "ok") {
                     onCancel();
@@ -692,18 +656,7 @@ export function ShowCashoutDetails({
                       }
                     }
                   }
-                } catch (error) {
-                  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
-                    )
-                  }
-                }
+                })
               }}
             >
               {i18n.str`Abort`}
@@ -715,9 +668,8 @@ export function ShowCashoutDetails({
               class="pure-button pure-button-primary "
               onClick={async (e) => {
                 e.preventDefault();
-                if (!creds) return;
-                try {
-                  if (!code) return;
+                if (!creds || !code) return;
+                await withRuntimeErrorHandling(i18n, async () => {
                   const resp = await api.confirmCashoutById(creds, id, {
                     tan: code,
                   });
@@ -745,19 +697,8 @@ export function ShowCashoutDetails({
                       })
                       default: assertUnreachable(resp)
                     }
-                  }
-                } catch (error) {
-                  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
-                    )
-                  }
-                }
+                  }                  
+                })
               }}
             >
               {i18n.str`Confirm`}
diff --git a/packages/demobank-ui/src/utils.ts 
b/packages/demobank-ui/src/utils.ts
index 310e80cd6..437618150 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -19,6 +19,8 @@ import {
   ErrorNotification,
   ErrorType,
   HttpError,
+  notify,
+  notifyError,
   useTranslationContext,
 } from "@gnu-taler/web-util/browser";
 
@@ -92,10 +94,27 @@ export enum CashoutStatus {
 export const PAGE_SIZE = 20;
 export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1;
 
-export function buildRequestErrorMessage(
-  i18n: ReturnType<typeof useTranslationContext>["i18n"],
-  cause: TalerError<{}>,
-): ErrorNotification {
+type Translator = ReturnType<typeof useTranslationContext>["i18n"]
+
+export async function withRuntimeErrorHandling<T>(i18n: Translator, cb: () => 
Promise<T>): Promise<void> {
+  try {
+    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
+      )
+    }      
+  }
+}
+
+
+export function buildRequestErrorMessage( i18n: Translator, cause: 
TalerError): ErrorNotification {
   let result: ErrorNotification;
   switch (cause.errorDetail.code) {
     case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {

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