gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: 2fa


From: gnunet
Subject: [taler-wallet-core] branch master updated: 2fa
Date: Thu, 11 Jan 2024 20:42:08 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new 82d4ed90c 2fa
82d4ed90c is described below

commit 82d4ed90caa4a6ea3bdda1fb80ccecf3dc3637f9
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Jan 11 16:41:24 2024 -0300

    2fa
---
 packages/demobank-ui/src/Routing.tsx               |  57 ++-
 packages/demobank-ui/src/components/app.tsx        |  22 +-
 packages/demobank-ui/src/context/config.ts         |  72 ++-
 packages/demobank-ui/src/hooks/access.ts           |  36 +-
 packages/demobank-ui/src/hooks/backend.ts          |  30 +-
 packages/demobank-ui/src/hooks/bank-state.ts       | 104 +++-
 packages/demobank-ui/src/hooks/circuit.ts          |  20 +-
 .../demobank-ui/src/pages/AccountPage/index.ts     |   2 +
 .../demobank-ui/src/pages/AccountPage/state.ts     |   3 +-
 .../demobank-ui/src/pages/AccountPage/views.tsx    |  48 +-
 .../demobank-ui/src/pages/OperationState/index.ts  |   4 +-
 .../demobank-ui/src/pages/OperationState/state.ts  |  14 +-
 .../demobank-ui/src/pages/OperationState/views.tsx |  71 +--
 packages/demobank-ui/src/pages/PaymentOptions.tsx  |  15 +-
 .../src/pages/PaytoWireTransferForm.tsx            |  23 +-
 .../demobank-ui/src/pages/SolveChallengePage.tsx   | 553 +++++++++++++++++++++
 .../demobank-ui/src/pages/WalletWithdrawForm.tsx   |   3 +
 packages/demobank-ui/src/pages/WireTransfer.tsx    |   8 +-
 .../src/pages/WithdrawalConfirmationQuestion.tsx   | 211 +++-----
 .../src/pages/WithdrawalOperationPage.tsx          |   3 +
 .../demobank-ui/src/pages/WithdrawalQRCode.tsx     |  28 +-
 .../src/pages/account/CashoutListForAccount.tsx    |   5 +-
 .../src/pages/account/ShowAccountDetails.tsx       |  19 +-
 .../src/pages/account/UpdateAccountPassword.tsx    |  21 +-
 .../demobank-ui/src/pages/admin/AccountForm.tsx    | 104 +++-
 packages/demobank-ui/src/pages/admin/AdminHome.tsx |  17 +-
 .../demobank-ui/src/pages/admin/RemoveAccount.tsx  |  16 +-
 .../src/pages/business/CreateCashout.tsx           |  38 +-
 .../src/pages/business/ShowCashoutDetails.tsx      |   5 +-
 packages/taler-util/src/http-client/bank-core.ts   |  33 +-
 packages/taler-util/src/http-client/types.ts       |  17 +-
 packages/taler-util/src/http-common.ts             |   2 +-
 .../src/components/LocalNotificationBanner.tsx     |  18 +-
 packages/web-util/src/utils/http-impl.browser.ts   |  11 +-
 packages/web-util/src/utils/http-impl.sw.ts        |  13 +-
 35 files changed, 1233 insertions(+), 413 deletions(-)

diff --git a/packages/demobank-ui/src/Routing.tsx 
b/packages/demobank-ui/src/Routing.tsx
index 4a250a0d5..4caa1dff0 100644
--- a/packages/demobank-ui/src/Routing.tsx
+++ b/packages/demobank-ui/src/Routing.tsx
@@ -39,6 +39,7 @@ import { AccountPage } from "./pages/AccountPage/index.js";
 import { useSettingsContext } from "./context/settings.js";
 import { useBankCoreApiContext } from "./context/config.js";
 import { DownloadStats } from "./pages/DownloadStats.js";
