From 37f46f4d6b821d163c3e4db5c374b1120212ac74 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 11 Mar 2024 14:56:25 -0300 Subject: obs and cancel request, plus lint --- packages/bank-ui/src/Routing.tsx | 69 +- packages/bank-ui/src/app.tsx | 2 +- packages/bank-ui/src/components/Cashouts/views.tsx | 37 +- packages/bank-ui/src/components/Time.tsx | 51 +- .../bank-ui/src/components/Transactions/index.ts | 24 +- .../bank-ui/src/components/Transactions/state.ts | 62 +- .../bank-ui/src/components/Transactions/views.tsx | 50 +- packages/bank-ui/src/context/config.ts | 108 +- packages/bank-ui/src/hooks/account.ts | 30 +- packages/bank-ui/src/hooks/bank-state.ts | 9 +- packages/bank-ui/src/hooks/form.ts | 125 +- packages/bank-ui/src/hooks/preferences.ts | 3 +- packages/bank-ui/src/hooks/regional.ts | 58 +- packages/bank-ui/src/pages/AccountPage/index.ts | 12 +- packages/bank-ui/src/pages/AccountPage/views.tsx | 18 +- packages/bank-ui/src/pages/BankFrame.tsx | 166 ++- packages/bank-ui/src/pages/LoginForm.tsx | 60 +- packages/bank-ui/src/pages/OperationState/index.ts | 16 +- packages/bank-ui/src/pages/OperationState/state.ts | 6 +- .../bank-ui/src/pages/OperationState/views.tsx | 17 +- packages/bank-ui/src/pages/PaymentOptions.tsx | 31 +- .../bank-ui/src/pages/PaytoWireTransferForm.tsx | 304 ++-- packages/bank-ui/src/pages/ProfileNavigation.tsx | 8 +- packages/bank-ui/src/pages/PublicHistoriesPage.tsx | 11 +- packages/bank-ui/src/pages/QrCodeSection.tsx | 22 +- packages/bank-ui/src/pages/RegistrationPage.tsx | 39 +- packages/bank-ui/src/pages/ShowNotifications.tsx | 55 + packages/bank-ui/src/pages/SolveChallengePage.tsx | 43 +- packages/bank-ui/src/pages/WalletWithdrawForm.tsx | 62 +- packages/bank-ui/src/pages/WireTransfer.tsx | 10 +- .../src/pages/WithdrawalConfirmationQuestion.tsx | 34 +- .../bank-ui/src/pages/WithdrawalOperationPage.tsx | 2 +- .../src/pages/account/CashoutListForAccount.tsx | 5 +- .../src/pages/account/ShowAccountDetails.tsx | 10 +- .../src/pages/account/UpdateAccountPassword.tsx | 17 +- packages/bank-ui/src/pages/admin/AccountForm.tsx | 162 ++- packages/bank-ui/src/pages/admin/AccountList.tsx | 13 +- packages/bank-ui/src/pages/admin/AdminHome.tsx | 41 +- .../bank-ui/src/pages/admin/CreateNewAccount.tsx | 11 + packages/bank-ui/src/pages/admin/DownloadStats.tsx | 11 +- packages/bank-ui/src/pages/admin/RemoveAccount.tsx | 4 + .../src/pages/regional/ConversionConfig.tsx | 1464 +++++++++++--------- .../bank-ui/src/pages/regional/CreateCashout.tsx | 104 +- .../src/pages/regional/ShowCashoutDetails.tsx | 34 +- packages/bank-ui/src/route.ts | 43 +- packages/bank-ui/src/stories.test.ts | 7 +- packages/bank-ui/src/utils.ts | 82 +- 47 files changed, 2133 insertions(+), 1419 deletions(-) create mode 100644 packages/bank-ui/src/pages/ShowNotifications.tsx diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx index 75f070e4b..fbf5aa9ec 100644 --- a/packages/bank-ui/src/Routing.tsx +++ b/packages/bank-ui/src/Routing.tsx @@ -22,6 +22,7 @@ import { import { Fragment, VNode, h } from "preact"; import { + AbsoluteTime, AccessToken, HttpStatusCode, TranslatedString, @@ -30,13 +31,13 @@ import { import { useEffect } from "preact/hooks"; import { useBankCoreApiContext } from "./context/config.js"; import { useNavigationContext } from "./context/navigation.js"; -import { useSettingsContext } from "./context/settings.js"; import { useSessionState } from "./hooks/session.js"; import { AccountPage } from "./pages/AccountPage/index.js"; import { BankFrame } from "./pages/BankFrame.js"; import { LoginForm } from "./pages/LoginForm.js"; import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js"; import { RegistrationPage } from "./pages/RegistrationPage.js"; +import { ShowNotifications } from "./pages/ShowNotifications.js"; import { SolveChallengePage } from "./pages/SolveChallengePage.js"; import { WireTransfer } from "./pages/WireTransfer.js"; import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js"; @@ -58,7 +59,10 @@ export function Routing(): VNode { if (session.state.status === "loggedIn") { const { isUserAdministrator, username } = session.state; return ( - + ); @@ -90,7 +94,6 @@ function PublicRounting({ }: { onLoggedUser: (username: string, token: AccessToken) => void; }): VNode { - const settings = useSettingsContext(); const { i18n } = useTranslationContext(); const location = useCurrentLocation(publicPages); const { navigateTo } = useNavigationContext(); @@ -109,12 +112,11 @@ function PublicRounting({ async function doAutomaticLogin(username: string, password: string) { await handleError(async () => { - const resp = await authenticator(username) - .createAccessToken(password, { - scope: "readwrite", - duration: { d_us: "forever" }, - refreshable: true, - }); + const resp = await authenticator(username).createAccessToken(password, { + scope: "readwrite", + duration: { d_us: "forever" }, + refreshable: true, + }); if (resp.type === "ok") { onLoggedUser(username, resp.body.access_token); } else { @@ -125,6 +127,7 @@ function PublicRounting({ title: i18n.str`Wrong credentials for "${username}"`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); case HttpStatusCode.NotFound: return notify({ @@ -132,6 +135,7 @@ function PublicRounting({ title: i18n.str`Account not found`, description: resp.detail.hint as TranslatedString, debug: resp.detail, + when: AbsoluteTime.now(), }); default: assertUnreachable(resp); @@ -198,14 +202,12 @@ export const privatePages = { () => "#/account/charge-wallet", ), homeWireTransfer: urlPattern<{ - account?: string, - subject?: string, - amount?: string, - }>( - /\/account\/wire-transfer/, - () => "#/account/wire-transfer", - ), + account?: string; + subject?: string; + amount?: string; + }>(/\/account\/wire-transfer/, () => "#/account/wire-transfer"), home: urlPattern(/\/account/, () => "#/account"), + notifications: urlPattern(/\/notifications/, () => "#/notifications"), solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"), cashoutCreate: urlPattern(/\/new-cashout/, () => "#/new-cashout"), cashoutDetails: urlPattern<{ cid: string }>( @@ -213,9 +215,9 @@ export const privatePages = { ({ cid }) => `#/cashout/${cid}`, ), wireTranserCreate: urlPattern<{ - account?: string, - subject?: string, - amount?: string, + account?: string; + subject?: string; + amount?: string; }>( /\/wire-transfer\/(?[a-zA-Z0-9]+)/, ({ account }) => `#/wire-transfer/${account}`, @@ -278,7 +280,6 @@ function PrivateRouting({ switch (location.name) { case "operationDetails": { - return ( { - navigateTo(privatePages.home.url({})) - }} - />; + return ( + { + navigateTo(privatePages.home.url({})); + }} + /> + ); } case "homeWireTransfer": { return ( @@ -598,6 +600,9 @@ function PrivateRouting({ /> ); } + case "notifications": { + return ; + } default: assertUnreachable(location); } diff --git a/packages/bank-ui/src/app.tsx b/packages/bank-ui/src/app.tsx index 3a7fafccf..893942059 100644 --- a/packages/bank-ui/src/app.tsx +++ b/packages/bank-ui/src/app.tsx @@ -88,7 +88,7 @@ export function App() { ); -}; +} // @ts-expect-error creating a new property for window object window.setGlobalLogLevelFromString = setGlobalLogLevelFromString; diff --git a/packages/bank-ui/src/components/Cashouts/views.tsx b/packages/bank-ui/src/components/Cashouts/views.tsx index 7f16d5840..22b8d8c1b 100644 --- a/packages/bank-ui/src/components/Cashouts/views.tsx +++ b/packages/bank-ui/src/components/Cashouts/views.tsx @@ -17,7 +17,6 @@ import { AbsoluteTime, Amounts, - Duration, HttpStatusCode, TalerError, assertUnreachable, @@ -32,19 +31,19 @@ import { Fragment, VNode, h } from "preact"; import { useConversionInfo } from "../../hooks/regional.js"; import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js"; -import { State } from "./index.js"; import { Time } from "../Time.js"; +import { State } from "./index.js"; export function FailedView({ error }: State.Failed) { const { i18n } = useTranslationContext(); switch (error.case) { case HttpStatusCode.NotImplemented: { return ( - - Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + + + Cashout should be enable by configuration and the conversion rate + should be initialized with fee, ratio and rounding mode. + ); } @@ -69,11 +68,11 @@ export function ReadyView({ switch (resp.case) { case HttpStatusCode.NotImplemented: { return ( - - Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + + + Cashout should be enable by configuration and the conversion rate + should be initialized with fee, ratio and rounding mode. + ); } @@ -89,8 +88,8 @@ export function ReadyView({ cur.creation_time.t_s === "never" ? "" : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy", { - locale: dateLocale, - }); + locale: dateLocale, + }); if (!prev[d]) { prev[d] = []; } @@ -156,9 +155,12 @@ export function ReadyView({ >
-
{ @@ -200,7 +202,6 @@ export function ReadyView({ - {item.subject} diff --git a/packages/bank-ui/src/components/Time.tsx b/packages/bank-ui/src/components/Time.tsx index 39ce33f60..5c8afe212 100644 --- a/packages/bank-ui/src/components/Time.tsx +++ b/packages/bank-ui/src/components/Time.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -16,16 +16,21 @@ import { AbsoluteTime, Duration } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { formatISO, format, formatDuration, intervalToDuration } from "date-fns"; +import { + formatISO, + format, + formatDuration, + intervalToDuration, +} from "date-fns"; import { Fragment, h, VNode } from "preact"; /** - * + * * @param timestamp time to be formatted * @param relative duration threshold, if the difference is lower * the timestamp will be formatted as relative time from "now" - * - * @returns + * + * @returns */ export function Time({ timestamp, @@ -33,34 +38,38 @@ export function Time({ format: formatString, }: { timestamp: AbsoluteTime | undefined; - relative?: Duration, + relative?: Duration; format: string; }): VNode { - const { i18n, dateLocale } = useTranslationContext() - if (!timestamp) return + const { i18n, dateLocale } = useTranslationContext(); + if (!timestamp) return ; if (timestamp.t_ms === "never") { - return + return ; } const now = AbsoluteTime.now(); - const diff = AbsoluteTime.difference(now, timestamp) + const diff = AbsoluteTime.difference(now, timestamp); if (relative && now.t_ms !== "never" && Duration.cmp(diff, relative) === -1) { const d = intervalToDuration({ start: now.t_ms, - end: timestamp.t_ms - }) - d.seconds = 0 - const duration = formatDuration(d, { locale: dateLocale }) - const isFuture = AbsoluteTime.cmp(now, timestamp) < 0 + end: timestamp.t_ms, + }); + d.seconds = 0; + const duration = formatDuration(d, { locale: dateLocale }); + const isFuture = AbsoluteTime.cmp(now, timestamp) < 0; if (isFuture) { - return + return ( + + ); } else { - return + return ( + + ); } } return ( diff --git a/packages/bank-ui/src/components/Transactions/index.ts b/packages/bank-ui/src/components/Transactions/index.ts index c8bb1e108..4cad6f306 100644 --- a/packages/bank-ui/src/components/Transactions/index.ts +++ b/packages/bank-ui/src/components/Transactions/index.ts @@ -23,11 +23,13 @@ import { RouteDefinition } from "../../route.js"; export interface Props { account: string; - routeCreateWireTransfer: RouteDefinition<{ - account?: string, - subject?: string, - amount?: string, - }> | undefined; + routeCreateWireTransfer: + | RouteDefinition<{ + account?: string; + subject?: string; + amount?: string; + }> + | undefined; } export type State = State.Loading | State.LoadingUriError | State.Ready; @@ -49,11 +51,13 @@ export namespace State { export interface Ready extends BaseInfo { status: "ready"; error: undefined; - routeCreateWireTransfer: RouteDefinition<{ - account?: string, - subject?: string, - amount?: string, - }> | undefined; + routeCreateWireTransfer: + | RouteDefinition<{ + account?: string; + subject?: string; + amount?: string; + }> + | undefined; transactions: Transaction[]; onGoStart?: () => void; onGoNext?: () => void; diff --git a/packages/bank-ui/src/components/Transactions/state.ts b/packages/bank-ui/src/components/Transactions/state.ts index 3e9103b59..e792ddfa0 100644 --- a/packages/bank-ui/src/components/Transactions/state.ts +++ b/packages/bank-ui/src/components/Transactions/state.ts @@ -23,7 +23,10 @@ import { import { useTransactions } from "../../hooks/account.js"; import { Props, State, Transaction } from "./index.js"; -export function useComponentState({ account, routeCreateWireTransfer }: Props): State { +export function useComponentState({ + account, + routeCreateWireTransfer, +}: Props): State { const txResult = useTransactions(account); if (!txResult) { return { @@ -38,36 +41,35 @@ export function useComponentState({ account, routeCreateWireTransfer }: Props): }; } - const transactions = - txResult.result - .map((tx) => { - const negative = tx.direction === "debit"; - const cp = parsePaytoUri( - negative ? tx.creditor_payto_uri : tx.debtor_payto_uri, - ); - const counterpart = - (cp === undefined || !cp.isKnown - ? undefined - : cp.targetType === "iban" - ? cp.iban - : cp.targetType === "x-taler-bank" - ? cp.account - : cp.targetType === "bitcoin" - ? `${cp.targetPath.substring(0, 6)}...` - : undefined) ?? "unknown"; + const transactions = txResult.result + .map((tx) => { + const negative = tx.direction === "debit"; + const cp = parsePaytoUri( + negative ? tx.creditor_payto_uri : tx.debtor_payto_uri, + ); + const counterpart = + (cp === undefined || !cp.isKnown + ? undefined + : cp.targetType === "iban" + ? cp.iban + : cp.targetType === "x-taler-bank" + ? cp.account + : cp.targetType === "bitcoin" + ? `${cp.targetPath.substring(0, 6)}...` + : undefined) ?? "unknown"; - const when = AbsoluteTime.fromProtocolTimestamp(tx.date); - const amount = Amounts.parse(tx.amount); - const subject = tx.subject; - return { - negative, - counterpart, - when, - amount, - subject, - }; - }) - .filter((x): x is Transaction => x !== undefined); + const when = AbsoluteTime.fromProtocolTimestamp(tx.date); + const amount = Amounts.parse(tx.amount); + const subject = tx.subject; + return { + negative, + counterpart, + when, + amount, + subject, + }; + }) + .filter((x): x is Transaction => x !== undefined); return { status: "ready", diff --git a/packages/bank-ui/src/components/Transactions/views.tsx b/packages/bank-ui/src/components/Transactions/views.tsx index 7da9fc5a9..417b34c71 100644 --- a/packages/bank-ui/src/components/Transactions/views.tsx +++ b/packages/bank-ui/src/components/Transactions/views.tsx @@ -19,9 +19,8 @@ import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useBankCoreApiContext } from "../../context/config.js"; import { RenderAmount } from "../../pages/PaytoWireTransferForm.js"; -import { State } from "./index.js"; -import { Duration } from "@gnu-taler/taler-util"; import { Time } from "../Time.js"; +import { State } from "./index.js"; export function ReadyView({ transactions, @@ -30,24 +29,26 @@ export function ReadyView({ onGoStart, }: State.Ready): VNode { const { i18n, dateLocale } = useTranslationContext(); - const { config } = useBankCoreApiContext() + const { config } = useBankCoreApiContext(); if (!transactions.length) { - return
-
-
-

- Transactions history -

+ return ( +
+
+
+

+ Transactions history +

+
-
- - - You can start sending a wire transfer or withdrawing to your wallet. - - -
; + + + You can start sending a wire transfer or withdrawing to your wallet. + + +
+ ); } const txByDate = transactions.reduce( @@ -116,9 +117,10 @@ export function ReadyView({ >
-
@@ -153,7 +155,9 @@ export function ReadyView({
{item.negative ? i18n.str`to` : i18n.str`from`}{" "} - {!routeCreateWireTransfer ? item.counterpart : + {!routeCreateWireTransfer ? ( + item.counterpart + ) : ( {item.counterpart} - } + )}
@@ -190,7 +194,9 @@ export function ReadyView({
                           )}
                         
                         
-                          {!routeCreateWireTransfer ? item.counterpart :
+                          {!routeCreateWireTransfer ? (
+                            item.counterpart
+                          ) : (
                             
                               {item.counterpart}
                             
-                          }
+                          )}
                         
                         
                           {item.subject}
diff --git a/packages/bank-ui/src/context/config.ts b/packages/bank-ui/src/context/config.ts
index cb0d599aa..f8be80a6c 100644
--- a/packages/bank-ui/src/context/config.ts
+++ b/packages/bank-ui/src/context/config.ts
@@ -26,11 +26,12 @@ import {
   TalerError,
   assertUnreachable,
   CacheEvictor,
+  ObservabilityEvent,
 } from "@gnu-taler/taler-util";
 import {
   BrowserFetchHttpLib,
   ErrorLoading,
-  useTranslationContext
+  useTranslationContext,
 } from "@gnu-taler/web-util/browser";
 import {
   ComponentChildren,
@@ -63,6 +64,8 @@ export type Type = {
   conversion: TalerBankConversionHttpClient;
   authenticator: (user: string) => TalerAuthenticationHttpClient;
   hints: VersionHint[];
+  onBackendActivity: (fn: Listener) => Unsuscriber;
+  cancelRequest: (eventId: string) => void;
 };
 
 // FIXME: below
@@ -78,6 +81,25 @@ export enum VersionHint {
   CASHOUT_BEFORE_2FA,
 }
 
+const observers = new Array<(e: ObservabilityEvent) => void>();
+type Listener = (e: ObservabilityEvent) => void;
+type Unsuscriber = () => void;
+
+const activity = Object.freeze({
+  notify: (data: ObservabilityEvent) =>
+    observers.forEach((observer) => observer(data)),
+  subscribe: (func: Listener): Unsuscriber => {
+    observers.push(func);
+    return () => {
+      observers.forEach((observer, index) => {
+        if (observer === func) {
+          observers.splice(index, 1);
+        }
+      });
+    };
+  },
+});
+
 export type ConfigResult =
   | undefined
   | { type: "ok"; config: TalerCorebankApi.Config; hints: VersionHint[] }
@@ -96,7 +118,8 @@ export const BankCoreApiProvider = ({
   const [checked, setChecked] = useState();
   const { i18n } = useTranslationContext();
 
-  const { bankClient, conversionClient, authClient } = buildApiClient(new URL(baseUrl))
+  const { bankClient, conversionClient, authClient, cancelRequest } =
+    buildApiClient(new URL(baseUrl));
 
   useEffect(() => {
     bankClient
@@ -150,8 +173,10 @@ export const BankCoreApiProvider = ({
     url: new URL(bankClient.baseUrl),
     config: checked.config,
     bank: bankClient,
+    onBackendActivity: activity.subscribe,
     conversion: conversionClient,
     authenticator: authClient,
+    cancelRequest,
     hints: checked.hints,
   };
   return h(Context.Provider, {
@@ -162,8 +187,8 @@ export const BankCoreApiProvider = ({
 
 /**
  * build http client with cache breaker due to SWR
- * @param url 
- * @returns 
+ * @param url
+ * @returns
  */
 function buildApiClient(url: URL) {
   const httpFetch = new BrowserFetchHttpLib({
@@ -172,15 +197,32 @@ function buildApiClient(url: URL) {
   });
   const httpLib = new ObservableHttpClientLibrary(httpFetch, {
     observe(ev) {
-      console.log(ev)
-    }
-  })
+      activity.notify(ev);
+    },
+  });
 
-  const bankClient = new TalerCoreBankHttpClient(url.href, httpLib, evictBankSwrCache);
-  const conversionClient = new TalerBankConversionHttpClient(bankClient.getConversionInfoAPI().href, httpLib, evictConversionSwrCache);
-  const authClient = (user: string) => new TalerAuthenticationHttpClient(bankClient.getAuthenticationAPI(user).href, user, httpLib);
+  function cancelRequest(id: string) {
+    httpLib.cancelRequest(id);
+  }
 
-  return { bankClient, conversionClient, authClient }
+  const bankClient = new TalerCoreBankHttpClient(
+    url.href,
+    httpLib,
+    evictBankSwrCache,
+  );
+  const conversionClient = new TalerBankConversionHttpClient(
+    bankClient.getConversionInfoAPI().href,
+    httpLib,
+    evictConversionSwrCache,
+  );
+  const authClient = (user: string) =>
+    new TalerAuthenticationHttpClient(
+      bankClient.getAuthenticationAPI(user).href,
+      user,
+      httpLib,
+    );
+
+  return { bankClient, conversionClient, authClient, cancelRequest };
 }
 
 export const BankCoreApiProviderTesting = ({
@@ -206,7 +248,6 @@ export const BankCoreApiProviderTesting = ({
   });
 };
 
-
 const evictBankSwrCache: CacheEvictor = {
   async notifySuccess(op) {
     switch (op) {
@@ -215,7 +256,7 @@ const evictBankSwrCache: CacheEvictor = {
           revalidatePublicAccounts(),
           revalidateBusinessAccounts(),
         ]);
-        return
+        return;
       }
       case TalerCoreBankCacheEviction.CREATE_ACCOUNT: {
         // admin balance change on new account
@@ -224,27 +265,25 @@ const evictBankSwrCache: CacheEvictor = {
           revalidateTransactions(),
           revalidatePublicAccounts(),
           revalidateBusinessAccounts(),
-        ])
+        ]);
         return;
       }
       case TalerCoreBankCacheEviction.UPDATE_ACCOUNT: {
-        await Promise.all([
-          revalidateAccountDetails(),
-        ])
+        await Promise.all([revalidateAccountDetails()]);
         return;
       }
       case TalerCoreBankCacheEviction.CREATE_TRANSACTION: {
         await Promise.all([
           revalidateAccountDetails(),
           revalidateTransactions(),
-        ])
+        ]);
         return;
       }
       case TalerCoreBankCacheEviction.CONFIRM_WITHDRAWAL: {
         await Promise.all([
           revalidateAccountDetails(),
           revalidateTransactions(),
-        ])
+        ]);
         return;
       }
       case TalerCoreBankCacheEviction.CREATE_CASHOUT: {
@@ -252,7 +291,7 @@ const evictBankSwrCache: CacheEvictor = {
           revalidateAccountDetails(),
           revalidateCashouts(),
           revalidateTransactions(),
-        ])
+        ]);
         return;
       }
       case TalerCoreBankCacheEviction.UPDATE_PASSWORD:
@@ -260,20 +299,21 @@ const evictBankSwrCache: CacheEvictor = {
       case TalerCoreBankCacheEviction.CREATE_WITHDRAWAL:
         return;
       default:
-        assertUnreachable(op)
+        assertUnreachable(op);
     }
-  }
-}
+  },
+};
 
-const evictConversionSwrCache: CacheEvictor = {
-  async notifySuccess(op) {
-    switch (op) {
-      case TalerBankConversionCacheEviction.UPDATE_RATE: {
-        await revalidateConversionInfo();
-        return
+const evictConversionSwrCache: CacheEvictor =
+  {
+    async notifySuccess(op) {
+      switch (op) {
+        case TalerBankConversionCacheEviction.UPDATE_RATE: {
+          await revalidateConversionInfo();
+          return;
+        }
+        default:
+          assertUnreachable(op);
       }
-      default:
-        assertUnreachable(op)
-    }
-  }
-}
\ No newline at end of file
+    },
+  };
diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts
index aa0745253..5fe12573c 100644
--- a/packages/bank-ui/src/hooks/account.ts
+++ b/packages/bank-ui/src/hooks/account.ts
@@ -62,7 +62,11 @@ export function useAccountDetails(account: string) {
 }
 
 export function revalidateWithdrawalDetails() {
-  return mutate((key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById", undefined, { revalidate: true });
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById",
+    undefined,
+    { revalidate: true },
+  );
 }
 
 export function useWithdrawalDetails(wid: string) {
@@ -110,7 +114,9 @@ export function useWithdrawalDetails(wid: string) {
 
 export function revalidateTransactionDetails() {
   return mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === "getTransactionById", undefined, { revalidate: true }
+    (key) => Array.isArray(key) && key[key.length - 1] === "getTransactionById",
+    undefined,
+    { revalidate: true },
   );
 }
 export function useTransactionDetails(account: string, tid: number) {
@@ -149,7 +155,9 @@ export function useTransactionDetails(account: string, tid: number) {
 
 export async function revalidatePublicAccounts() {
   return mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts", undefined, { revalidate: true }
+    (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts",
+    undefined,
+    { revalidate: true },
   );
 }
 export function usePublicAccounts(
@@ -193,9 +201,10 @@ export function usePublicAccounts(
     data && data.type === "ok" && data.body.public_accounts.length <= PAGE_SIZE;
   const isFirstPage = !offset;
 
-  const result = data && data.type == "ok" ? structuredClone(data.body.public_accounts) : []
-  if (result.length == PAGE_SIZE+1) {
-    result.pop()
+  const result =
+    data && data.type == "ok" ? structuredClone(data.body.public_accounts) : [];
+  if (result.length == PAGE_SIZE + 1) {
+    result.pop();
   }
   const pagination = {
     result,
@@ -243,7 +252,7 @@ export function useTransactions(account: string, initial?: number) {
     return await api.getTransactions(
       { username, token },
       {
-        limit: PAGE_SIZE +1 ,
+        limit: PAGE_SIZE + 1,
         offset: txid ? String(txid) : undefined,
         order: "dec",
       },
@@ -267,9 +276,10 @@ export function useTransactions(account: string, initial?: number) {
     data && data.type === "ok" && data.body.transactions.length <= PAGE_SIZE;
   const isFirstPage = !offset;
 
-  const result = data && data.type == "ok" ? structuredClone(data.body.transactions) : []
-  if (result.length == PAGE_SIZE+1) {
-    result.pop()
+  const result =
+    data && data.type == "ok" ? structuredClone(data.body.transactions) : [];
+  if (result.length == PAGE_SIZE + 1) {
+    result.pop();
   }
   const pagination = {
     result,
diff --git a/packages/bank-ui/src/hooks/bank-state.ts b/packages/bank-ui/src/hooks/bank-state.ts
index 83bb009cf..1d8c4f9e6 100644
--- a/packages/bank-ui/src/hooks/bank-state.ts
+++ b/packages/bank-ui/src/hooks/bank-state.ts
@@ -118,7 +118,7 @@ const codecForChallengeConfirmWithdrawal =
       .property("request", codecForString())
       .build("ConfirmWithdrawalChallenge");
 
-const codecForAppLocation = codecForString as () => Codec
+const codecForAppLocation = codecForString as () => Codec;
 
 const codecForChallengeCashout = (): Codec =>
   buildCodecForObject()
@@ -141,8 +141,6 @@ const codecForChallenge = (): Codec =>
     .alternative("update-password", codecForChallengeUpdatePassword())
     .build("ChallengeInProgess");
 
-
-    
 interface BankState {
   currentWithdrawalOperationId: string | undefined;
   currentChallenge: ChallengeInProgess | undefined;
@@ -163,10 +161,10 @@ const BANK_STATE_KEY = buildStorageKey("bank-app-state", codecForBankState());
 
 /**
  * Client state saved in local storage.
- * 
+ *
  * This information is saved in the client because
  * the backend server session API is not enough.
- * 
+ *
  * @returns tuple of [state, update(), reset()]
  */
 export function useBankState(): [
@@ -185,4 +183,3 @@ export function useBankState(): [
   }
   return [value, updateField, reset];
 }
-
diff --git a/packages/bank-ui/src/hooks/form.ts b/packages/bank-ui/src/hooks/form.ts
index 26354b108..afa4912eb 100644
--- a/packages/bank-ui/src/hooks/form.ts
+++ b/packages/bank-ui/src/hooks/form.ts
@@ -14,87 +14,102 @@
  GNU Taler; see the file COPYING.  If not, see 
  */
 
-import { AmountJson, TalerBankConversionApi, TranslatedString } from "@gnu-taler/taler-util";
+import { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
 import { useState } from "preact/hooks";
 
 export type UIField = {
   value: string | undefined;
   onUpdate: (s: string) => void;
   error: TranslatedString | undefined;
-}
+};
 
 type FormHandler = {
-  [k in keyof T]?:
-  T[k] extends string ? UIField :
-  T[k] extends AmountJson ? UIField :
-  FormHandler;
-}
+  [k in keyof T]?: T[k] extends string
+    ? UIField
+    : T[k] extends AmountJson
+      ? UIField
+      : FormHandler;
+};
 
 export type FormValues = {
-  [k in keyof T]:
-  T[k] extends string ? (string | undefined) :
-  T[k] extends AmountJson ? (string | undefined) :
-  FormValues;
-}
+  [k in keyof T]: T[k] extends string
+    ? string | undefined
+    : T[k] extends AmountJson
+      ? string | undefined
+      : FormValues;
+};
 
 export type RecursivePartial = {
-  [k in keyof T]?:
-  T[k] extends string ? (string) :
-  T[k] extends AmountJson ? (AmountJson) :
-  RecursivePartial;
-}
+  [k in keyof T]?: T[k] extends string
+    ? string
+    : T[k] extends AmountJson
+      ? AmountJson
+      : RecursivePartial;
+};
 
 export type FormErrors = {
-  [k in keyof T]?:
-  T[k] extends string ? (TranslatedString) :
-  T[k] extends AmountJson ? (TranslatedString) :
-  FormErrors;
-}
-
-export type FormStatus = {
-  status: "ok",
-  result: T,
-  errors: undefined,
-} | {
-  status: "fail",
-  result: RecursivePartial,
-  errors: FormErrors,
-}
-
-
-function constructFormHandler(form: FormValues, updateForm: (d: FormValues) => void, errors: FormErrors | undefined): FormHandler {
-  const keys = (Object.keys(form) as Array)
+  [k in keyof T]?: T[k] extends string
+    ? TranslatedString
+    : T[k] extends AmountJson
+      ? TranslatedString
+      : FormErrors;
+};
+
+export type FormStatus =
+  | {
+      status: "ok";
+      result: T;
+      errors: undefined;
+    }
+  | {
+      status: "fail";
+      result: RecursivePartial;
+      errors: FormErrors;
+    };
+
+function constructFormHandler(
+  form: FormValues,
+  updateForm: (d: FormValues) => void,
+  errors: FormErrors | undefined,
+): FormHandler {
+  const keys = Object.keys(form) as Array;
 
   const handler = keys.reduce((prev, fieldName) => {
-    const currentValue: any = form[fieldName];
-    const currentError: any = errors ? errors[fieldName] : undefined;
-    function updater(newValue: any) {
-      updateForm({ ...form, [fieldName]: newValue })
+    const currentValue: unknown = form[fieldName];
+    const currentError: unknown = errors ? errors[fieldName] : undefined;
+    function updater(newValue: unknown) {
+      updateForm({ ...form, [fieldName]: newValue });
     }
     if (typeof currentValue === "object") {
-      const group = constructFormHandler(currentValue, updater, currentError)
-      // @ts-expect-error asdasd
-      prev[fieldName] = group
+      // @ts-expect-error FIXME better typing
+      const group = constructFormHandler(currentValue, updater, currentError);
+      // @ts-expect-error FIXME better typing
+      prev[fieldName] = group;
       return prev;
     }
     const field: UIField = {
+      // @ts-expect-error FIXME better typing
       error: currentError,
+      // @ts-expect-error FIXME better typing
       value: currentValue,
-      onUpdate: updater
-    }
-    // @ts-expect-error asdasd
-    prev[fieldName] = field
-    return prev
-  }, {} as FormHandler)
+      onUpdate: updater,
+    };
+    // @ts-expect-error FIXME better typing
+    prev[fieldName] = field;
+    return prev;
+  }, {} as FormHandler);
 
   return handler;
 }
 
-export function useFormState(defaultValue: FormValues, check: (f: FormValues) => FormStatus): [FormHandler, FormStatus] {
-  const [form, updateForm] = useState>(defaultValue)
+export function useFormState(
+  defaultValue: FormValues,
+  check: (f: FormValues) => FormStatus,
+): [FormHandler, FormStatus] {
+  const [form, updateForm] = useState>(defaultValue);
 
-  const status = check(form)
-  const handler = constructFormHandler(form, updateForm, status.errors)
+  const status = check(form);
+  const handler = constructFormHandler(form, updateForm, status.errors);
 
-  return [handler, status]
-}
\ No newline at end of file
+  return [handler, status];
+}
diff --git a/packages/bank-ui/src/hooks/preferences.ts b/packages/bank-ui/src/hooks/preferences.ts
index 454dc8d80..bb3dcb153 100644
--- a/packages/bank-ui/src/hooks/preferences.ts
+++ b/packages/bank-ui/src/hooks/preferences.ts
@@ -61,7 +61,7 @@ const BANK_PREFERENCES_KEY = buildStorageKey(
 );
 /**
  * User preferences.
- * 
+ *
  * @returns tuple of [state, update()]
  */
 export function usePreferences(): [
@@ -109,4 +109,3 @@ export function getLabelForPreferences(
       return i18n.str`Show debug info`;
   }
 }
-
diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts
index bf948d293..51f3edad4 100644
--- a/packages/bank-ui/src/hooks/regional.ts
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -31,18 +31,20 @@ import {
   TalerHttpError,
   opFixedSuccess,
 } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
 import _useSWR, { SWRHook, mutate } from "swr";
 import { useBankCoreApiContext } from "../context/config.js";
-import { useState } from "preact/hooks";
 
 // FIX default import https://github.com/microsoft/TypeScript/issues/49189
 const useSWR = _useSWR as unknown as SWRHook;
 
-export type TransferCalculation = {
-  debit: AmountJson;
-  credit: AmountJson;
-  beforeFee: AmountJson;
-} | "amount-is-too-small";
+export type TransferCalculation =
+  | {
+      debit: AmountJson;
+      credit: AmountJson;
+      beforeFee: AmountJson;
+    }
+  | "amount-is-too-small";
 type EstimatorFunction = (
   amount: AmountJson,
   fee: AmountJson,
@@ -95,7 +97,7 @@ export function useCashinEstimator(): ConversionEstimators {
       if (resp.type === "fail") {
         switch (resp.case) {
           case HttpStatusCode.Conflict: {
-            return "amount-is-too-small"
+            return "amount-is-too-small";
           }
           // this below can't happen
           case HttpStatusCode.NotImplemented: //it should not be able to call this function
@@ -120,7 +122,7 @@ export function useCashinEstimator(): ConversionEstimators {
       if (resp.type === "fail") {
         switch (resp.case) {
           case HttpStatusCode.Conflict: {
-            return "amount-is-too-small"
+            return "amount-is-too-small";
           }
           // this below can't happen
           case HttpStatusCode.NotImplemented: //it should not be able to call this function
@@ -142,7 +144,7 @@ export function useCashinEstimator(): ConversionEstimators {
 }
 
 export function useCashoutEstimator(): ConversionEstimators {
-  const { bank, conversion } = useBankCoreApiContext();
+  const { conversion } = useBankCoreApiContext();
   return {
     estimateByCredit: async (fiatAmount, fee) => {
       const resp = await conversion.getCashoutRate({
@@ -151,7 +153,7 @@ export function useCashoutEstimator(): ConversionEstimators {
       if (resp.type === "fail") {
         switch (resp.case) {
           case HttpStatusCode.Conflict: {
-            return "amount-is-too-small"
+            return "amount-is-too-small";
           }
           // this below can't happen
           case HttpStatusCode.NotImplemented: //it should not be able to call this function
@@ -176,7 +178,7 @@ export function useCashoutEstimator(): ConversionEstimators {
       if (resp.type === "fail") {
         switch (resp.case) {
           case HttpStatusCode.Conflict: {
-            return "amount-is-too-small"
+            return "amount-is-too-small";
           }
           // this below can't happen
           case HttpStatusCode.NotImplemented: //it should not be able to call this function
@@ -201,11 +203,15 @@ export function useCashoutEstimator(): ConversionEstimators {
  * @deprecated use useCashoutEstimator
  */
 export function useEstimator(): ConversionEstimators {
-  return useCashoutEstimator()
+  return useCashoutEstimator();
 }
 
 export async function revalidateBusinessAccounts() {
-  return mutate((key) => Array.isArray(key) && key[key.length - 1] === "getAccounts", undefined, { revalidate: true });
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === "getAccounts",
+    undefined,
+    { revalidate: true },
+  );
 }
 export function useBusinessAccounts() {
   const { state: credentials } = useSessionState();
@@ -247,9 +253,10 @@ export function useBusinessAccounts() {
     data && data.type === "ok" && data.body.accounts.length <= PAGE_SIZE;
   const isFirstPage = !offset;
 
-  const result = data && data.type == "ok" ? structuredClone(data.body.accounts) : []
+  const result =
+    data && data.type == "ok" ? structuredClone(data.body.accounts) : [];
   if (result.length == PAGE_SIZE + 1) {
-    result.pop()
+    result.pop();
   }
   const pagination = {
     result,
@@ -276,7 +283,9 @@ function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
 export function revalidateOnePendingCashouts() {
   return mutate(
     (key) =>
-      Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts", undefined, { revalidate: true }
+      Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts",
+    undefined,
+    { revalidate: true },
   );
 }
 export function useOnePendingCashouts(account: string) {
@@ -290,7 +299,8 @@ export function useOnePendingCashouts(account: string) {
     if (list.type !== "ok") {
       return list;
     }
-    const pendingCashout = list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined;
+    const pendingCashout =
+      list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined;
     if (!pendingCashout) return opFixedSuccess(undefined);
     const cashoutInfo = await api.getCashoutById(
       { username, token },
@@ -334,7 +344,9 @@ export function useOnePendingCashouts(account: string) {
 }
 
 export function revalidateCashouts() {
-  return mutate((key) => Array.isArray(key) && key[key.length - 1] === "useCashouts");
+  return mutate(
+    (key) => Array.isArray(key) && key[key.length - 1] === "useCashouts",
+  );
 }
 export function useCashouts(account: string) {
   const { state: credentials } = useSessionState();
@@ -357,7 +369,7 @@ export function useCashouts(account: string) {
       }),
     );
     const cashouts = all.filter(notUndefined);
-    return { type: "ok" as const, body: { cashouts }};
+    return { type: "ok" as const, body: { cashouts } };
   }
   const { data, error } = useSWR<
     | OperationOk<{ cashouts: CashoutWithId[] }>
@@ -386,7 +398,9 @@ export function useCashouts(account: string) {
 
 export function revalidateCashoutDetails() {
   return mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById", undefined, { revalidate: true }
+    (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById",
+    undefined,
+    { revalidate: true },
   );
 }
 export function useCashoutDetails(cashoutId: number | undefined) {
@@ -435,7 +449,9 @@ export type LastMonitor = {
 };
 export function revalidateLastMonitorInfo() {
   return mutate(
-    (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo", undefined, { revalidate: true }
+    (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo",
+    undefined,
+    { revalidate: true },
   );
 }
 export function useLastMonitorInfo(
diff --git a/packages/bank-ui/src/pages/AccountPage/index.ts b/packages/bank-ui/src/pages/AccountPage/index.ts
index 7776fbaa3..757346c5c 100644
--- a/packages/bank-ui/src/pages/AccountPage/index.ts
+++ b/packages/bank-ui/src/pages/AccountPage/index.ts
@@ -88,14 +88,14 @@ export namespace State {
     routeChargeWallet: RouteDefinition;
     routePublicAccounts: RouteDefinition;
     routeWireTransfer: RouteDefinition<{
-      account?: string,
-      subject?: string,
-      amount?: string,
+      account?: string;
+      subject?: string;
+      amount?: string;
     }>;
     routeCreateWireTransfer: RouteDefinition<{
-      account?: string,
-      subject?: string,
-      amount?: string,
+      account?: string;
+      subject?: string;
+      amount?: string;
     }>;
     routeOperationDetails: RouteDefinition<{ wopid: string }>;
     routeSolveSecondFactor: RouteDefinition;
diff --git a/packages/bank-ui/src/pages/AccountPage/views.tsx b/packages/bank-ui/src/pages/AccountPage/views.tsx
index 7ad00cf1d..3a182ed1b 100644
--- a/packages/bank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/bank-ui/src/pages/AccountPage/views.tsx
@@ -32,7 +32,9 @@ export function InvalidIbanView({ error }: State.InvalidIban) {
 
 const IS_PUBLIC_ACCOUNT_ENABLED = false;
 
-function ShowDemoInfo({ routePublicAccounts }: {
+function ShowDemoInfo({
+  routePublicAccounts,
+}: {
   routePublicAccounts: RouteDefinition;
 }): VNode {
   const { i18n } = useTranslationContext();
@@ -50,7 +52,10 @@ function ShowDemoInfo({ routePublicAccounts }: {
           This part of the demo shows how a bank that supports Taler directly
           would work. In addition to using your own bank account, you can also
           see the transaction history of some{" "}
-          Public Accounts.
+          
+            Public Accounts
+          
+          .
         
       ) : (
         
@@ -62,7 +67,9 @@ function ShowDemoInfo({ routePublicAccounts }: {
   );
 }
 
-function ShowPedingOperation({ routeSolveSecondFactor }: {
+function ShowPedingOperation({
+  routeSolveSecondFactor,
+}: {
   routeSolveSecondFactor: RouteDefinition;
 }): VNode {
   const { i18n } = useTranslationContext();
@@ -140,7 +147,10 @@ export function ReadyView({
         onOperationCreated={onOperationCreated}
         onAuthorizationRequired={onAuthorizationRequired}
       />
-      
+      
     
   );
 }
diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx
index 427e9a156..39f042455 100644
--- a/packages/bank-ui/src/pages/BankFrame.tsx
+++ b/packages/bank-ui/src/pages/BankFrame.tsx
@@ -14,7 +14,14 @@
  GNU Taler; see the file COPYING.  If not, see 
  */
 
-import { Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AbsoluteTime,
+  Amounts,
+  ObservabilityEventType,
+  TalerError,
+  TranslatedString,
+  assertUnreachable,
+} from "@gnu-taler/taler-util";
 import {
   Footer,
   Header,
@@ -22,22 +29,23 @@ import {
   ToastBanner,
   notifyError,
   notifyException,
-  useTranslationContext
+  useTranslationContext,
 } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, VNode, h } from "preact";
-import { useEffect, useErrorBoundary } from "preact/hooks";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
 import { useBankCoreApiContext } from "../context/config.js";
 import { useSettingsContext } from "../context/settings.js";
 import { useAccountDetails } from "../hooks/account.js";
-import { useSessionState } from "../hooks/session.js";
 import { useBankState } from "../hooks/bank-state.js";
 import {
   getAllBooleanPreferences,
   getLabelForPreferences,
   usePreferences,
 } from "../hooks/preferences.js";
+import { useSessionState } from "../hooks/session.js";
 import { RouteDefinition } from "../route.js";
 import { RenderAmount } from "./PaytoWireTransferForm.js";
+import { privatePages } from "../Routing.js";
 
 const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
 const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -85,13 +93,18 @@ export function BankFrame({
           title="Bank"
           iconLinkURL={settings.iconLinkURL ?? "#"}
           profileURL={routeAccountDetails?.url({})}
+          notificationURL={
+            preferences.showDebugInfo
+              ? privatePages.notifications.url({})
+              : undefined
+          }
           onLogout={
             session.state.status !== "loggedIn"
               ? undefined
               : () => {
-                session.logOut();
-                resetBankState();
-              }
+                  session.logOut();
+                  resetBankState();
+                }
           }
           sites={
             !settings.topNavSites ? [] : Object.entries(settings.topNavSites)
@@ -102,11 +115,11 @@ export function BankFrame({
             
Preferences
-
diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx index bce7afe11..10b6afdf9 100644 --- a/packages/bank-ui/src/pages/admin/AccountForm.tsx +++ b/packages/bank-ui/src/pages/admin/AccountForm.tsx @@ -18,14 +18,12 @@ import { Amounts, PaytoString, TalerCorebankApi, - TranslatedString, assertUnreachable, buildPayto, parsePaytoUri, stringifyPaytoUri, } from "@gnu-taler/taler-util"; import { - Attention, CopyButton, ShowInputErrorLabel, useTranslationContext, @@ -41,7 +39,11 @@ import { validateIBAN, validateTalerBank, } from "../../utils.js"; -import { InputAmount, TextField, doAutoFocus } from "../PaytoWireTransferForm.js"; +import { + InputAmount, + TextField, + doAutoFocus, +} from "../PaytoWireTransferForm.js"; import { getRandomPassword } from "../rnd.js"; const EMAIL_REGEX = @@ -99,7 +101,10 @@ export function AccountForm({ ErrorMessageMappingFor | undefined >(undefined); - const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as const : "iban" as const; + const paytoType = + config.wire_type === "X_TALER_BANK" + ? ("x-taler-bank" as const) + : ("iban" as const); const cashoutPaytoType: typeof paytoType = "iban" as const; const defaultValue: AccountFormData = { @@ -110,8 +115,10 @@ export function AccountForm({ isPublic: template?.is_public, name: template?.name ?? "", cashout_payto_uri: - getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ?? ("" as PaytoString), - payto_uri: getAccountId(paytoType, template?.payto_uri) ?? ("" as PaytoString), + getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ?? + ("" as PaytoString), + payto_uri: + getAccountId(paytoType, template?.payto_uri) ?? ("" as PaytoString), email: template?.contact_data?.email ?? "", phone: template?.contact_data?.phone ?? "", username: username ?? "", @@ -130,9 +137,9 @@ export function AccountForm({ const isCashoutEnabled = config.allow_conversion; const editableCashout = - (purpose === "create" || - (purpose === "update" && - (config.allow_edit_cashout_payto_uri || userIsAdmin))); + purpose === "create" || + (purpose === "update" && + (config.allow_edit_cashout_payto_uri || userIsAdmin)); const editableThreshold = userIsAdmin && (purpose === "create" || purpose === "update"); const editableAccount = purpose === "create" && userIsAdmin; @@ -141,7 +148,6 @@ export function AccountForm({ const hasEmail = !!defaultValue.email || !!form.email; function updateForm(newForm: typeof defaultValue): void { - const trimmedAmountStr = newForm.debit_threshold?.trim(); const parsedAmount = Amounts.parse( `${config.currency}:${trimmedAmountStr}`, @@ -154,19 +160,25 @@ export function AccountForm({ ? undefined : !editableCashout ? undefined - : !newForm.cashout_payto_uri ? undefined - : cashoutPaytoType === "iban" ? validateIBAN(newForm.cashout_payto_uri, i18n) : - cashoutPaytoType === "x-taler-bank" ? validateTalerBank(newForm.cashout_payto_uri, i18n) : - undefined, + : !newForm.cashout_payto_uri + ? undefined + : cashoutPaytoType === "iban" + ? validateIBAN(newForm.cashout_payto_uri, i18n) + : cashoutPaytoType === "x-taler-bank" + ? validateTalerBank(newForm.cashout_payto_uri, i18n) + : undefined, payto_uri: !newForm.payto_uri ? undefined : !editableAccount ? undefined - : !newForm.payto_uri ? undefined - : paytoType === "iban" ? validateIBAN(newForm.payto_uri, i18n) : - paytoType === "x-taler-bank" ? validateTalerBank(newForm.payto_uri, i18n) : - undefined, + : !newForm.payto_uri + ? undefined + : paytoType === "iban" + ? validateIBAN(newForm.payto_uri, i18n) + : paytoType === "x-taler-bank" + ? validateTalerBank(newForm.payto_uri, i18n) + : undefined, email: !newForm.email ? undefined @@ -207,30 +219,38 @@ export function AccountForm({ onChange(undefined); } else { let cashout; - if (newForm.cashout_payto_uri) switch (cashoutPaytoType) { - case "x-taler-bank": { - cashout = buildPayto("x-taler-bank", url.host, newForm.cashout_payto_uri); - break; - } - case "iban": { - cashout = buildPayto("iban", newForm.cashout_payto_uri, undefined); - break; + if (newForm.cashout_payto_uri) + switch (cashoutPaytoType) { + case "x-taler-bank": { + cashout = buildPayto( + "x-taler-bank", + url.host, + newForm.cashout_payto_uri, + ); + break; + } + case "iban": { + cashout = buildPayto("iban", newForm.cashout_payto_uri, undefined); + break; + } + default: + assertUnreachable(cashoutPaytoType); } - default: assertUnreachable(cashoutPaytoType) - } const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout); let internal; - if (newForm.payto_uri) switch (paytoType) { - case "x-taler-bank": { - internal = buildPayto("x-taler-bank", url.host, newForm.payto_uri); - break; - } - case "iban": { - internal = buildPayto("iban", newForm.payto_uri, undefined); - break; + if (newForm.payto_uri) + switch (paytoType) { + case "x-taler-bank": { + internal = buildPayto("x-taler-bank", url.host, newForm.payto_uri); + break; + } + case "iban": { + internal = buildPayto("iban", newForm.payto_uri, undefined); + break; + } + default: + assertUnreachable(paytoType); } - default: assertUnreachable(paytoType) - } const internalURI = !internal ? undefined : stringifyPaytoUri(internal); const threshold = !parsedAmount @@ -247,7 +267,7 @@ export function AccountForm({ username: newForm.username!, contact_data: undefinedIfEmpty({ email: !newForm.email ? undefined : newForm.email, - phone: !newForm.phone ? undefined :newForm.phone, + phone: !newForm.phone ? undefined : newForm.phone, }), debit_threshold: threshold ?? config.default_debit_threshold, cashout_payto_uri: cashoutURI, @@ -270,7 +290,7 @@ export function AccountForm({ cashout_payto_uri: cashoutURI, contact_data: undefinedIfEmpty({ email: !newForm.email ? undefined : newForm.email, - phone: !newForm.phone ? undefined :newForm.phone, + phone: !newForm.phone ? undefined : newForm.phone, }), debit_threshold: threshold, is_public: newForm.isPublic, @@ -370,7 +390,7 @@ export function AccountForm({

- {purpose === "create" ? undefined : + {purpose === "create" ? undefined : ( ({ ? i18n.str`If empty a random account id will be assigned` : i18n.str`Share this id to receive bank transfers` } - error={errors?.payto_uri} onChange={(e) => { form.payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} - rightIcons={ form.payto_uri ?? defaultValue.payto_uri ?? ""} - />} + rightIcons={ + + form.payto_uri ?? defaultValue.payto_uri ?? "" + } + /> + } value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString} disabled={!editableAccount} /> - } + )}

- To be used when second factor authentication is enabled + + To be used when second factor authentication is enabled +

@@ -454,7 +479,9 @@ export function AccountForm({ />

- To be used when second factor authentication is enabled + + To be used when second factor authentication is enabled +

@@ -468,14 +495,17 @@ export function AccountForm({ form.cashout_payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} - value={(form.cashout_payto_uri ?? defaultValue.cashout_payto_uri) as PaytoString} + value={ + (form.cashout_payto_uri ?? + defaultValue.cashout_payto_uri) as PaytoString + } disabled={!editableCashout} /> )} {/* channel, not shown if old cashout api */} {OLD_CASHOUT_API || - config.supported_tan_channels.length === 0 ? undefined : ( + config.supported_tan_channels.length === 0 ? undefined : (
@@ -685,7 +719,9 @@ export function AccountForm({ class="text-sm text-black font-medium leading-6 " id="availability-label" > - Is this account a payment provider? + + Is this account a payment provider? +