+import { SolveChallengePage } from "./pages/SolveChallengePage.js";
 
 export function Routing(): VNode {
   const history = createHashHistory();
@@ -75,6 +76,9 @@ export function Routing(): VNode {
           component={({ wopid }: { wopid: string }) => (
             <WithdrawalOperationPage
               operationId={wopid}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onContinue={() => {
                 route("/account");
               }}
@@ -113,6 +117,19 @@ export function Routing(): VNode {
               onContinue={() => {
                 route("/account");
               }}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
+            />
+          )}
+        />
+        <Route
+          path="/2fa"
+          component={({ }: {}) => (
+            <SolveChallengePage
+              onContinue={() => {
+                route("/account");
+              }}
             />
           )}
         />
@@ -122,7 +139,7 @@ export function Routing(): VNode {
         />
         <Route
           path="/download-stats"
-          component={() => <DownloadStats 
+          component={() => <DownloadStats
             onCancel={() => {
               route("/account")
             }}
@@ -149,6 +166,9 @@ export function Routing(): VNode {
               onUpdateSuccess={() => {
                 route("/account")
               }}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onClear={() => {
                 route("/account")
               }}
@@ -165,6 +185,9 @@ export function Routing(): VNode {
               onUpdateSuccess={() => {
                 route("/account")
               }}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onCancel={() => {
                 route("/account")
               }}
@@ -179,6 +202,9 @@ export function Routing(): VNode {
               onUpdateSuccess={() => {
                 route("/account")
               }}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onCancel={() => {
                 route("/account")
               }}
@@ -194,6 +220,9 @@ export function Routing(): VNode {
               onSelected={(cid) => {
                 route(`/cashout/${cid}`)
               }}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onClose={() => {
                 route("/account")
               }}
@@ -209,6 +238,9 @@ export function Routing(): VNode {
               onUpdateSuccess={() => {
                 route("/")
               }}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onCancel={() => {
                 route("/account")
               }}
@@ -223,6 +255,9 @@ export function Routing(): VNode {
               onUpdateSuccess={() => {
                 route("/account")
               }}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onClear={() => {
                 route("/account")
               }}
@@ -238,6 +273,9 @@ export function Routing(): VNode {
               onUpdateSuccess={() => {
                 route("/account")
               }}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onCancel={() => {
                 route("/account")
               }}
@@ -253,6 +291,9 @@ export function Routing(): VNode {
               onSelected={(cid) => {
                 route(`/cashout/${cid}`)
               }}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onClose={() => {
                 route("/account");
               }}
@@ -265,8 +306,8 @@ export function Routing(): VNode {
           component={() => (
             <CreateCashout
               account={username}
-              onComplete={(cid) => {
-                route(`/cashout/${cid}`);
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
               }}
               onCancel={() => {
                 route("/account");
@@ -293,6 +334,9 @@ export function Routing(): VNode {
           component={({ dest }: { dest: string }) => (
             <WireTransfer
               toAccount={dest}
+              onAuthorizationRequired={() => {
+                route(`/2fa`)
+              }}
               onCancel={() => {
                 route("/account")
               }}
@@ -308,8 +352,8 @@ export function Routing(): VNode {
           component={() => {
             if (isUserAdministrator) {
               return <AdminHome
-                onRegister={() => {
-                  route("/register");
+                onAuthorizationRequired={() => {
+                  route(`/2fa`)
                 }}
                 onCreateAccount={() => {
                   route("/new-account")
@@ -331,6 +375,9 @@ export function Routing(): VNode {
             } else {
               return <AccountPage
                 account={username}
+                onAuthorizationRequired={() => {
+                  route(`/2fa`)
+                }}
                 goToConfirmOperation={(wopid) => {
                   route(`/operation/${wopid}`);
                 }}
diff --git a/packages/demobank-ui/src/components/app.tsx 
b/packages/demobank-ui/src/components/app.tsx
index 4921b6bff..3d1a43803 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -38,7 +38,7 @@ const App: FunctionalComponent = () => {
     fetchSettings(setSettings)
   }, [])
   if (!settings) return <Loading />;
-  
+
   const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
   return (
     <SettingsProvider value={settings}>
@@ -50,6 +50,26 @@ const App: FunctionalComponent = () => {
                 provider: WITH_LOCAL_STORAGE_CACHE
                   ? localStorageProvider
                   : undefined,
+                // normally, do not revalidate
+                revalidateOnFocus: false,
+                revalidateOnReconnect: false,
+                revalidateIfStale: false,
+                revalidateOnMount: undefined,
+                focusThrottleInterval: undefined,
+
+                // normally, do not refresh
+                refreshInterval: undefined,
+                dedupingInterval: 2000,
+                refreshWhenHidden: false,
+                refreshWhenOffline: false,
+
+                //ignore errors
+                shouldRetryOnError: false,
+                errorRetryCount: 0,
+                errorRetryInterval: undefined,
+
+                // do not go to loading again if already has data
+                keepPreviousData: true,
               }}
             >
               <Routing />
diff --git a/packages/demobank-ui/src/context/config.ts 
b/packages/demobank-ui/src/context/config.ts
index 2d70cf932..0bf920006 100644
--- a/packages/demobank-ui/src/context/config.ts
+++ b/packages/demobank-ui/src/context/config.ts
@@ -14,10 +14,13 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { LibtoolVersion, TalerCorebankApi, TalerCoreBankHttpClient, TalerError 
} from "@gnu-taler/taler-util";
+import { AccessToken, HttpStatusCode, LibtoolVersion, OperationAlternative, 
OperationFail, OperationOk, TalerCorebankApi, TalerCoreBankHttpClient, 
TalerError, TalerErrorCode, UserAndToken } from "@gnu-taler/taler-util";
+import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
 import { BrowserHttpLib, ErrorLoading, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { ComponentChildren, createContext, FunctionComponent, h, VNode } from 
"preact";
 import { useContext, useEffect, useState } from "preact/hooks";
+import { revalidateAccountDetails, revalidatePublicAccounts, 
revalidateTransactions } from "../hooks/access.js";
+import { revalidateBusinessAccounts, revalidateCashouts } from 
"../hooks/circuit.js";
 
 /**
  *
@@ -28,13 +31,14 @@ export type Type = {
   url: URL,
   config: TalerCorebankApi.Config,
   api: TalerCoreBankHttpClient,
+  hints: VersionHint[]
 };
 
 const Context = createContext<Type>(undefined as any);
 
 export const useBankCoreApiContext = (): Type => useContext(Context);
 
-enum VersionHint {
+export enum VersionHint {
   /**
    * when this flag is on, server is running an old version with cashout 
before implementing 2fa API
    */
@@ -42,7 +46,7 @@ enum VersionHint {
 }
 
 export type ConfigResult = undefined
-  | { type: "ok", config: TalerCorebankApi.Config, hint: VersionHint[] }
+  | { type: "ok", config: TalerCorebankApi.Config, hints: VersionHint[] }
   | { type: "incompatible", result: TalerCorebankApi.Config, supported: string 
}
   | { type: "error", error: TalerError }
 
@@ -58,17 +62,17 @@ export const BankCoreApiProvider = ({
   const [checked, setChecked] = useState<ConfigResult>()
   const { i18n } = useTranslationContext();
   const url = new URL(baseUrl)
-  const api = new TalerCoreBankHttpClient(url.href, new BrowserHttpLib())
+  const api = new CacheAwareApi(url.href, new BrowserHttpLib())
   useEffect(() => {
     api.getConfig()
       .then((resp) => {
         if (api.isCompatible(resp.body.version)) {
-          setChecked({ type: "ok", config: resp.body, hint: [] });
+          setChecked({ type: "ok", config: resp.body, hints: [] });
         } else {
           //this API supports version 3.0.3
           const compare = LibtoolVersion.compare("3:0:3", resp.body.version)
           if (compare?.compatible ?? false) {
-            setChecked({ type: "ok", config: resp.body, hint: 
[VersionHint.CASHOUT_BEFORE_2FA] });
+            setChecked({ type: "ok", config: resp.body, hints: 
[VersionHint.CASHOUT_BEFORE_2FA] });
           } else {
             setChecked({ type: "incompatible", result: resp.body, supported: 
api.PROTOCOL_VERSION })
           }
@@ -91,7 +95,7 @@ export const BankCoreApiProvider = ({
     return h(frameOnError, { children: h("div", {}, i18n.str`the bank backend 
is not supported. supported version "${checked.supported}", server version 
"${checked.result.version}"`) })
   }
   const value: Type = {
-    url, config: checked.config, api
+    url, config: checked.config, api: api, hints: checked.hints,
   }
   return h(Context.Provider, {
     value,
@@ -99,6 +103,59 @@ export const BankCoreApiProvider = ({
   });
 };
 
+export class CacheAwareApi extends TalerCoreBankHttpClient {
+  constructor(baseUrl: string, httpClient?: HttpRequestLibrary) {
+    super(baseUrl, httpClient)
+  }
+  async deleteAccount(auth: UserAndToken, cid?: string | undefined) {
+    const resp = await super.deleteAccount(auth, cid)
+    if (resp.type === "ok") {
+      revalidatePublicAccounts()
+      revalidateBusinessAccounts()
+    }
+    return resp;
+  }
+  async createAccount(auth: AccessToken, body: 
TalerCorebankApi.RegisterAccountRequest) {
+    const resp = await super.createAccount(auth, body)
+    if (resp.type === "ok") {
+      revalidatePublicAccounts()
+      revalidateBusinessAccounts()
+    }
+    return resp;
+  }
+  async updateAccount(auth: UserAndToken, body: 
TalerCorebankApi.AccountReconfiguration, cid?: string | undefined) {
+    const resp = await super.updateAccount(auth, body, cid)
+    if (resp.type === "ok") {
+      revalidateAccountDetails()
+    }
+    return resp;
+  }
+  async createTransaction(auth: UserAndToken, body: 
TalerCorebankApi.CreateTransactionRequest, cid?: string | undefined) {
+    const resp = await super.createTransaction(auth, body, cid)
+    if (resp.type === "ok") {
+      revalidateAccountDetails()
+      revalidateTransactions()
+    }
+    return resp;
+  }
+  async confirmWithdrawalById(auth: UserAndToken, wid: string, cid?: string | 
undefined) {
+    const resp = await super.confirmWithdrawalById(auth, wid, cid)
+    if (resp.type === "ok") {
+      revalidateAccountDetails()
+      revalidateTransactions()
+    }
+    return resp;
+  }
+  async createCashout(auth: UserAndToken, body: 
TalerCorebankApi.CashoutRequest, cid?: string | undefined) {
+    const resp = await super.createCashout(auth, body, cid)
+    if (resp.type === "ok") {
+      revalidateAccountDetails()
+      revalidateCashouts()
+    }
+    return resp;
+  }
+}
+
 export const BankCoreApiProviderTesting = ({
   children,
   state,
@@ -112,6 +169,7 @@ export const BankCoreApiProviderTesting = ({
     url: new URL(url),
     config: state,
     api: undefined as any,
+    hints: [],
   };
 
   return h(Context.Provider, {
diff --git a/packages/demobank-ui/src/hooks/access.ts 
b/packages/demobank-ui/src/hooks/access.ts
index fc1cff129..80ef1874f 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -14,13 +14,13 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { AccessToken, TalerBankIntegrationResultByMethod, 
TalerCoreBankResultByMethod, TalerHttpError, WithdrawalOperationStatus } from 
"@gnu-taler/taler-util";
+import { AccessToken, TalerCoreBankResultByMethod, TalerHttpError, 
WithdrawalOperationStatus } from "@gnu-taler/taler-util";
 import { useEffect, useState } from "preact/hooks";
 import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
 import { useBackendState } from "./backend.js";
 
 // FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import _useSWR, { SWRHook } from "swr";
+import _useSWR, { SWRHook, mutate } from "swr";
 import { useBankCoreApiContext } from "../context/config.js";
 const useSWR = _useSWR as unknown as SWRHook;
 
@@ -30,6 +30,10 @@ export interface InstanceTemplateFilter {
   position?: string;
 }
 
+export function revalidateAccountDetails() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === "getAccount", 
undefined, { revalidate: true })
+}
+
 export function useAccountDetails(account: string) {
   const { state: credentials } = useBackendState();
   const { api } = useBankCoreApiContext();
@@ -40,15 +44,6 @@ export function useAccountDetails(account: string) {
   const token = credentials.status !== "loggedIn" ? undefined : 
credentials.token
   const { data, error } = useSWR<TalerCoreBankResultByMethod<"getAccount">, 
TalerHttpError>(
     [account, token, "getAccount"], fetcher, {
-    refreshInterval: 0,
-    refreshWhenHidden: false,
-    revalidateOnFocus: false,
-    revalidateOnReconnect: false,
-    refreshWhenOffline: false,
-    errorRetryCount: 0,
-    errorRetryInterval: 1,
-    shouldRetryOnError: false,
-    keepPreviousData: true,
   });
 
   if (data) return data
@@ -56,6 +51,10 @@ export function useAccountDetails(account: string) {
   return undefined;
 }
 
+export function revalidateWithdrawalDetails() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === 
"getWithdrawalById")
+}
+
 export function useWithdrawalDetails(wid: string) {
   const { api } = useBankCoreApiContext();
   const [latestStatus, setLatestStatus] = useState<WithdrawalOperationStatus>()
@@ -90,6 +89,9 @@ export function useWithdrawalDetails(wid: string) {
   return undefined;
 }
 
+export function revalidateTransactionDetails() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === 
"getTransactionById")
+}
 export function useTransactionDetails(account: string, tid: number) {
   const { state: credentials } = useBackendState();
   const token = credentials.status !== "loggedIn" ? undefined : 
credentials.token
@@ -117,6 +119,9 @@ export function useTransactionDetails(account: string, tid: 
number) {
   return undefined;
 }
 
+export function revalidatePublicAccounts() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === 
"getPublicAccounts")
+}
 export function usePublicAccounts(filterAccount: string | undefined, initial?: 
number) {
   const [offset, setOffset] = useState<number | undefined>(initial);
   const { api } = useBankCoreApiContext();
@@ -171,12 +176,9 @@ export function usePublicAccounts(filterAccount: string | 
undefined, initial?: n
   return undefined;
 }
 
-/**
-
- * @param account
- * @param args
- * @returns
- */
+export function revalidateTransactions() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === 
"getTransactions", undefined, { revalidate: true })
+}
 export function useTransactions(account: string, initial?: number) {
   const { state: credentials } = useBackendState();
   const token = credentials.status !== "loggedIn" ? undefined : 
credentials.token
diff --git a/packages/demobank-ui/src/hooks/backend.ts 
b/packages/demobank-ui/src/hooks/backend.ts
index 863b47bf3..46918ac10 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -19,16 +19,15 @@ import {
   Codec,
   buildCodecForObject,
   buildCodecForUnion,
-  canonicalizeBaseUrl,
   codecForBoolean,
   codecForConstString,
-  codecForString,
+  codecForString
 } from "@gnu-taler/taler-util";
 import {
   buildStorageKey,
   useLocalStorage
 } from "@gnu-taler/web-util/browser";
-import { useSWRConfig } from "swr";
+import { mutate } from "swr";
 
 /**
  * Has the information to reach and
@@ -105,7 +104,6 @@ export function useBackendState(): BackendStateHandler {
     BACKEND_STATE_KEY,
     defaultState,
   );
-  const mutateAll = useMatchMutate();
 
   return {
     state,
@@ -129,29 +127,11 @@ export function useBackendState(): BackendStateHandler {
         isUserAdministrator: info.username === "admin",
       };
       update(nextState);
-      mutateAll(/.*/)
+      cleanAllCache()
     },
   };
 }
 
-export function useMatchMutate(): (
-  re: RegExp,
-  value?: unknown,
-) => Promise<any> {
-  const { cache, mutate } = useSWRConfig();
-
-  if (!(cache instanceof Map)) {
-    throw new Error(
-      "matchMutate requires the cache provider to be a Map instance",
-    );
-  }
-
-  return function matchRegexMutate(re: RegExp, value?: unknown) {
-    const allKeys = Array.from(cache.keys());
-    const keys = allKeys.filter((key) => re.test(key));
-    const mutations = keys.map((key) => {
-      return mutate(key, value, true);
-    });
-    return Promise.all(mutations);
-  };
+function cleanAllCache(): void {
+  mutate(() => true, undefined, { revalidate: false })
 }
diff --git a/packages/demobank-ui/src/hooks/bank-state.ts 
b/packages/demobank-ui/src/hooks/bank-state.ts
index addbbfc0f..99d835c9c 100644
--- a/packages/demobank-ui/src/hooks/bank-state.ts
+++ b/packages/demobank-ui/src/hooks/bank-state.ts
@@ -15,31 +15,127 @@
  */
 
 import {
+  AbsoluteTime,
   Codec,
+  TalerCorebankApi,
   buildCodecForObject,
+  buildCodecForUnion,
+  codecForAbsoluteTime,
+  codecForAny,
+  codecForConstString,
   codecForString,
+  codecForTanTransmission,
   codecOptional
 } from "@gnu-taler/taler-util";
 import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
 
+export type ChallengeInProgess =
+  DeleteAccountChallenge |
+  UpdateAccountChallenge |
+  UpdatePasswordChallenge |
+  CreateTransactionChallenge |
+  ConfirmWithdrawalChallenge |
+  CashoutChallenge;
+
+type BaseChallenge<OpType extends string, ReqType> = {
+  id: string,
+  operation: OpType,
+  sent: AbsoluteTime,
+  info?: TalerCorebankApi.TanTransmission,
+  request: ReqType
+}
+
+type DeleteAccountChallenge = BaseChallenge<"delete-account", string>
+type UpdateAccountChallenge = BaseChallenge<"update-account", 
TalerCorebankApi.AccountReconfiguration>
+type UpdatePasswordChallenge = BaseChallenge<"update-password", 
TalerCorebankApi.AccountPasswordChange>
+type CreateTransactionChallenge = BaseChallenge<"create-transaction", 
TalerCorebankApi.CreateTransactionRequest>
+type ConfirmWithdrawalChallenge = BaseChallenge<"confirm-withdrawal", string>
+type CashoutChallenge = BaseChallenge<"create-cashout", 
TalerCorebankApi.CashoutRequest>
+
+const codecForChallengeUpdatePassword = (): Codec<UpdatePasswordChallenge> =>
+  buildCodecForObject<UpdatePasswordChallenge>()
+    .property("operation", codecForConstString("update-password"))
+    .property("id", codecForString())
+    .property("sent", codecForAbsoluteTime)
+    .property("info", codecOptional(codecForTanTransmission()))
+    .property("request", codecForAny())
+    .build("UpdatePasswordChallenge");
+
+const codecForChallengeDeleteAccount = (): Codec<DeleteAccountChallenge> =>
+  buildCodecForObject<DeleteAccountChallenge>()
+    .property("operation", codecForConstString("delete-account"))
+    .property("id", codecForString())
+    .property("sent", codecForAbsoluteTime)
+    .property("request", codecForString())
+    .property("info", codecOptional(codecForTanTransmission()))
+    .build("DeleteAccountChallenge");
+
+const codecForChallengeUpdateAccount = (): Codec<UpdateAccountChallenge> =>
+  buildCodecForObject<UpdateAccountChallenge>()
+    .property("operation", codecForConstString("update-account"))
+    .property("id", codecForString())
+    .property("sent", codecForAbsoluteTime)
+    .property("info", codecOptional(codecForTanTransmission()))
+    .property("request", codecForAny())
+    .build("UpdateAccountChallenge");
+
+const codecForChallengeCreateTransaction = (): 
Codec<CreateTransactionChallenge> =>
+  buildCodecForObject<CreateTransactionChallenge>()
+    .property("operation", codecForConstString("create-transaction"))
+    .property("id", codecForString())
+    .property("sent", codecForAbsoluteTime)
+    .property("info", codecOptional(codecForTanTransmission()))
+    .property("request", codecForAny())
+    .build("CreateTransactionChallenge");
+
+const codecForChallengeConfirmWithdrawal = (): 
Codec<ConfirmWithdrawalChallenge> =>
+  buildCodecForObject<ConfirmWithdrawalChallenge>()
+    .property("operation", codecForConstString("confirm-withdrawal"))
+    .property("id", codecForString())
+    .property("sent", codecForAbsoluteTime)
+    .property("info", codecOptional(codecForTanTransmission()))
+    .property("request", codecForString())
+    .build("ConfirmWithdrawalChallenge");
+
+const codecForChallengeCashout = (): Codec<CashoutChallenge> =>
+  buildCodecForObject<CashoutChallenge>()
+    .property("operation", codecForConstString("create-cashout"))
+    .property("id", codecForString())
+    .property("sent", codecForAbsoluteTime)
+    .property("info", codecOptional(codecForTanTransmission()))
+    .property("request", codecForAny())
+    .build("CashoutChallenge");
+
+const codecForChallenge = (): Codec<ChallengeInProgess> =>
+  buildCodecForUnion<ChallengeInProgess>()
+    .discriminateOn("operation")
+    .alternative("confirm-withdrawal", codecForChallengeConfirmWithdrawal())
+    .alternative("create-cashout", codecForChallengeCashout())
+    .alternative("create-transaction", codecForChallengeCreateTransaction())
+    .alternative("delete-account", codecForChallengeDeleteAccount())
+    .alternative("update-account", codecForChallengeUpdateAccount())
+    .alternative("update-password", codecForChallengeUpdatePassword())
+    .build("ChallengeInProgess");
+
+
 interface BankState {
   currentWithdrawalOperationId: string | undefined;
-  currentChallengeId: string | undefined;
+  currentChallenge: ChallengeInProgess | undefined;
 }
 
 export const codecForBankState = (): Codec<BankState> =>
   buildCodecForObject<BankState>()
     .property("currentWithdrawalOperationId", codecOptional(codecForString()))
-    .property("currentChallengeId", codecOptional(codecForString()))
+    .property("currentChallenge", codecOptional(codecForChallenge()))
     .build("BankState");
 
 const defaultBankState: BankState = {
   currentWithdrawalOperationId: undefined,
-  currentChallengeId: undefined,
+  currentChallenge: undefined,
 };
 
 const BANK_STATE_KEY = buildStorageKey(
-  "bank-state",
+  "bank-app-state",
   codecForBankState(),
 );
 
diff --git a/packages/demobank-ui/src/hooks/circuit.ts 
b/packages/demobank-ui/src/hooks/circuit.ts
index 8a27f652c..8bff6858d 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -19,7 +19,7 @@ import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
 import { useBackendState } from "./backend.js";
 
 import { AccessToken, AmountJson, AmountString, Amounts, OperationOk, 
TalerBankConversionResultByMethod, TalerCoreBankErrorsByMethod, 
TalerCoreBankResultByMethod, TalerCorebankApi, TalerError, TalerHttpError, 
opFixedSuccess } from "@gnu-taler/taler-util";
-import _useSWR, { SWRHook } from "swr";
+import _useSWR, { SWRHook, mutate } from "swr";
 import { useBankCoreApiContext } from "../context/config.js";
 import { assertUnreachable } from "../pages/WithdrawalOperationPage.js";
 import { format, getDate, getDay, getHours, getMonth, getYear, set, sub } from 
"date-fns";
@@ -42,6 +42,9 @@ type CashoutEstimators = {
   estimateByDebit: EstimatorFunction;
 };
 
+export function revalidateConversionInfo() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === 
"getConversionInfoAPI")
+}
 export function useConversionInfo() {
   const { api, config } = useBankCoreApiContext()
 
@@ -114,6 +117,9 @@ export function useEstimator(): CashoutEstimators {
   };
 }
 
+export function revalidateBusinessAccounts() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === "getAccounts")
+}
 export function useBusinessAccounts() {
   const { state: credentials } = useBackendState();
   const token = credentials.status !== "loggedIn" ? undefined : 
credentials.token
@@ -174,6 +180,9 @@ type CashoutWithId = TalerCorebankApi.CashoutStatusResponse 
& { id: number }
 function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
   return c !== undefined
 }
+export function revalidateOnePendingCashouts() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === 
"useOnePendingCashouts")
+}
 export function useOnePendingCashouts(account: string) {
   const { state: credentials } = useBackendState();
   const { api, config } = useBankCoreApiContext();
@@ -211,6 +220,9 @@ export function useOnePendingCashouts(account: string) {
   return undefined;
 }
 
+export function revalidateCashouts() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === "useCashouts")
+}
 export function useCashouts(account: string) {
   const { state: credentials } = useBackendState();
   const { api, config } = useBankCoreApiContext();
@@ -251,6 +263,9 @@ export function useCashouts(account: string) {
   return undefined;
 }
 
+export function revalidateCashoutDetails() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === "getCashoutById")
+}
 export function useCashoutDetails(cashoutId: number | undefined) {
   const { state: credentials } = useBackendState();
   const creds = credentials.status !== "loggedIn" ? undefined : credentials
@@ -284,6 +299,9 @@ export type MonitorMetrics = {
 }
 
 export type LastMonitor = { current: 
TalerCoreBankResultByMethod<"getMonitor">, previous: 
TalerCoreBankResultByMethod<"getMonitor"> }
+export function revalidateLastMonitorInfo() {
+  mutate(key => Array.isArray(key) && key[key.length - 1] === 
"useLastMonitorInfo")
+}
 export function useLastMonitorInfo(currentMoment: number, previousMoment: 
number, timeframe: TalerCorebankApi.MonitorTimeframeParam) {
   const { api, config } = useBankCoreApiContext();
   const { state: credentials } = useBackendState();
diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts 
b/packages/demobank-ui/src/pages/AccountPage/index.ts
index 7261af69a..cfe184612 100644
--- a/packages/demobank-ui/src/pages/AccountPage/index.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/index.ts
@@ -23,6 +23,7 @@ import { InvalidIbanView, ReadyView } from "./views.js";
 
 export interface Props {
   account: string;
+  onAuthorizationRequired: () => void;
   goToConfirmOperation: (id: string) => void;
 }
 
@@ -48,6 +49,7 @@ export namespace State {
     error: undefined;
     account: string,
     limit: AmountJson,
+    onAuthorizationRequired: () => void;
     goToConfirmOperation: (id: string) => void;
   }
 
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts 
b/packages/demobank-ui/src/pages/AccountPage/state.ts
index 88e8cf747..38b4d9f36 100644
--- a/packages/demobank-ui/src/pages/AccountPage/state.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/state.ts
@@ -20,7 +20,7 @@ import { useAccountDetails } from "../../hooks/access.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
 import { Props, State } from "./index.js";
 
-export function useComponentState({ account, goToConfirmOperation }: Props): 
State {
+export function useComponentState({ account, goToConfirmOperation, 
onAuthorizationRequired }: Props): State {
   const result = useAccountDetails(account);
   const { i18n } = useTranslationContext();
 
@@ -78,6 +78,7 @@ export function useComponentState({ account, 
goToConfirmOperation }: Props): Sta
     status: "ready",
     goToConfirmOperation,
     error: undefined,
+    onAuthorizationRequired,
     account,
     limit,
   };
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx 
b/packages/demobank-ui/src/pages/AccountPage/views.tsx
index d760543c6..59a6db7b9 100644
--- a/packages/demobank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -14,15 +14,15 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
-import { Attention } from "@gnu-taler/web-util/browser";
 import { Transactions } from "../../components/Transactions/index.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { useOnePendingCashouts } from "../../hooks/circuit.js";
 import { usePreferences } from "../../hooks/preferences.js";
 import { PaymentOptions } from "../PaymentOptions.js";
 import { State } from "./index.js";
-import { useCashouts, useOnePendingCashouts } from "../../hooks/circuit.js";
-import { TalerError } from "@gnu-taler/taler-util";
 
 export function InvalidIbanView({ error }: State.InvalidIban) {
   return (
@@ -55,27 +55,35 @@ function ShowDemoInfo(): VNode {
   </Attention>
 }
 
-export function ReadyView({ account, limit, goToConfirmOperation }: 
State.Ready): VNode<{}> {
+function ShowPedingOperation(): VNode {
+  const { i18n } = useTranslationContext();
+  const [bankState, updateBankState] = useBankState();
+  if (!bankState.currentChallenge) return <Fragment />;
+  const title = ((op): TranslatedString => {
+    switch (op) {
+      case "delete-account": return i18n.str`Pending account delete operation`
+      case "update-account": return i18n.str`Pending account update operation`
+      case "update-password": return i18n.str`Pending password update 
operation`
+      case "create-transaction": return i18n.str`Pending transaction operation`
+      case "confirm-withdrawal": return i18n.str`Pending withdrawal operation`
+      case "create-cashout": return i18n.str`Pending cashout operation`
+    }
+  })(bankState.currentChallenge.operation)
+  return <Attention title={title} type="warning" onClose={() => { 
updateBankState("currentChallenge", undefined); }}>
+    <i18n.Translate>
+      To complete or cancel the operation click <a class="font-semibold 
text-yellow-700 hover:text-yellow-600" href={`#/2fa`}>here</a>
+    </i18n.Translate>
+  </Attention>
+}
+
+export function ReadyView({ account, limit, goToConfirmOperation, 
onAuthorizationRequired }: State.Ready): VNode<{}> {
 
   return <Fragment>
+    <ShowPedingOperation />
     <ShowDemoInfo />
-    <PendingCashouts account={account}/>
-    <PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} 
/>
+    <PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} 
onAuthorizationRequired={onAuthorizationRequired} />
     <Transactions account={account} />
   </Fragment>;
 }
 
 
-function PendingCashouts({account}: {account: string}):VNode {
-  const { i18n } = useTranslationContext();
-  const result = useOnePendingCashouts(account)
-  if (!result || result instanceof TalerError || result.type !== "ok" || 
!result.body) {
-    return <Fragment />
-  }
-
-  return <Attention title={i18n.str`You have pending cashout operation to 
complete`} >
-    <i18n.Translate>
-      Cashout with subject "{result.body.subject}", look for the code and 
complete the operation <a target="_blank" rel="noreferrer noopener" 
class="font-semibold text-blue-700 hover:text-blue-600" 
href={`#/cashout/${result.body.id}`}>here</a>.
-    </i18n.Translate>
-  </Attention>
-}
\ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts 
b/packages/demobank-ui/src/pages/OperationState/index.ts
index e3aec21c5..53d07e44b 100644
--- a/packages/demobank-ui/src/pages/OperationState/index.ts
+++ b/packages/demobank-ui/src/pages/OperationState/index.ts
@@ -22,6 +22,7 @@ import { AbortedView, ConfirmedView, FailedView, 
InvalidPaytoView, InvalidReserv
 
 export interface Props {
   currency: string;
+  onAuthorizationRequired: () => void,
   onClose: () => void;
 }
 
@@ -82,11 +83,12 @@ export namespace State {
   }
   export interface NeedConfirmation {
     status: "need-confirmation",
+    onAuthorizationRequired: () => void,
     account: string,
     onAbort: undefined | (() => 
Promise<TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined>);
     onConfirm: undefined | (() => 
Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined>);
     error: undefined;
-    busy: boolean,
+    id: string,
   }
   export interface Aborted {
     status: "aborted",
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts 
b/packages/demobank-ui/src/pages/OperationState/state.ts
index b214a400d..fbf43867f 100644
--- a/packages/demobank-ui/src/pages/OperationState/state.ts
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -14,26 +14,25 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Amounts, FailCasesByMethod, HttpStatusCode, 
TalerCoreBankErrorsByMethod, TalerError, TalerErrorDetail, TranslatedString, 
parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from 
"@gnu-taler/taler-util";
-import { notify, notifyError, notifyInfo, useTranslationContext, utils } from 
"@gnu-taler/web-util/browser";
+import { Amounts, HttpStatusCode, TalerCoreBankErrorsByMethod, TalerError, 
parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from 
"@gnu-taler/taler-util";
+import { utils } from "@gnu-taler/web-util/browser";
 import { useEffect, useState } from "preact/hooks";
 import { mutate } from "swr";
 import { useBankCoreApiContext } from "../../context/config.js";
 import { useWithdrawalDetails } from "../../hooks/access.js";
 import { useBackendState } from "../../hooks/backend.js";
+import { useBankState } from "../../hooks/bank-state.js";
 import { usePreferences } from "../../hooks/preferences.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
 import { Props, State } from "./index.js";
-import { useBankState } from "../../hooks/bank-state.js";
 
-export function useComponentState({ currency, onClose }: Props): 
utils.RecursiveState<State> {
+export function useComponentState({ currency, onClose, 
onAuthorizationRequired, }: Props): utils.RecursiveState<State> {
   const [settings] = usePreferences()
   const [bankState, updateBankState] = useBankState();
   const { state: credentials } = useBackendState()
   const creds = credentials.status !== "loggedIn" ? undefined : credentials
   const { api } = useBankCoreApiContext()
 
-  const [busy, setBusy] = useState<Record<string, undefined>>()
   const [failure, setFailure] = 
useState<TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined>()
   const amount = settings.maxWithdrawalAmount
 
@@ -88,9 +87,7 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
 
   async function doConfirm(): 
Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined> {
     if (!creds) return;
-    setBusy({})
     const resp = await api.confirmWithdrawalById(creds, wid);
-    setBusy(undefined)
     if (resp.type === "ok") {
       mutate(() => true)//clean withdrawal state
     } else {
@@ -213,9 +210,10 @@ export function useComponentState({ currency, onClose }: 
Props): utils.Recursive
     return {
       status: "need-confirmation",
       error: undefined,
+      onAuthorizationRequired,
       account: data.username,
+      id: withdrawalOperationId,
       onAbort: !creds ? undefined : doAbort,
-      busy: !!busy,
       onConfirm: !creds ? undefined : doConfirm
     }
   }
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx 
b/packages/demobank-ui/src/pages/OperationState/views.tsx
index 5ebd66dac..0ebdeea47 100644
--- a/packages/demobank-ui/src/pages/OperationState/views.tsx
+++ b/packages/demobank-ui/src/pages/OperationState/views.tsx
@@ -14,17 +14,16 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { HttpStatusCode, TalerErrorCode, TranslatedString, 
stringifyWithdrawUri } from "@gnu-taler/taler-util";
-import { Attention, LocalNotificationBanner, ShowInputErrorLabel, notifyInfo, 
useLocalNotification, useTranslationContext } from 
"@gnu-taler/web-util/browser";
+import { AbsoluteTime, HttpStatusCode, TalerErrorCode, TranslatedString, 
stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { Attention, LocalNotificationBanner, notifyInfo, useLocalNotification, 
useTranslationContext } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
-import { useEffect, useMemo, useState } from "preact/hooks";
+import { useEffect } from "preact/hooks";
 import { QR } from "../../components/QR.js";
+import { useBankState } from "../../hooks/bank-state.js";
 import { usePreferences } from "../../hooks/preferences.js";
-import { undefinedIfEmpty } from "../../utils.js";
 import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
 import { State } from "./index.js";
-import { useBankState } from "../../hooks/bank-state.js";
 
 export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) {
   return (
@@ -42,30 +41,12 @@ export function InvalidReserveView({ reserve, onClose }: 
State.InvalidReserve) {
   );
 }
 
-export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: 
doConfirm, busy, account }: State.NeedConfirmation) {
+export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: 
doConfirm, account, id, onAuthorizationRequired, }: State.NeedConfirmation) {
   const { i18n } = useTranslationContext()
   const [settings] = usePreferences()
   const [notification, notify, errorHandler] = useLocalNotification()
   const [, updateBankState] = useBankState()
 
-  const captchaNumbers = useMemo(() => {
-    return {
-      a: Math.floor(Math.random() * 10),
-      b: Math.floor(Math.random() * 10),
-    };
-  }, []);
-  const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
-  const answer = parseInt(captchaAnswer ?? "", 10);
-  const errors = undefinedIfEmpty({
-    answer: !captchaAnswer
-      ? i18n.str`Answer the question before continue`
-      : Number.isNaN(answer)
-        ? i18n.str`The answer should be a number`
-        : answer !== captchaNumbers.a + captchaNumbers.b
-          ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + 
${captchaNumbers.b}" is wrong.`
-          : undefined,
-  }) ?? (busy ? {} as Record<string, undefined> : undefined);
-
   async function onCancel() {
     errorHandler(async () => {
       if (!doAbort) return;
@@ -137,11 +118,13 @@ export function NeedConfirmationView({ error, onAbort: 
doAbort, onConfirm: doCon
           debug: resp.detail,
         });
         case HttpStatusCode.Accepted: {
-          updateBankState("currentChallengeId", resp.body.challenge_id)
-          return notify({
-            type: "info",
-            title: i18n.str`The operation needs a confirmation to complete.`,
-          });
+          updateBankState("currentChallenge", {
+            operation: "confirm-withdrawal",
+            id: String(resp.body.challenge_id),
+            sent: AbsoluteTime.never(),
+            request: id,
+          })
+          return onAuthorizationRequired()
         }
         default: assertUnreachable(resp)
       }
@@ -165,35 +148,6 @@ export function NeedConfirmationView({ error, onAbort: 
doAbort, onConfirm: doCon
                 e.preventDefault()
               }}
             >
-              <div class="px-4 py-6 sm:p-8">
-                <label for="withdraw-amount">{i18n.str`What is`}&nbsp;
-                  <em>
-                    {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
-                  </em>
-                  ?
-                </label>
-                <div class="mt-2">
-                  <div class="relative rounded-md shadow-sm">
-                    <input
-                      type="text"
-                      // class="block w-full rounded-md border-0 py-1.5 pl-16 
text-gray-900 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"
-                      aria-describedby="answer"
-                      autoFocus
-                      class="block w-full 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={captchaAnswer ?? ""}
-                      required
-
-                      name="answer"
-                      id="answer"
-                      autocomplete="off"
-                      onChange={(e): void => {
-                        setCaptchaAnswer(e.currentTarget.value)
-                      }}
-                    />
-                  </div>
-                  <ShowInputErrorLabel message={errors?.answer} 
isDirty={captchaAnswer !== undefined} />
-                </div>
-              </div>
               <div class="flex items-center justify-between gap-x-6 border-t 
border-gray-900/10 px-4 py-4 sm:px-8">
                 <button type="button" class="text-sm font-semibold leading-6 
text-gray-900"
                   onClick={(e) => {
@@ -204,7 +158,6 @@ export function NeedConfirmationView({ error, onAbort: 
doAbort, onConfirm: doCon
                   <i18n.Translate>Cancel</i18n.Translate></button>
                 <button type="submit"
                   class="disabled:opacity-50 disabled:cursor-default 
cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 
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) => {
                     e.preventDefault()
                     onConfirm()
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx 
b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 1a431a939..06d293097 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -16,18 +16,21 @@
 
 import { AmountJson } from "@gnu-taler/taler-util";
 import { notifyInfo, useTranslationContext } from 
"@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { VNode, h } from "preact";
 import { useState } from "preact/hooks";
-import { PaytoWireTransferForm, doAutoFocus } from 
"./PaytoWireTransferForm.js";
-import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
-import { usePreferences } from "../hooks/preferences.js";
 import { useBankState } from "../hooks/bank-state.js";
+import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
+import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
 
 /**
  * Let the user choose a payment option,
  * then specify the details trigger the action.
  */
-export function PaymentOptions({ limit, goToConfirmOperation }: { limit: 
AmountJson, goToConfirmOperation: (id: string) => void }): VNode {
+export function PaymentOptions({ limit, goToConfirmOperation, 
onAuthorizationRequired }: {
+  limit: AmountJson,
+  onAuthorizationRequired: () => void,
+  goToConfirmOperation: (id: string) => void,
+}): VNode {
   const { i18n } = useTranslationContext();
   const [bankState] = useBankState();
 
@@ -96,6 +99,7 @@ export function PaymentOptions({ limit, goToConfirmOperation 
}: { limit: AmountJ
           <WalletWithdrawForm
             focus
             limit={limit}
+            onAuthorizationRequired={onAuthorizationRequired}
             goToConfirmOperation={goToConfirmOperation}
             onCancel={() => {
               setTab(undefined)
@@ -107,6 +111,7 @@ export function PaymentOptions({ limit, 
goToConfirmOperation }: { limit: AmountJ
             focus
             title={i18n.str`Transfer details`}
             limit={limit}
+            onAuthorizationRequired={onAuthorizationRequired}
             onSuccess={() => {
               notifyInfo(i18n.str`Wire transfer created!`);
               setTab(undefined)
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 0c6c9ada2..f7b81be48 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -15,6 +15,7 @@
  */
 
 import {
+  AbsoluteTime,
   AmountJson,
   AmountLike,
   AmountString,
@@ -57,12 +58,14 @@ export function PaytoWireTransferForm({
   toAccount,
   onSuccess,
   onCancel,
+  onAuthorizationRequired,
   limit,
 }: {
   title: TranslatedString,
   focus?: boolean;
   toAccount?: string,
   onSuccess: () => void;
+  onAuthorizationRequired: () => void;
   onCancel: (() => void) | undefined;
   limit: AmountJson;
 }): VNode {
@@ -146,12 +149,14 @@ export function PaytoWireTransferForm({
       sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString
     }
     const puri = payto_uri;
+    const sAmount = sendingAmount;
 
     await handleError(async () => {
-      const resp = await api.createTransaction(credentials, {
+      const request = {
         payto_uri: puri,
-        amount: sendingAmount,
-      });
+        amount: sAmount,
+      }
+      const resp = await api.createTransaction(credentials, request);
       mutate(() => true)
       if (resp.type === "fail") {
         switch (resp.case) {
@@ -192,11 +197,13 @@ export function PaytoWireTransferForm({
             debug: resp.detail,
           })
           case HttpStatusCode.Accepted: {
-            updateBankState("currentChallengeId", resp.body.challenge_id)
-            return notify({
-              type: "info",
-              title: i18n.str`The operation needs a confirmation to complete.`,
-            });
+            updateBankState("currentChallenge", {
+              operation: "create-transaction",
+              id: String(resp.body.challenge_id),
+              sent: AbsoluteTime.never(),
+              request,
+            })
+            return onAuthorizationRequired()
           }
           default: assertUnreachable(resp)
         }
diff --git a/packages/demobank-ui/src/pages/SolveChallengePage.tsx 
b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
new file mode 100644
index 000000000..e55038df5
--- /dev/null
+++ b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
@@ -0,0 +1,553 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {
+  AbsoluteTime,
+  Amounts,
+  HttpStatusCode,
+  Logger,
+  TalerCorebankApi,
+  TalerError,
+  TalerErrorCode,
+  TranslatedString,
+  assertUnreachable,
+  parsePaytoUri
+} from "@gnu-taler/taler-util";
+import {
+  Loading,
+  LocalNotificationBanner,
+  ShowInputErrorLabel,
+  useLocalNotification,
+  useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { useBankCoreApiContext } from "../context/config.js";
+import { useWithdrawalDetails } from "../hooks/access.js";
+import { useBackendState } from "../hooks/backend.js";
+import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js";
+import { useConversionInfo } from "../hooks/circuit.js";
+import { undefinedIfEmpty } from "../utils.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
+import { OperationNotFound } from "./WithdrawalQRCode.js";
+
+const logger = new Logger("SolveChallenge");
+
+export function SolveChallengePage({
+  onContinue,
+}: {
+  onContinue: () => void;
+}): VNode {
+  const { api } = useBankCoreApiContext()
+  const { i18n } = useTranslationContext();
+  const [bankState, updateBankState] = useBankState();
+  const [code, setCode] = useState<string | undefined>(undefined);
+  const [notification, notify, handleError] = useLocalNotification()
+  const { state } = useBackendState();
+  const creds = state.status !== "loggedIn" ? undefined : state
+
+  if (!bankState.currentChallenge) {
+    return <div>
+      <span>no challenge to solve  </span>
+      <button type="button"
+        class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 
text-sm font-semibold text-white shadow-sm hover:bg-red-500 
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-red-500"
+        onClick={() => {
+          onContinue()
+        }}
+      >
+        <i18n.Translate>Continue</i18n.Translate>
+      </button>
+    </div>
+  }
+
+  const ch = bankState.currentChallenge
+  const errors = undefinedIfEmpty({
+    code: !code ? i18n.str`required` : undefined,
+  });
+
+  async function startChallenge() {
+    if (!creds) return;
+    await handleError(async () => {
+      const resp = await api.sendChallenge(creds, ch.id);
+      if (resp.type === "ok") {
+        const newCh = structuredClone(ch)
+        newCh.sent = AbsoluteTime.now()
+        newCh.info = resp.body
+        updateBankState("currentChallenge", newCh)
+      } else {
+        switch (resp.case) {
+          case HttpStatusCode.NotFound: return notify({
+            type: "error",
+            title: i18n.str`Cashout not found. It may be also mean that it was 
already aborted.`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          })
+          case HttpStatusCode.Unauthorized: return notify({
+            type: "error",
+            title: i18n.str`Cashout not found. It may be also mean that it was 
already aborted.`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          })
+          case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return notify({
+            type: "error",
+            title: i18n.str`Cashout not found. It may be also mean that it was 
already aborted.`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          })
+          default: assertUnreachable(resp)
+        }
+      }
+    })
+  }
+
+  async function completeChallenge() {
+    if (!creds || !code) return;
+    await handleError(async () => {
+      {
+        const resp = await api.confirmChallenge(creds, ch.id, {
+          tan: code
+        });
+        if (resp.type === "fail") {
+          setCode("")
+          switch (resp.case) {
+            case HttpStatusCode.NotFound: return notify({
+              type: "error",
+              title: i18n.str`Challenge not found.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+            })
+            case HttpStatusCode.Unauthorized: return notify({
+              type: "error",
+              title: i18n.str`This user is not authorized to complete this 
challenge.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+            })
+            case HttpStatusCode.TooManyRequests: return notify({
+              type: "error",
+              title: i18n.str`Too many attemps, try another code.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+            })
+            case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED: return notify({
+              type: "error",
+              title: i18n.str`The confirmation code is wrong, try again.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+            })
+            case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: return notify({
+              type: "error",
+              title: i18n.str`The operation expired.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+            })
+            default: assertUnreachable(resp)
+          }
+        }
+      }
+      {
+        const resp = await (async (ch: ChallengeInProgess) => {
+          switch (ch.operation) {
+            case "delete-account": return await api.deleteAccount(creds, ch.id)
+            case "update-account": return await api.updateAccount(creds, 
ch.request, ch.id)
+            case "update-password": return await api.updatePassword(creds, 
ch.request, ch.id)
+            case "create-transaction": return await 
api.createTransaction(creds, ch.request, ch.id)
+            case "confirm-withdrawal": return await 
api.confirmWithdrawalById(creds, ch.request, ch.id)
+            case "create-cashout": return await api.createCashout(creds, 
ch.request, ch.id)
+            default: assertUnreachable(ch)
+          }
+        })(ch);
+
+        if (resp.type === "fail") {
+          if (resp.case !== HttpStatusCode.Accepted) {
+            return notify({
+              type: "error",
+              title: i18n.str`The operation failed.`,
+              description: resp.detail.hint as TranslatedString,
+              debug: resp.detail,
+            })
+          }
+          // another challenge required
+          updateBankState("currentChallenge", {
+            operation: ch.operation,
+            id: String(resp.body.challenge_id),
+            sent: AbsoluteTime.never(),
+            request: ch.request as any,
+          })
+          return notify({
+            type: "info",
+            title: i18n.str`The operation needs another confirmation to 
complete.`,
+          })
+        }
+        updateBankState("currentChallenge", undefined)
+        return onContinue()
+      }
+    })
+  }
+
+  const subtitle = ((op): TranslatedString => {
+    switch (op) {
+      case "delete-account": return i18n.str`Account delete`
+      case "update-account": return i18n.str`Account update`
+      case "update-password": return i18n.str`Password update`
+      case "create-transaction": return i18n.str`Wire transfer`
+      case "confirm-withdrawal": return i18n.str`Withdrawal`
+      case "create-cashout": return i18n.str`Cashout`
+    }
+  })(ch.operation)
+
+  return (
+    <Fragment>
+      <LocalNotificationBanner notification={notification} />
+      <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">
+        <div class="px-4 sm:px-0">
+          <h2 class="text-base font-semibold leading-7 text-gray-900">
+            <span class="text-sm text-black font-semibold leading-6 " 
id="availability-label">
+              <i18n.Translate>Confirm the operation</i18n.Translate>
+            </span>
+          </h2>
+          <span>
+            {subtitle}
+          </span>
+        </div>
+
+        <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl 
md:col-span-2">
+          <ChallengeDetails challenge={bankState.currentChallenge} 
onStart={startChallenge} />
+          {ch.info &&
+            <div class="mt-3 text-sm leading-6">
+              <form
+                class="bg-white shadow-sm ring-1 ring-gray-900/5"
+                autoCapitalize="none"
+                autoCorrect="off"
+                onSubmit={e => {
+                  e.preventDefault()
+                }}
+              >
+                <div class="px-4 py-6 sm:p-8">
+                  <label for="withdraw-amount">
+                    <i18n.Translate>Enter the confirmation 
code</i18n.Translate>
+                  </label>
+                  <div class="mt-2">
+                    <div class="relative rounded-md shadow-sm">
+                      <input
+                        type="text"
+                        // class="block w-full rounded-md border-0 py-1.5 
pl-16 text-gray-900 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"
+                        aria-describedby="answer"
+                        autoFocus
+                        class="block w-full 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={code ?? ""}
+                        required
+
+                        name="answer"
+                        id="answer"
+                        autocomplete="off"
+                        onChange={(e): void => {
+                          setCode(e.currentTarget.value)
+                        }}
+                      />
+                    </div>
+                    <ShowInputErrorLabel message={errors?.code} isDirty={code 
!== undefined} />
+                  </div>
+                </div>
+                <div class="flex items-center justify-between gap-x-6 border-t 
border-gray-900/10 px-4 py-4 sm:px-8">
+                  <button type="button"
+                    class="inline-flex items-center rounded-md bg-red-600 px-3 
py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 
focus-visible:outline-red-500"
+                    onClick={() => {
+                      updateBankState("currentChallenge", undefined)
+                      onContinue()
+                    }}
+                  >
+                    <i18n.Translate>Cancel</i18n.Translate>
+                  </button>
+                  <button type="submit"
+                    class="disabled:opacity-50 disabled:cursor-default 
cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 
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) => {
+                      completeChallenge()
+                    }}
+                  >
+                    <i18n.Translate>Confirm</i18n.Translate>
+                  </button>
+                </div>
+              </form>
+
+              {/* <ShouldBeSameUser username={details.username}> */}
+              {/* </ShouldBeSameUser> */}
+            </div>
+          }
+        </div>
+      </div>
+    </Fragment>
+
+  );
+}
+
+function ChallengeDetails({ challenge, onStart }: { challenge: 
ChallengeInProgess, onStart: () => void }): VNode {
+  const { i18n } = useTranslationContext();
+  const { config } = useBankCoreApiContext();
+
+  return <div class="px-4 mt-4 ">
+    <div class="w-full">
+      <div class="flex justify-center">
+
+        {challenge.info ?
+          <button type="submit"
+            class="disabled:opacity-50 disabled:cursor-default cursor-pointer 
rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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"
+            onClick={(e) => {
+              onStart()
+            }}
+          >
+            <i18n.Translate>Send again</i18n.Translate>
+          </button>
+          :
+          <button type="submit"
+            class="disabled:opacity-50 disabled:cursor-default cursor-pointer 
rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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"
+            onClick={(e) => {
+              onStart()
+            }}
+          >
+            <i18n.Translate>Send code</i18n.Translate>
+          </button>
+        }
+      </div>
+      <div class="mt-6 border-t border-gray-100">
+        <h2 class="text-base font-semibold leading-7 text-gray-900">
+          <span class="text-sm text-black font-semibold leading-6 " 
id="availability-label">
+            <i18n.Translate>Operation details</i18n.Translate>
+          </span>
+        </h2>
+        <dl class="divide-y divide-gray-100">
+          {((): VNode => {
+            switch (challenge.operation) {
+              case "delete-account": return <div class="px-4 py-2 sm:grid 
sm:grid-cols-3 sm:gap-4 sm:px-0">
+                <dt class="text-sm font-medium leading-6 
text-gray-900">Account</dt>
+                <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 
sm:mt-0">{challenge.request}</dd>
+              </div>
+              case "create-transaction": {
+                const payto = parsePaytoUri(challenge.request.payto_uri)!
+                return <Fragment>
+                  {challenge.request.amount &&
+                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                      <dt class="text-sm font-medium leading-6 
text-gray-900">Amount</dt>
+                      <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                        <RenderAmount 
value={Amounts.parseOrThrow(challenge.request.amount)} 
spec={config.currency_specification} />
+                      </dd>
+                    </div>
+                  }
+                  {payto.isKnown && payto.targetType === "iban" &&
+                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                      <dt class="text-sm font-medium leading-6 
text-gray-900">To account</dt>
+                      <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                        {payto.iban}
+                      </dd>
+                    </div>
+                  }
+                </Fragment>
+              }
+              case "confirm-withdrawal": return <ShowWithdrawalDetails 
id={challenge.request} />
+              case "create-cashout": {
+                return <ShowCashoutDetails request={challenge.request} />
+              }
+              case "update-account": {
+                return <Fragment>
+                  {challenge.request.cashout_payto_uri !== undefined &&
+                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                      <dt class="text-sm font-medium leading-6 
text-gray-900">Cashout account</dt>
+                      <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                        {challenge.request.cashout_payto_uri}
+                      </dd>
+                    </div>
+                  }
+                  {challenge.request.contact_data?.email !== undefined &&
+                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                      <dt class="text-sm font-medium leading-6 
text-gray-900">Email</dt>
+                      <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                        {challenge.request.contact_data?.email}
+                      </dd>
+                    </div>
+                  }
+                  {challenge.request.contact_data?.phone !== undefined &&
+                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                      <dt class="text-sm font-medium leading-6 
text-gray-900">Phone</dt>
+                      <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                        {challenge.request.contact_data?.phone}
+                      </dd>
+                    </div>
+                  }
+                  {challenge.request.debit_threshold !== undefined &&
+                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                      <dt class="text-sm font-medium leading-6 
text-gray-900">Debit threshold</dt>
+                      <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                        <RenderAmount 
value={Amounts.parseOrThrow(challenge.request.debit_threshold)} 
spec={config.currency_specification} />
+                      </dd>
+                    </div>
+                  }
+                  {challenge.request.is_public !== undefined &&
+                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                      <dt class="text-sm font-medium leading-6 
text-gray-900">Is public</dt>
+                      <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                        {challenge.request.is_public ? "enable" : "disable"}
+                      </dd>
+                    </div>
+                  }
+                  {challenge.request.name !== undefined &&
+                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                      <dt class="text-sm font-medium leading-6 
text-gray-900">Name</dt>
+                      <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                        {challenge.request.name}
+                      </dd>
+                    </div>
+                  }
+                  {challenge.request.tan_channel !== undefined &&
+                    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                      <dt class="text-sm font-medium leading-6 
text-gray-900">Authentication channel</dt>
+                      <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                        {challenge.request.tan_channel}
+                      </dd>
+                    </div>
+                  }
+                </Fragment>
+              }
+              case "update-password": {
+                return <Fragment>
+                  <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
+                    <dt class="text-sm font-medium leading-6 
text-gray-900">New password</dt>
+                    <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                      {challenge.request.new_password}
+                    </dd>
+                  </div>
+                </Fragment>
+              }
+              default: assertUnreachable(challenge)
+            }
+          })()}
+
+          {challenge.info &&
+            <h2 class="text-base font-semibold leading-7 text-gray-900">
+              <span class="text-sm text-black font-semibold leading-6 " 
id="availability-label">
+                <i18n.Translate>Challenge details</i18n.Translate>
+              </span>
+            </h2>
+          }
+          {challenge.sent.t_ms !== "never" &&
+            <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+              <dt class="text-sm font-medium leading-6 text-gray-900">Sent 
at</dt>
+              <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 
sm:mt-0">
+                {format(challenge.sent.t_ms, "dd/MM/yyyy HH:mm:ss")}
+              </dd>
+            </div>
+          }
+          {challenge.info &&
+            <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+              <dt class="text-sm font-medium leading-6 text-gray-900">
+                {((ch: TalerCorebankApi.TanChannel): VNode => {
+                  switch (ch) {
+                    case TalerCorebankApi.TanChannel.SMS: return 
<i18n.Translate>To phone</i18n.Translate>
+                    case TalerCorebankApi.TanChannel.EMAIL: return 
<i18n.Translate>To email</i18n.Translate>
+                    default: assertUnreachable(ch)
+                  }
+                })(challenge.info.tan_channel)}
+              </dt>
+              <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 
sm:mt-0">
+                {challenge.info.tan_info}
+              </dd>
+            </div>
+          }
+
+        </dl>
+      </div>
+    </div>
+  </div>
+}
+
+function ShowWithdrawalDetails({ id }: { id: string }): VNode {
+  const { i18n } = useTranslationContext();
+  const details = useWithdrawalDetails(id)
+  const { config } = useBankCoreApiContext();
+  if (!details) {
+    return <Loading />
+  }
+  if (details instanceof TalerError) {
+    return <ErrorLoadingWithDebug error={details} />
+  }
+  if (details.type === "fail") {
+    switch (details.case) {
+      case HttpStatusCode.BadRequest:
+      case HttpStatusCode.NotFound: return <OperationNotFound 
onClose={undefined} />
+      default: assertUnreachable(details)
+    }
+  }
+
+  return <Fragment>
+    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+      <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
+      <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+        <RenderAmount value={Amounts.parseOrThrow(details.body.amount)} 
spec={config.currency_specification} />
+      </dd>
+    </div>
+    {details.body.selected_reserve_pub !== undefined &&
+      <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+        <dt class="text-sm font-medium leading-6 text-gray-900">Withdraw 
id</dt>
+        <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0" 
title={details.body.selected_reserve_pub}>
+          {details.body.selected_reserve_pub.substring(0, 16)}...
+        </dd>
+      </div>
+    }
+    {details.body.selected_exchange_account !== undefined &&
+      <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+        <dt class="text-sm font-medium leading-6 text-gray-900">To account</dt>
+        <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+          {details.body.selected_exchange_account}
+        </dd>
+      </div>
+    }
+  </Fragment>
+}
+
+function ShowCashoutDetails({ request }: { request: 
TalerCorebankApi.CashoutRequest }): VNode {
+  const { i18n } = useTranslationContext();
+  const info = useConversionInfo();
+  if (!info) {
+    return <Loading />
+  }
+
+  if (info instanceof TalerError) {
+    return <ErrorLoadingWithDebug error={info} />
+  }
+  return <Fragment>
+    {request.subject !== undefined &&
+      <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+        <dt class="text-sm font-medium leading-6 text-gray-900">Subject</dt>
+        <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+          {request.subject}
+        </dd>
+      </div>
+    }
+    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+      <dt class="text-sm font-medium leading-6 text-gray-900">Debit</dt>
+      <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+        <RenderAmount value={Amounts.parseOrThrow(request.amount_credit)} 
spec={info.body.regional_currency_specification} />
+      </dd>
+    </div>
+    <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+      <dt class="text-sm font-medium leading-6 text-gray-900">Credit</dt>
+      <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+        <RenderAmount value={Amounts.parseOrThrow(request.amount_credit)} 
spec={info.body.fiat_currency_specification} />
+      </dd>
+    </div>
+  </Fragment>
+}
\ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx 
b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index 6e13ae657..c04e85e0c 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -235,10 +235,12 @@ export function WalletWithdrawForm({
   focus,
   limit,
   onCancel,
+  onAuthorizationRequired,
   goToConfirmOperation,
 }: {
   limit: AmountJson;
   focus?: boolean;
+  onAuthorizationRequired: () => void,
   goToConfirmOperation: (operationId: string) => void;
   onCancel: () => void;
 }): VNode {
@@ -274,6 +276,7 @@ export function WalletWithdrawForm({
         :
         <OperationState
           currency={limit.currency}
+          onAuthorizationRequired={onAuthorizationRequired}
           onClose={onCancel}
         />
       }
diff --git a/packages/demobank-ui/src/pages/WireTransfer.tsx 
b/packages/demobank-ui/src/pages/WireTransfer.tsx
index d6133b504..25d43a832 100644
--- a/packages/demobank-ui/src/pages/WireTransfer.tsx
+++ b/packages/demobank-ui/src/pages/WireTransfer.tsx
@@ -8,7 +8,12 @@ import { LoginForm } from "./LoginForm.js";
 import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
 import { assertUnreachable } from "./WithdrawalOperationPage.js";
 
-export function WireTransfer({ toAccount, onRegister, onCancel, onSuccess }: { 
onSuccess?: () => void; toAccount?: string, onCancel?: () => void, onRegister?: 
() => void }): VNode {
+export function WireTransfer({ toAccount, onAuthorizationRequired, onCancel, 
onSuccess }: {
+  onSuccess?: () => void;
+  toAccount?: string,
+  onCancel?: () => void,
+  onAuthorizationRequired: () => void,
+}): VNode {
   const { i18n } = useTranslationContext();
   const r = useBackendState();
   const account = r.state.status !== "loggedOut" ? r.state.username : "admin";
@@ -42,6 +47,7 @@ export function WireTransfer({ toAccount, onRegister, 
onCancel, onSuccess }: { o
       title={i18n.str`Make a wire transfer`}
       toAccount={toAccount}
       limit={limit}
+      onAuthorizationRequired={onAuthorizationRequired}
       onSuccess={() => {
         notifyInfo(i18n.str`Wire transfer created!`);
         if (onSuccess) onSuccess()
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx 
b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 206b51008..890478f82 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -15,40 +15,33 @@
  */
 
 import {
+  AbsoluteTime,
   AmountJson,
   HttpStatusCode,
   Logger,
   PaytoUri,
   PaytoUriIBAN,
   PaytoUriTalerBank,
-  TalerError,
   TalerErrorCode,
   TranslatedString,
   WithdrawUriResult
 } from "@gnu-taler/taler-util";
 import {
   Attention,
-  Loading,
   LocalNotificationBanner,
-  ShowInputErrorLabel,
   notifyInfo,
   useLocalNotification,
   useTranslationContext
 } from "@gnu-taler/web-util/browser";
 import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { useMemo, useState } from "preact/hooks";
 import { mutate } from "swr";
-import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
 import { useBankCoreApiContext } from "../context/config.js";
-import { useWithdrawalDetails } from "../hooks/access.js";
 import { useBackendState } from "../hooks/backend.js";
+import { useBankState } from "../hooks/bank-state.js";
 import { usePreferences } from "../hooks/preferences.js";
-import { undefinedIfEmpty } from "../utils.js";
 import { LoginForm } from "./LoginForm.js";
 import { RenderAmount } from "./PaytoWireTransferForm.js";
 import { assertUnreachable } from "./WithdrawalOperationPage.js";
-import { OperationNotFound } from "./WithdrawalQRCode.js";
-import { useBankState } from "../hooks/bank-state.js";
 
 const logger = new Logger("WithdrawalConfirmationQuestion");
 
@@ -60,7 +53,8 @@ interface Props {
     reserve: string,
     username: string,
     amount: AmountJson,
-  }
+  },
+  onAuthorizationRequired: () => void,
 }
 /**
  * Additional authentication required to complete the operation.
@@ -69,52 +63,20 @@ interface Props {
 export function WithdrawalConfirmationQuestion({
   onAborted,
   details,
+  onAuthorizationRequired,
   withdrawUri,
 }: Props): VNode {
   const { i18n } = useTranslationContext();
   const [settings] = usePreferences()
   const { state: credentials } = useBackendState();
   const creds = credentials.status !== "loggedIn" ? undefined : credentials
-  const withdrawalInfo = 
useWithdrawalDetails(withdrawUri.withdrawalOperationId)
   const [, updateBankState] = useBankState()
-  if (!withdrawalInfo) {
-    return <Loading />
-  }
-  if (withdrawalInfo instanceof TalerError) {
-    return <ErrorLoadingWithDebug error={withdrawalInfo} />
-  }
-  if (withdrawalInfo.type === "fail") {
-    switch (withdrawalInfo.case) {
-      case HttpStatusCode.NotFound: return <OperationNotFound 
onClose={onAborted} />
-      case HttpStatusCode.BadRequest: return <OperationNotFound 
onClose={onAborted} />
-      default: assertUnreachable(withdrawalInfo)
-    }
-  }
 
-  const captchaNumbers = useMemo(() => {
-    return {
-      a: Math.floor(Math.random() * 10),
-      b: Math.floor(Math.random() * 10),
-    };
-  }, []);
   const [notification, notify, handleError] = useLocalNotification()
 
   const { config, api } = useBankCoreApiContext()
-  const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
-  const answer = parseInt(captchaAnswer ?? "", 10);
-  const [busy, setBusy] = useState<Record<string, undefined>>()
-  const errors = undefinedIfEmpty({
-    answer: !captchaAnswer
-      ? i18n.str`Answer the question before continue`
-      : Number.isNaN(answer)
-        ? i18n.str`The answer should be a number`
-        : answer !== captchaNumbers.a + captchaNumbers.b
-          ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + 
${captchaNumbers.b}" is wrong.`
-          : undefined,
-  }) ?? busy;
 
   async function doTransfer() {
-    setBusy({})
     await handleError(async () => {
       if (!creds) return;
       const resp = await api.confirmWithdrawalById(creds, 
withdrawUri.withdrawalOperationId);
@@ -156,21 +118,21 @@ export function WithdrawalConfirmationQuestion({
             debug: resp.detail,
           })
           case HttpStatusCode.Accepted: {
-            updateBankState("currentChallengeId", resp.body.challenge_id)
-            return notify({
-              type: "info",
-              title: i18n.str`The operation needs a confirmation to complete.`,
-            });
+            updateBankState("currentChallenge", {
+              operation: "confirm-withdrawal",
+              id: String(resp.body.challenge_id),
+              sent: AbsoluteTime.never(),
+              request: withdrawUri.withdrawalOperationId,
+            })
+            return onAuthorizationRequired()
           }
           default: assertUnreachable(resp)
         }
       }
     })
-    setBusy(undefined)
   }
 
   async function doCancel() {
-    setBusy({})
     await handleError(async () => {
       if (!creds) return;
       const resp = await api.abortWithdrawalById(creds, 
withdrawUri.withdrawalOperationId);
@@ -200,7 +162,6 @@ export function WithdrawalConfirmationQuestion({
         }
       }
     })
-    setBusy(undefined)
   }
 
   return (
@@ -215,10 +176,7 @@ export function WithdrawalConfirmationQuestion({
           <div class="mt-3 text-sm leading-6">
 
             <ShouldBeSameUser username={details.username}>
-              <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">
-                <div class="px-4 sm:px-0">
-                  <h2 class="text-base font-semibold 
text-gray-900"><i18n.Translate>Answer the next question to authorize the wire 
transfer.</i18n.Translate></h2>
-                </div>
+              <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 
md:grid-cols-2 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
                 <form
                   class="bg-white shadow-sm ring-1 ring-gray-900/5 
sm:rounded-xl md:col-span-2"
                   autoCapitalize="none"
@@ -227,35 +185,65 @@ export function WithdrawalConfirmationQuestion({
                     e.preventDefault()
                   }}
                 >
-                  <div class="px-4 py-6 sm:p-8">
-                    <label for="withdraw-amount">{i18n.str`What is`}&nbsp;
-                      <em>
-                        {captchaNumbers.a}&nbsp;+&nbsp;{captchaNumbers.b}
-                      </em>
-                      ?
-                    </label>
-
-                    <div class="mt-2">
-                      <div class="relative rounded-md shadow-sm">
-                        <input
-                          type="text"
-                          // class="block w-full rounded-md border-0 py-1.5 
pl-16 text-gray-900 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"
-                          aria-describedby="answer"
-                          autoFocus
-                          class="block w-full 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={captchaAnswer ?? ""}
-                          required
+                  <div class="px-4 mt-4">
+                    <div class="w-full">
+                      <div class="px-4 sm:px-0 text-sm">
+                        <p><i18n.Translate>Wire transfer 
details</i18n.Translate></p>
+                      </div>
+                      <div class="mt-6 border-t border-gray-100">
+                        <dl class="divide-y divide-gray-100">
+                          {((): VNode => {
+                            switch (details.account.targetType) {
+                              case "iban": {
+                                const p = details.account as PaytoUriIBAN
+                                const name = p.params["receiver-name"]
+                                return <Fragment>
+                                  <div class="px-4 py-2 sm:grid sm:grid-cols-3 
sm:gap-4 sm:px-0">
+                                    <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange account</dt>
+                                    <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
+                                  </div>
+                                  {name &&
+                                    <div class="px-4 py-2 sm:grid 
sm:grid-cols-3 sm:gap-4 sm:px-0">
+                                      <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange name</dt>
+                                      <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+                                    </div>
+                                  }
+                                </Fragment>
+                              }
+                              case "x-taler-bank": {
+                                const p = details.account as PaytoUriTalerBank
+                                const name = p.params["receiver-name"]
+                                return <Fragment>
+                                  <div class="px-4 py-2 sm:grid sm:grid-cols-3 
sm:gap-4 sm:px-0">
+                                    <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange account</dt>
+                                    <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
+                                  </div>
+                                  {name &&
+                                    <div class="px-4 py-2 sm:grid 
sm:grid-cols-3 sm:gap-4 sm:px-0">
+                                      <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange name</dt>
+                                      <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+                                    </div>
+                                  }
+                                </Fragment>
+                              }
+                              default:
+                                return <div class="px-4 py-2 sm:grid 
sm:grid-cols-3 sm:gap-4 sm:px-0">
+                                  <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange account</dt>
+                                  <dd class="mt-1 text-sm leading-6 
text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
+                                </div>
 
-                          name="answer"
-                          id="answer"
-                          autocomplete="off"
-                          onChange={(e): void => {
-                            setCaptchaAnswer(e.currentTarget.value)
-                          }}
-                        />
+                            }
+                          })()}
+                          <div class="px-4 py-2 sm:grid sm:grid-cols-3 
sm:gap-4 sm:px-0">
+                            <dt class="text-sm font-medium leading-6 
text-gray-900">Amount</dt>
+                            <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
+                              <RenderAmount value={details.amount} 
spec={config.currency_specification} />
+                            </dd>
+                          </div>
+                        </dl>
                       </div>
-                      <ShowInputErrorLabel message={errors?.answer} 
isDirty={captchaAnswer !== undefined} />
                     </div>
+
                   </div>
 
                   <div class="flex items-center justify-between gap-x-6 
border-t border-gray-900/10 px-4 py-4 sm:px-8">
@@ -265,7 +253,6 @@ export function WithdrawalConfirmationQuestion({
                       <i18n.Translate>Cancel</i18n.Translate></button>
                     <button type="submit"
                       class="disabled:opacity-50 disabled:cursor-default 
cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 
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) => {
                         e.preventDefault()
                         doTransfer()
@@ -279,66 +266,6 @@ export function WithdrawalConfirmationQuestion({
               </div>
             </ShouldBeSameUser>
           </div>
-          <div class="px-4 mt-4 ">
-            <div class="w-full">
-              <div class="px-4 sm:px-0 text-sm">
-                <p><i18n.Translate>Wire transfer details</i18n.Translate></p>
-              </div>
-              <div class="mt-6 border-t border-gray-100">
-                <dl class="divide-y divide-gray-100">
-                  {((): VNode => {
-                    switch (details.account.targetType) {
-                      case "iban": {
-                        const p = details.account as PaytoUriIBAN
-                        const name = p.params["receiver-name"]
-                        return <Fragment>
-                          <div class="px-4 py-2 sm:grid sm:grid-cols-3 
sm:gap-4 sm:px-0">
-                            <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange account</dt>
-                            <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">{p.iban}</dd>
-                          </div>
-                          {name &&
-                            <div class="px-4 py-2 sm:grid sm:grid-cols-3 
sm:gap-4 sm:px-0">
-                              <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange name</dt>
-                              <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
-                            </div>
-                          }
-                        </Fragment>
-                      }
-                      case "x-taler-bank": {
-                        const p = details.account as PaytoUriTalerBank
-                        const name = p.params["receiver-name"]
-                        return <Fragment>
-                          <div class="px-4 py-2 sm:grid sm:grid-cols-3 
sm:gap-4 sm:px-0">
-                            <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange account</dt>
-                            <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">{p.account}</dd>
-                          </div>
-                          {name &&
-                            <div class="px-4 py-2 sm:grid sm:grid-cols-3 
sm:gap-4 sm:px-0">
-                              <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange name</dt>
-                              <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
-                            </div>
-                          }
-                        </Fragment>
-                      }
-                      default:
-                        return <div class="px-4 py-2 sm:grid sm:grid-cols-3 
sm:gap-4 sm:px-0">
-                          <dt class="text-sm font-medium leading-6 
text-gray-900">Exchange account</dt>
-                          <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
-                        </div>
-
-                    }
-                  })()}
-                  <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
-                    <dt class="text-sm font-medium leading-6 
text-gray-900">Amount</dt>
-                    <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
-                      <RenderAmount value={details.amount} 
spec={config.currency_specification} />
-                    </dd>
-                  </div>
-                </dl>
-              </div>
-            </div>
-
-          </div>
         </div>
       </div>
 
diff --git a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx 
b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
index 4bb3b4d7b..7ed5e4b0a 100644
--- a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
@@ -32,8 +32,10 @@ const logger = new Logger("AccountPage");
 
 export function WithdrawalOperationPage({
   operationId,
+  onAuthorizationRequired,
   onContinue,
 }: {
+  onAuthorizationRequired: () => void;
   operationId: string;
   onContinue: () => void;
 }): VNode {
@@ -56,6 +58,7 @@ export function WithdrawalOperationPage({
   return (
     <WithdrawalQRCode
       withdrawUri={parsedUri}
+      onAuthorizationRequired={onAuthorizationRequired}
       onClose={() => {
         updateBankState("currentWithdrawalOperationId", undefined)
         onContinue()
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx 
b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index f05f183d4..97bc9f61f 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -35,6 +35,8 @@ const logger = new Logger("WithdrawalQRCode");
 interface Props {
   withdrawUri: WithdrawUriResult;
   onClose: () => void;
+  onAuthorizationRequired: () => void,
+
 }
 /**
  * Offer the QR code (and a clickable taler://-link) to
@@ -44,6 +46,7 @@ interface Props {
 export function WithdrawalQRCode({
   withdrawUri,
   onClose,
+  onAuthorizationRequired,
 }: Props): VNode {
   const { i18n } = useTranslationContext();
   const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
@@ -164,6 +167,7 @@ export function WithdrawalQRCode({
         reserve: data.selected_reserve_pub,
         amount: Amounts.parseOrThrow(data.amount)
       }}
+      onAuthorizationRequired={onAuthorizationRequired}
       onAborted={() => {
         notifyInfo(i18n.str`Operation canceled`);
         onClose()
@@ -173,7 +177,7 @@ export function WithdrawalQRCode({
 }
 
 
-export function OperationNotFound({ onClose }: { onClose: () => void }): VNode 
{
+export function OperationNotFound({ onClose }: { onClose: (() => void) | 
undefined }): VNode {
   const { i18n } = useTranslationContext();
   return <div class="relative ml-auto mr-auto transform overflow-hidden 
rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 
sm:w-full sm:max-w-sm sm:p-6">
     <div>
@@ -197,15 +201,17 @@ export function OperationNotFound({ onClose }: { onClose: 
() => void }): VNode {
         </div>
       </div>
     </div>
-    <div class="mt-5 sm:mt-6">
-      <button type="button"
-        class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 
py-2 text-sm font-semibold 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"
-        onClick={async (e) => {
-          e.preventDefault();
-          onClose()
-        }}>
-        <i18n.Translate>Cotinue to dashboard</i18n.Translate>
-      </button>
-    </div>
+    {onClose &&
+      <div class="mt-5 sm:mt-6">
+        <button type="button"
+          class="inline-flex w-full justify-center rounded-md bg-indigo-600 
px-3 py-2 text-sm font-semibold 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"
+          onClick={async (e) => {
+            e.preventDefault();
+            onClose()
+          }}>
+          <i18n.Translate>Cotinue to dashboard</i18n.Translate>
+        </button>
+      </div>
+    }
   </div>
 }
\ No newline at end of file
diff --git a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx 
b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
index f2972ed65..1676d8b6a 100644
--- a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
+++ b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -9,10 +9,11 @@ import { CreateCashout } from "../business/CreateCashout.js";
 interface Props {
   account: string,
   onClose: () => void,
+  onAuthorizationRequired: () => void,
   onSelected: (cid: number) => void
 }
 
-export function CashoutListForAccount({ account, onSelected, onClose }: 
Props): VNode {
+export function CashoutListForAccount({ account, onAuthorizationRequired, 
onSelected, onClose }: Props): VNode {
   const { i18n } = useTranslationContext();
 
   const { state: credentials } = useBackendState();
@@ -29,7 +30,7 @@ export function CashoutListForAccount({ account, onSelected, 
onClose }: Props):
       </h1>
     }
 
-    <CreateCashout focus onCancel={onClose} onComplete={() => { }} 
account={account} />
+    <CreateCashout focus onCancel={onClose} 
onAuthorizationRequired={onAuthorizationRequired} account={account} />
 
     <Cashouts
       account={account}
diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx 
b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
index 28875bde6..ca3e2fbdf 100644
--- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -1,4 +1,4 @@
-import { HttpStatusCode, TalerCorebankApi, TalerError, TalerErrorCode, 
TranslatedString } from "@gnu-taler/taler-util";
+import { AbsoluteTime, HttpStatusCode, TalerCorebankApi, TalerError, 
TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
 import { Loading, LocalNotificationBanner, notifyInfo, useLocalNotification, 
useTranslationContext } from "@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
@@ -16,9 +16,11 @@ export function ShowAccountDetails({
   account,
   onClear,
   onUpdateSuccess,
+  onAuthorizationRequired,
 }: {
   onClear?: () => void;
   onUpdateSuccess: () => void;
+  onAuthorizationRequired: () => void,
   account: string;
 }): VNode {
   const { i18n } = useTranslationContext();
@@ -54,7 +56,6 @@ export function ShowAccountDetails({
       const resp = await api.updateAccount({
         token: creds.token,
         username: account,
-
       }, submitAccount);
 
       if (resp.type === "ok") {
@@ -99,11 +100,13 @@ export function ShowAccountDetails({
             debug: resp.detail,
           })
           case HttpStatusCode.Accepted: {
-            updateBankState("currentChallengeId", resp.body.challenge_id)
-            return notify({
-              type: "info",
-              title: i18n.str`Cashout created but confirmation is required.`,
-            });
+            updateBankState("currentChallenge", {
+              operation: "update-account",
+              id: String(resp.body.challenge_id),
+              sent: AbsoluteTime.never(),
+              request: submitAccount,
+            })
+            return onAuthorizationRequired()
           }
           case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: {
             return notify({
@@ -122,7 +125,7 @@ export function ShowAccountDetails({
 
   return (
     <Fragment>
-      <LocalNotificationBanner notification={notification} />
+      <LocalNotificationBanner notification={notification} showDebug={true} />
       {accountIsTheCurrentUser ?
         <ProfileNavigation current="details" />
         :
diff --git a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx 
b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
index 0ff1cf725..3c4a865ed 100644
--- a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -9,17 +9,19 @@ import { doAutoFocus } from "../PaytoWireTransferForm.js";
 import { ProfileNavigation } from "../ProfileNavigation.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
 import { LocalNotificationBanner } from "@gnu-taler/web-util/browser";
-import { HttpStatusCode, TalerErrorCode } from "@gnu-taler/taler-util";
+import { AbsoluteTime, HttpStatusCode, TalerErrorCode } from 
"@gnu-taler/taler-util";
 import { useBankState } from "../../hooks/bank-state.js";
 
 export function UpdateAccountPassword({
   account: accountName,
   onCancel,
   onUpdateSuccess,
+  onAuthorizationRequired,
   focus,
 }: {
   onCancel: () => void;
   focus?: boolean,
+  onAuthorizationRequired: () => void,
   onUpdateSuccess: () => void;
   account: string;
 }): VNode {
@@ -51,10 +53,11 @@ export function UpdateAccountPassword({
   async function doChangePassword() {
     if (!!errors || !password || !token) return;
     await handleError(async () => {
-      const resp = await api.updatePassword({ username: accountName, token }, {
+      const request = {
         old_password: current,
         new_password: password,
-      });
+      }
+      const resp = await api.updatePassword({ username: accountName, token }, 
request);
       if (resp.type === "ok") {
         notifyInfo(i18n.str`Password changed`);
         onUpdateSuccess();
@@ -77,11 +80,13 @@ export function UpdateAccountPassword({
             title: i18n.str`Your current password doesn't match, can't change 
to a new password.`
           })
           case HttpStatusCode.Accepted: {
-            updateBankState("currentChallengeId", resp.body.challenge_id)
-            return notify({
-              type: "info",
-              title: i18n.str`Cashout created but confirmation is required.`,
-            });
+            updateBankState("currentChallenge", {
+              operation: "update-password",
+              id: String(resp.body.challenge_id),
+              sent: AbsoluteTime.never(),
+              request,
+            })
+            return onAuthorizationRequired()
           }
           default: assertUnreachable(resp)
         }
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx 
b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 859c04396..7296e7744 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -1,9 +1,9 @@
 import { AmountString, Amounts, PaytoString, TalerCorebankApi, 
TranslatedString, buildPayto, parsePaytoUri, stringifyPaytoUri } from 
"@gnu-taler/taler-util";
-import { CopyButton, ShowInputErrorLabel, useTranslationContext } from 
"@gnu-taler/web-util/browser";
+import { Attention, CopyButton, ShowInputErrorLabel, useTranslationContext } 
from "@gnu-taler/web-util/browser";
 import { ComponentChildren, Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
-import { useBankCoreApiContext } from "../../context/config.js";
-import { ErrorMessageMappingFor, PartialButDefined, WithIntermediate, 
undefinedIfEmpty, validateIBAN } from "../../utils.js";
+import { VersionHint, useBankCoreApiContext } from "../../context/config.js";
+import { ErrorMessageMappingFor, PartialButDefined, TanChannel, 
WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
 import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
 import { getRandomPassword } from "../rnd.js";
@@ -24,6 +24,7 @@ export type AccountFormData = {
   cashout_payto_uri?: string,
   email?: string,
   phone?: string,
+  tan_channel?: TanChannel | "remove",
 }
 
 type ChangeByPurposeType = {
@@ -55,7 +56,7 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
   onChange: ChangeByPurposeType[PurposeType];
   purpose: PurposeType;
 }): VNode {
-  const { config } = useBankCoreApiContext()
+  const { config, hints } = useBankCoreApiContext()
   const { i18n } = useTranslationContext();
   const { state: credentials } = useBackendState();
   const [form, setForm] = useState<AccountFormData>({});
@@ -75,8 +76,11 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
     email: template?.contact_data?.email ?? "",
     phone: template?.contact_data?.phone ?? "",
     username: username ?? "",
+    tan_channel: template?.tan_channel,
   }
 
+  const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1
+
   const showingCurrentUserInfo = credentials.status !== "loggedIn" ? false : 
username === credentials.username
   const userIsAdmin = credentials.status !== "loggedIn" ? false : 
credentials.isUserAdministrator
 
@@ -86,6 +90,9 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
   const editableThreshold = userIsAdmin && (purpose === "create" || purpose 
=== "update")
   const editableAccount = purpose === "create" && userIsAdmin
 
+  const hasPhone = !!defaultValue.phone || !!form.phone
+  const hasEmail = !!defaultValue.email || !!form.email
+
   function updateForm(newForm: typeof defaultValue): void {
     const cashoutParsed = !newForm.cashout_payto_uri
       ? undefined
@@ -173,6 +180,8 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
             payto_uri: internalURI,
             is_public: !!newForm.isPublic,
             is_taler_exchange: !!newForm.isExchange,
+            // @ts-ignore
+            tan_channel: newForm.tan_channel === "remove" ? null : 
newForm.tan_channel,
           }
           callback(result)
           return;
@@ -190,6 +199,8 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
             debit_threshold: threshold,
             is_public: !!newForm.isPublic,
             name: newForm.name,
+            // @ts-ignore
+            tan_channel: newForm?.tan_channel === "remove" ? null : 
newForm.tan_channel,
           }
           callback(result)
           return;
@@ -409,7 +420,87 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
                   <span aria-hidden="true" data-enabled={form.isExchange ?? 
defaultValue.isExchange ? "true" : "false"} class="translate-x-5 
data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 
transform rounded-full bg-white shadow ring-0 transition duration-200 
ease-in-out"></span>
                 </button>
               </div>
-            </div>}
+            </div>
+          }
+          {/* channel, not shown if old cashout api */}
+          {OLD_CASHOUT_API ? undefined : config.supported_tan_channels.length 
=== 0 ?
+            <div class="sm:col-span-5">
+              <Attention type="warning" title={i18n.str`No cashout channel 
available`}>
+                <i18n.Translate>
+                  This server doesn't support second factor authentication.
+                </i18n.Translate>
+              </Attention>
+            </div>
+            :
+            <div class="sm:col-span-5">
+              <label
+                class="block text-sm font-medium leading-6 text-gray-900"
+                for="channel"
+              >
+                {i18n.str`Confirmation the operation using`}
+              </label>
+              <div class="mt-2 max-w-xl text-sm text-gray-500">
+                <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
+                  {config.supported_tan_channels.indexOf(TanChannel.EMAIL) === 
-1 ? undefined :
+                    <label onClick={(e) => {
+                      if (!hasEmail) return;
+                      if (form.tan_channel === TanChannel.EMAIL) {
+                        form.tan_channel = "remove"
+                      } else {
+                        form.tan_channel = TanChannel.EMAIL
+                      }
+                      updateForm(structuredClone(form))
+                      e.preventDefault()
+                    }} data-disabled={purpose === "show" || !hasEmail} 
data-selected={(form.tan_channel ?? defaultValue.tan_channel) === 
TanChannel.EMAIL}
+                      class="relative flex 
data-[disabled=false]:cursor-pointer rounded-lg border bg-white 
data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none 
border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600">
+                      <input type="radio" name="channel" value="Newsletter" 
class="sr-only" />
+                      <span class="flex flex-1">
+                        <span class="flex flex-col">
+                          <span id="project-type-0-label" class="block text-sm 
font-medium text-gray-900 ">
+                            <i18n.Translate>Email</i18n.Translate>
+                          </span>
+                          {purpose !== "show" && !hasEmail && i18n.str`add a 
email in your profile to enable this option`}
+                        </span>
+                      </span>
+                      <svg data-selected={(form.tan_channel ?? 
defaultValue.tan_channel) === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 
data-[selected=false]: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>
+                    </label>
+                  }
+
+                  {config.supported_tan_channels.indexOf(TanChannel.SMS) === 
-1 ? undefined :
+                    <label onClick={(e) => {
+                      if (!hasPhone) return;
+                      if (form.tan_channel === TanChannel.SMS) {
+                        form.tan_channel = "remove"
+                      } else {
+                        form.tan_channel = TanChannel.SMS
+                      }
+                      updateForm(structuredClone(form))
+                      e.preventDefault()
+                    }} data-disabled={purpose === "show" || !hasPhone} 
data-selected={(form.tan_channel ?? defaultValue.tan_channel) === 
TanChannel.SMS}
+                      class="relative flex 
data-[disabled=false]:cursor-pointer rounded-lg border 
data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none 
border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600">
+                      <input type="radio" name="channel" value="Existing 
Customers" class="sr-only" />
+                      <span class="flex flex-1">
+                        <span class="flex flex-col">
+                          <span id="project-type-1-label" class="block text-sm 
font-medium text-gray-900">
+                            <i18n.Translate>SMS</i18n.Translate>
+                          </span>
+                          {purpose !== "show" && !hasPhone && i18n.str`add a 
phone number in your profile to enable this option`}
+                        </span>
+                      </span>
+                      <svg data-selected={(form.tan_channel ?? 
defaultValue.tan_channel) === TanChannel.SMS} class="h-5 w-5 text-indigo-600 
data-[selected=false]: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>
+                    </label>
+                  }
+                  <pre>
+                    {JSON.stringify(form, undefined, 2)}
+                  </pre>
+                </div>
+              </div>
+            </div>
+          }
 
           <div class="sm:col-span-5">
             <div class="flex items-center justify-between">
@@ -434,9 +525,6 @@ export function AccountForm<PurposeType extends keyof 
ChangeByPurposeType>({
 
         </div>
       </div>
-      <pre>
-        {JSON.stringify(errors, undefined, 2)}
-      </pre>
       {children}
     </form>
   );
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx 
b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
index 82a341dbe..f5bce1396 100644
--- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -16,18 +16,17 @@ import { AccountList } from "./AccountList.js";
  * Query account information and show QR code if there is pending withdrawal
  */
 interface Props {
-  onRegister: () => void;
-
   onCreateAccount: () => void;
   onShowAccountDetails: (aid: string) => void;
   onRemoveAccount: (aid: string) => void;
   onUpdateAccountPassword: (aid: string) => void;
   onShowCashoutForAccount: (aid: string) => void;
+  onAuthorizationRequired: () => void;
 }
-export function AdminHome({ onCreateAccount, onRegister, onRemoveAccount, 
onShowAccountDetails, onShowCashoutForAccount, onUpdateAccountPassword }: 
Props): VNode {
+export function AdminHome({ onCreateAccount, onAuthorizationRequired, 
onRemoveAccount, onShowAccountDetails, onShowCashoutForAccount, 
onUpdateAccountPassword }: Props): VNode {
   return <Fragment>
     <Metrics />
-    <WireTransfer onRegister={onRegister} />
+    <WireTransfer onAuthorizationRequired={onAuthorizationRequired} />
 
     <Transactions account="admin" />
     <AccountList
@@ -184,11 +183,11 @@ function Metrics(): VNode {
       </div>
     </dl>
     <div class="flex justify-end mt-2">
-      <a href="#/download-stats" 
-              class="disabled:opacity-50 disabled:cursor-default 
cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 
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"
-              ><i18n.Translate>
-        download stats as csv
-      </i18n.Translate></a>
+      <a href="#/download-stats"
+        class="disabled:opacity-50 disabled:cursor-default cursor-pointer 
rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold 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"
+      ><i18n.Translate>
+          download stats as csv
+        </i18n.Translate></a>
     </div>
   </Fragment>
 
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx 
b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index 3f7d62935..beadad957 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -1,4 +1,4 @@
-import { Amounts, HttpStatusCode, TalerError, TalerErrorCode, TranslatedString 
} from "@gnu-taler/taler-util";
+import { AbsoluteTime, Amounts, HttpStatusCode, TalerError, TalerErrorCode, 
TranslatedString } from "@gnu-taler/taler-util";
 import { Attention, Loading, LocalNotificationBanner, ShowInputErrorLabel, 
notifyInfo, useLocalNotification, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { Fragment, VNode, h } from "preact";
 import { useState } from "preact/hooks";
@@ -16,9 +16,11 @@ export function RemoveAccount({
   account,
   onCancel,
   onUpdateSuccess,
+  onAuthorizationRequired,
   focus,
 }: {
   focus?: boolean;
+  onAuthorizationRequired: () => void,
   onCancel: () => void;
   onUpdateSuccess: () => void;
   account: string;
@@ -92,11 +94,13 @@ export function RemoveAccount({
             debug: resp.detail,
           })
           case HttpStatusCode.Accepted: {
-            updateBankState("currentChallengeId", resp.body.challenge_id)
-            return notify({
-              type: "info",
-              title: i18n.str`The operation needs a confirmation to complete.`,
-            });
+            updateBankState("currentChallenge", {
+              operation: "delete-account",
+              id: String(resp.body.challenge_id),
+              sent: AbsoluteTime.never(),
+              request: account,
+            })
+            return onAuthorizationRequired()
           }
           default: {
             assertUnreachable(resp)
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx 
b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index d97a00a2e..e4fda8fb6 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -14,6 +14,7 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 import {
+  AbsoluteTime,
   Amounts,
   HttpStatusCode,
   TalerCorebankApi,
@@ -36,7 +37,7 @@ import {
 import { Fragment, VNode, h } from "preact";
 import { useEffect, useState } from "preact/hooks";
 import { ErrorLoadingWithDebug } from 
"../../components/ErrorLoadingWithDebug.js";
-import { useBankCoreApiContext } from "../../context/config.js";
+import { VersionHint, useBankCoreApiContext } from "../../context/config.js";
 import { useAccountDetails } from "../../hooks/access.js";
 import { useBackendState } from "../../hooks/backend.js";
 import {
@@ -55,7 +56,7 @@ import { useBankState } from "../../hooks/bank-state.js";
 interface Props {
   account: string;
   focus?: boolean,
-  onComplete: (id: string) => void;
+  onAuthorizationRequired: () => void,
   onCancel?: () => void;
 }
 
@@ -72,7 +73,7 @@ type ErrorFrom<T> = {
 
 export function CreateCashout({
   account: accountName,
-  onComplete,
+  onAuthorizationRequired,
   focus,
   onCancel,
 }: Props): VNode {
@@ -86,7 +87,7 @@ export function CreateCashout({
   const creds = credentials.status !== "loggedIn" ? undefined : credentials
   const [, updateBankState] = useBankState()
 
-  const { api, config } = useBankCoreApiContext()
+  const { api, config, hints } = useBankCoreApiContext()
   const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, });
   const [notification, notify, handleError] = useLocalNotification()
   const info = useConversionInfo();
@@ -96,6 +97,9 @@ export function CreateCashout({
       <i18n.Translate>The bank configuration does not support cashout 
operations.</i18n.Translate>
     </Attention>
   }
+
+  const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1
+
   if (!resultAccount) {
     return <Loading />
   }
@@ -179,33 +183,37 @@ export function CreateCashout({
             : Amounts.isZero(calc.credit)
               ? i18n.str`the total transfer at destination will be zero`
               : undefined,
-    channel: !form.channel ? i18n.str`required` : undefined,
+    channel: OLD_CASHOUT_API && !form.channel ? i18n.str`required` : undefined,
   });
   const trimmedAmountStr = form.amount?.trim();
 
   async function createCashout() {
     const request_uid = encodeCrock(getRandomBytes(32))
     await handleError(async () => {
-      const validChannel = config.supported_tan_channels.length === 0 || 
form.channel
+      //new cashout api doesn't require channel
+      const validChannel = !OLD_CASHOUT_API || 
config.supported_tan_channels.length === 0 || form.channel
 
       if (!creds || !form.subject || !validChannel) return;
-      const resp = await api.createCashout(creds, {
+      const request = {
         request_uid,
         amount_credit: Amounts.stringify(calc.credit),
         amount_debit: Amounts.stringify(calc.debit),
         subject: form.subject,
         tan_channel: form.channel,
-      })
+      }
+      const resp = await api.createCashout(creds, request)
       if (resp.type === "ok") {
         notifyInfo(i18n.str`Cashout created`)
       } else {
         switch (resp.case) {
           case HttpStatusCode.Accepted: {
-            updateBankState("currentChallengeId", resp.body.challenge_id)
-            return notify({
-              type: "info",
-              title: i18n.str`Cashout created but confirmation is required.`,
-            });
+            updateBankState("currentChallenge", {
+              operation: "create-cashout",
+              id: String(resp.body.challenge_id),
+              sent: AbsoluteTime.never(),
+              request,
+            })
+            return onAuthorizationRequired()
           }
           case HttpStatusCode.NotFound: return notify({
             type: "error",
@@ -444,8 +452,8 @@ export function CreateCashout({
                 </div>
               )}
 
-              {/* channel */}
-              {config.supported_tan_channels.length === 0 ?
+              {/* channel, not shown if new cashout api */}
+              {!OLD_CASHOUT_API ? undefined : 
config.supported_tan_channels.length === 0 ?
                 <div class="sm:col-span-5">
                   <Attention type="warning" title={i18n.str`No cashout channel 
available`}>
                     <i18n.Translate>
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx 
b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
index 5d8db5aee..b517a7d42 100644
--- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -93,6 +93,9 @@ export function ShowCashoutDetails({
   const errors = undefinedIfEmpty({
     code: !code ? i18n.str`required` : undefined,
   });
+  /**
+   * @deprecated
+   */
   const isPending = String(result.body.status).toUpperCase() === "PENDING";
   const { fiat_currency_specification, regional_currency_specification } = 
info.body
   // won't implement in retry in old API 3:0:3 since request_uid is missing
@@ -266,7 +269,6 @@ export function ShowCashoutDetails({
 
         {!isPending ? undefined :
           <Fragment>
-
             <div />
             <form
               class="bg-white shadow-sm ring-1 ring-gray-900/5"
@@ -318,7 +320,6 @@ export function ShowCashoutDetails({
                   <i18n.Translate>Confirm</i18n.Translate>
                 </button>
               </div>
-
             </form>
           </Fragment>}
       </div>
diff --git a/packages/taler-util/src/http-client/bank-core.ts 
b/packages/taler-util/src/http-client/bank-core.ts
index 50cedefa9..dbb6c7112 100644
--- a/packages/taler-util/src/http-client/bank-core.ts
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -122,12 +122,13 @@ export class TalerCoreBankHttpClient {
    * https://docs.taler.net/core/api-corebank.html#delete--accounts-$USERNAME
    * 
    */
-  async deleteAccount(auth: UserAndToken) {
+  async deleteAccount(auth: UserAndToken, cid?: string) {
     const url = new URL(`accounts/${auth.username}`, this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "DELETE",
       headers: {
-        Authorization: makeBearerTokenAuthHeader(auth.token)
+        Authorization: makeBearerTokenAuthHeader(auth.token),
+        "X-Challenge-Id": cid,
       },
     });
     switch (resp.status) {
@@ -152,13 +153,14 @@ export class TalerCoreBankHttpClient {
    * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME
    * 
    */
-  async updateAccount(auth: UserAndToken, body: 
TalerCorebankApi.AccountReconfiguration) {
+  async updateAccount(auth: UserAndToken, body: 
TalerCorebankApi.AccountReconfiguration, cid?: string) {
     const url = new URL(`accounts/${auth.username}`, this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "PATCH",
       body,
       headers: {
-        Authorization: makeBearerTokenAuthHeader(auth.token)
+        Authorization: makeBearerTokenAuthHeader(auth.token),
+        "X-Challenge-Id": cid,
       },
     });
     switch (resp.status) {
@@ -186,13 +188,14 @@ export class TalerCoreBankHttpClient {
    * 
https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME-auth
    * 
    */
-  async updatePassword(auth: UserAndToken, body: 
TalerCorebankApi.AccountPasswordChange) {
+  async updatePassword(auth: UserAndToken, body: 
TalerCorebankApi.AccountPasswordChange, cid?: string) {
     const url = new URL(`accounts/${auth.username}/auth`, this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "PATCH",
       body,
       headers: {
-        Authorization: makeBearerTokenAuthHeader(auth.token)
+        Authorization: makeBearerTokenAuthHeader(auth.token),
+        "X-Challenge-Id": cid,
       },
     });
     switch (resp.status) {
@@ -328,12 +331,13 @@ export class TalerCoreBankHttpClient {
    * 
https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-transactions
    * 
    */
-  async createTransaction(auth: UserAndToken, body: 
TalerCorebankApi.CreateTransactionRequest) {
+  async createTransaction(auth: UserAndToken, body: 
TalerCorebankApi.CreateTransactionRequest, cid?: string) {
     const url = new URL(`accounts/${auth.username}/transactions`, 
this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "POST",
       headers: {
-        Authorization: makeBearerTokenAuthHeader(auth.token)
+        Authorization: makeBearerTokenAuthHeader(auth.token),
+        "X-Challenge-Id": cid,
       },
       body,
     });
@@ -409,12 +413,13 @@ export class TalerCoreBankHttpClient {
    * 
https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-confirm
    * 
    */
-  async confirmWithdrawalById(auth: UserAndToken, wid: string) {
+  async confirmWithdrawalById(auth: UserAndToken, wid: string, cid?: string) {
     const url = new 
URL(`accounts/${auth.username}/withdrawals/${wid}/confirm`, this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "POST",
       headers: {
-        Authorization: makeBearerTokenAuthHeader(auth.token)
+        Authorization: makeBearerTokenAuthHeader(auth.token),
+        "X-Challenge-Id": cid,
       },
     });
     switch (resp.status) {
@@ -470,12 +475,13 @@ export class TalerCoreBankHttpClient {
    * 
https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts
    * 
    */
-  async createCashout(auth: UserAndToken, body: 
TalerCorebankApi.CashoutRequest) {
+  async createCashout(auth: UserAndToken, body: 
TalerCorebankApi.CashoutRequest, cid?: string) {
     const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "POST",
       headers: {
-        Authorization: makeBearerTokenAuthHeader(auth.token)
+        Authorization: makeBearerTokenAuthHeader(auth.token),
+        "X-Challenge-Id": cid,
       },
       body,
     });
@@ -685,13 +691,14 @@ export class TalerCoreBankHttpClient {
     }
   }
 
-  async confirmChallenge(auth: UserAndToken, cid: string) {
+  async confirmChallenge(auth: UserAndToken, cid: string, body: 
TalerCorebankApi.ChallengeSolve) {
     const url = new URL(`accounts/${auth.username}/challenge/${cid}/confirm`, 
this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "POST",
       headers: {
         Authorization: makeBearerTokenAuthHeader(auth.token)
       },
+      body,
     });
     switch (resp.status) {
       case HttpStatusCode.NoContent: return opEmptySuccess()
diff --git a/packages/taler-util/src/http-client/types.ts 
b/packages/taler-util/src/http-client/types.ts
index 740d4204e..75241aa30 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -358,6 +358,13 @@ export const codecForAccountData = (): 
Codec<TalerCorebankApi.AccountData> =>
     .property("cashout_payto_uri", codecOptional(codecForPaytoString()))
     .property("is_public", codecForBoolean())
     .property("is_taler_exchange", codecForBoolean())
+    .property(
+      "tan_channel",
+      codecOptional(codecForEither(
+        codecForConstString(TalerCorebankApi.TanChannel.SMS),
+        codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
+      )),
+    )
     .build("TalerCorebankApi.AccountData");
 
 export const codecForChallengeContactData =
@@ -740,7 +747,7 @@ export const codecForAmlDecisionDetail =
 export const codecForChallenge =
   (): Codec<TalerCorebankApi.Challenge> =>
     buildCodecForObject<TalerCorebankApi.Challenge>()
-      .property("challenge_id", codecForString())
+      .property("challenge_id", codecForNumber())
       .build("TalerCorebankApi.Challenge");
 
 export const codecForTanTransmission =
@@ -1824,9 +1831,15 @@ export namespace TalerCorebankApi {
   export interface Challenge {
     // Unique identifier of the challenge to solve to run this protected
     // operation.
-    challenge_id: string;
+    challenge_id: number;
   }
 
+  export interface ChallengeSolve {
+    // The TAN code that solves $CHALLENGE_ID
+    tan: string;
+  }
+
+
   export enum TanChannel {
     SMS = "sms",
     EMAIL = "email"
diff --git a/packages/taler-util/src/http-common.ts 
b/packages/taler-util/src/http-common.ts
index 7c58b3874..68e1d2816 100644
--- a/packages/taler-util/src/http-common.ts
+++ b/packages/taler-util/src/http-common.ts
@@ -51,7 +51,7 @@ export const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
 
 export interface HttpRequestOptions {
   method?: "POST" | "PATCH" | "PUT" | "GET" | "DELETE";
-  headers?: { [name: string]: string };
+  headers?: { [name: string]: string | undefined };
 
   /**
    * Timeout after which the request should be aborted.
diff --git a/packages/web-util/src/components/LocalNotificationBanner.tsx 
b/packages/web-util/src/components/LocalNotificationBanner.tsx
index ab46703cb..62733ab3c 100644
--- a/packages/web-util/src/components/LocalNotificationBanner.tsx
+++ b/packages/web-util/src/components/LocalNotificationBanner.tsx
@@ -1,9 +1,8 @@
 import { h, Fragment, VNode } from "preact";
 import { Attention } from "./Attention.js";
 import { Notification } from "../index.browser.js";
-// import { useSettings } from "../hooks/settings.js";
 
-export function LocalNotificationBanner({ notification }: { notification?: 
Notification }): VNode {
+export function LocalNotificationBanner({ notification, showDebug }: { 
notification?: Notification, showDebug?: boolean }): VNode {
   if (!notification) return <Fragment />
   switch (notification.message.type) {
     case "error":
@@ -17,7 +16,9 @@ export function LocalNotificationBanner({ notification }: { 
notification?: Notif
                 {notification.message.description}
               </div>
             }
-            {/* <MaybeShowDebugInfo info={notification.message.debug} /> */}
+            {showDebug && <pre class="whitespace-break-spaces ">
+              {notification.message.debug}
+            </pre>}
           </Attention>
         </div>
       </div>
@@ -30,14 +31,3 @@ export function LocalNotificationBanner({ notification }: { 
notification?: Notif
   }
 }
 
-
-// function MaybeShowDebugInfo({ info }: { info: any }): VNode {
-//   const [settings] = useSettings()
-//   if (settings.showDebugInfo) {
-//     return <pre class="whitespace-break-spaces ">
-//       {info}
-//     </pre>
-//   }
-//   return <Fragment />
-// }
-
diff --git a/packages/web-util/src/utils/http-impl.browser.ts 
b/packages/web-util/src/utils/http-impl.browser.ts
index 974a7d1b8..18140ef13 100644
--- a/packages/web-util/src/utils/http-impl.browser.ts
+++ b/packages/web-util/src/utils/http-impl.browser.ts
@@ -73,10 +73,13 @@ export class BrowserHttpLib implements HttpRequestLibrary {
         ? encodeBody(requestBody)
         : undefined;
 
-    const requestHeadersMap = {
-      ...getDefaultHeaders(requestMethod),
-      ...requestHeader,
-    };
+    const requestHeadersMap = getDefaultHeaders(requestMethod);
+    if (requestHeader) {
+      Object.entries(requestHeader).forEach(([key, value]) => {
+        if (value === undefined) return;
+        requestHeadersMap[key] = value
+      })
+    }
 
     return new Promise<HttpResponse>((resolve, reject) => {
       const myRequest = new XMLHttpRequest();
diff --git a/packages/web-util/src/utils/http-impl.sw.ts 
b/packages/web-util/src/utils/http-impl.sw.ts
index 3120309f4..3c269e695 100644
--- a/packages/web-util/src/utils/http-impl.sw.ts
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -68,10 +68,13 @@ export class ServiceWorkerHttpLib implements 
HttpRequestLibrary {
     let myBody: ArrayBuffer | undefined =
       requestMethod === "POST" ? encodeBody(requestBody) : undefined;
 
-    const requestHeadersMap = {
-      ...getDefaultHeaders(requestMethod),
-      ...requestHeader,
-    };
+    const requestHeadersMap = getDefaultHeaders(requestMethod);
+    if (requestHeader) {
+      Object.entries(requestHeader).forEach(([key, value]) => {
+        if (value === undefined) return;
+        requestHeadersMap[key] = value
+      })
+    }
 
     const controller = new AbortController();
     let timeoutId: any | undefined;
@@ -190,7 +193,7 @@ function makeJsonHandler(
             requestMethod,
             httpStatusCode: response.status,
           },
-         message,
+          message,
         );
       }
     }

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