gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: cashout for business accounts


From: gnunet
Subject: [taler-wallet-core] branch master updated: cashout for business accounts
Date: Fri, 17 Feb 2023 20:24:11 +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 9697e953f cashout for business accounts
9697e953f is described below

commit 9697e953f56dc37208c2852d686d1854256f71ef
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Fri Feb 17 16:23:37 2023 -0300

    cashout for business accounts
---
 .../demobank-ui/src/components/Cashouts/index.ts   |   6 +-
 .../demobank-ui/src/components/Cashouts/state.ts   |   8 +-
 .../demobank-ui/src/components/Cashouts/test.ts    |   4 +-
 .../demobank-ui/src/components/Cashouts/views.tsx  |  23 +-
 packages/demobank-ui/src/declaration.d.ts          |  16 +-
 packages/demobank-ui/src/hooks/access.ts           |   8 +-
 packages/demobank-ui/src/hooks/backend.ts          |  23 +-
 packages/demobank-ui/src/hooks/circuit.ts          | 136 ++++-
 packages/demobank-ui/src/pages/AccountPage.tsx     |  37 --
 packages/demobank-ui/src/pages/AdminPage.tsx       | 150 ++++-
 packages/demobank-ui/src/pages/BankFrame.tsx       |  24 +-
 packages/demobank-ui/src/pages/BusinessAccount.tsx | 677 ++++++++++++++++++++-
 packages/demobank-ui/src/pages/HomePage.tsx        |   2 -
 packages/demobank-ui/src/pages/Routing.tsx         |   2 +-
 packages/demobank-ui/src/scss/bank.scss            |  32 +
 packages/demobank-ui/src/scss/main.scss            |   1 +
 packages/demobank-ui/src/scss/toggle.scss          |  51 ++
 packages/demobank-ui/src/utils.ts                  |  15 +
 18 files changed, 1117 insertions(+), 98 deletions(-)

diff --git a/packages/demobank-ui/src/components/Cashouts/index.ts 
b/packages/demobank-ui/src/components/Cashouts/index.ts
index 1410267be..3ca7d9026 100644
--- a/packages/demobank-ui/src/components/Cashouts/index.ts
+++ b/packages/demobank-ui/src/components/Cashouts/index.ts
@@ -23,7 +23,8 @@ import { useComponentState } from "./state.js";
 import { LoadingUriView, ReadyView } from "./views.js";
 
 export interface Props {
-  empty?: boolean;
+  account: string;
+  onSelected: (id: string) => void;
 }
 
 export type State = State.Loading | State.LoadingUriError | State.Ready;
@@ -45,7 +46,8 @@ export namespace State {
   export interface Ready extends BaseInfo {
     status: "ready";
     error: undefined;
-    cashouts: SandboxBackend.Circuit.CashoutStatusResponse[];
+    cashouts: SandboxBackend.Circuit.CashoutStatusResponseWithId[];
+    onSelected: (id: string) => void;
   }
 }
 
diff --git a/packages/demobank-ui/src/components/Cashouts/state.ts 
b/packages/demobank-ui/src/components/Cashouts/state.ts
index 178a1e815..124f9bf9c 100644
--- a/packages/demobank-ui/src/components/Cashouts/state.ts
+++ b/packages/demobank-ui/src/components/Cashouts/state.ts
@@ -14,12 +14,11 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { AbsoluteTime, Amounts } from "@gnu-taler/taler-util";
 import { useCashouts } from "../../hooks/circuit.js";
-import { Props, State, Transaction } from "./index.js";
+import { Props, State } from "./index.js";
 
-export function useComponentState({ empty }: Props): State {
-  const result = useCashouts();
+export function useComponentState({ account, onSelected }: Props): State {
+  const result = useCashouts(account);
   if (result.loading) {
     return {
       status: "loading",
@@ -37,5 +36,6 @@ export function useComponentState({ empty }: Props): State {
     status: "ready",
     error: undefined,
     cashouts: result.data,
+    onSelected,
   };
 }
diff --git a/packages/demobank-ui/src/components/Cashouts/test.ts 
b/packages/demobank-ui/src/components/Cashouts/test.ts
index 78450ed2d..e91116378 100644
--- a/packages/demobank-ui/src/components/Cashouts/test.ts
+++ b/packages/demobank-ui/src/components/Cashouts/test.ts
@@ -31,7 +31,7 @@ describe("Transaction states", () => {
     const env = new SwrMockEnvironment();
 
     const props: Props = {
-
+      account: "123",
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
@@ -115,6 +115,7 @@ describe("Transaction states", () => {
     const env = new SwrMockEnvironment();
 
     const props: Props = {
+      account: "123",
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {});
@@ -147,6 +148,7 @@ describe("Transaction states", () => {
     const env = new SwrMockEnvironment(false);
 
     const props: Props = {
+      account: "123",
     };
 
     env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {});
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx 
b/packages/demobank-ui/src/components/Cashouts/views.tsx
index 16ae8a58f..af1d9ed2c 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -30,8 +30,15 @@ export function LoadingUriView({ error }: 
State.LoadingUriError): VNode {
   );
 }
 
-export function ReadyView({ cashouts }: State.Ready): VNode {
+export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
   const { i18n } = useTranslationContext();
+  if (!cashouts.length) {
+    return (
+      <div>
+        <i18n.Translate>No cashout at the moment</i18n.Translate>
+      </div>
+    );
+  }
   return (
     <div class="results">
       <table class="pure-table pure-table-striped">
@@ -39,6 +46,8 @@ export function ReadyView({ cashouts }: State.Ready): VNode {
           <tr>
             <th>{i18n.str`Created`}</th>
             <th>{i18n.str`Confirmed`}</th>
+            <th>{i18n.str`Total debit`}</th>
+            <th>{i18n.str`Total credit`}</th>
             <th>{i18n.str`Status`}</th>
             <th>{i18n.str`Subject`}</th>
           </tr>
@@ -56,7 +65,17 @@ export function ReadyView({ cashouts }: State.Ready): VNode {
                 <td>{Amounts.stringifyValue(item.amount_debit)}</td>
                 <td>{Amounts.stringifyValue(item.amount_credit)}</td>
                 <td>{item.status}</td>
-                <td>{item.subject}</td>
+                <td>
+                  <a
+                    href="#"
+                    onClick={(e) => {
+                      e.preventDefault();
+                      onSelected(item.id);
+                    }}
+                  >
+                    {item.subject}
+                  </a>
+                </td>
               </tr>
             );
           })}
diff --git a/packages/demobank-ui/src/declaration.d.ts 
b/packages/demobank-ui/src/declaration.d.ts
index c46fcc9ed..e3160d9ae 100644
--- a/packages/demobank-ui/src/declaration.d.ts
+++ b/packages/demobank-ui/src/declaration.d.ts
@@ -322,11 +322,6 @@ namespace SandboxBackend {
       // where to send cashouts.
       cashout_address: string;
     }
-    enum TanChannel {
-      SMS = "sms",
-      EMAIL = "email",
-      FILE = "file",
-    }
     interface CashoutRequest {
       // Optional subject to associate to the
       // cashout operation.  This data will appear
@@ -369,6 +364,7 @@ namespace SandboxBackend {
       // Contains ratios and fees related to buying
       // and selling the circuit currency.
       ratios_and_fees: RatiosAndFees;
+      currency: string;
     }
     interface RatiosAndFees {
       // Exchange rate to buy the circuit currency from fiat.
@@ -400,14 +396,6 @@ namespace SandboxBackend {
       // Missing or null, when the operation wasn't confirmed yet.
       confirmation_time?: number | null; // milliseconds since the Unix epoch
     }
-    enum CashoutStatus {
-      // The payment was initiated after a valid
-      // TAN was received by the bank.
-      CONFIRMED = "confirmed",
-
-      // The cashout was created and now waits
-      // for the TAN by the author.
-      PENDING = "pending",
-    }
+    type CashoutStatusResponseWithId = CashoutStatusResponse & { id: string };
   }
 }
diff --git a/packages/demobank-ui/src/hooks/access.ts 
b/packages/demobank-ui/src/hooks/access.ts
index 0379de27d..6046146ba 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -18,7 +18,7 @@ import {
   HttpResponse,
   HttpResponseOk,
   HttpResponsePaginated,
-  RequestError
+  RequestError,
 } from "@gnu-taler/web-util/lib/index.browser";
 import { useEffect, useState } from "preact/hooks";
 import { useBackendContext } from "../context/backend.js";
@@ -26,12 +26,12 @@ import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
 import {
   useAuthenticatedBackend,
   useMatchMutate,
-  usePublicBackend
+  usePublicBackend,
 } from "./backend.js";
 
 // FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import _useSWR, { SWRHook } from 'swr';
-const useSWR = _useSWR as unknown as SWRHook
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
 
 export function useAccessAPI(): AccessAPI {
   const mutateAll = useMatchMutate();
diff --git a/packages/demobank-ui/src/hooks/backend.ts 
b/packages/demobank-ui/src/hooks/backend.ts
index e87bdd5fe..e0649f5fe 100644
--- a/packages/demobank-ui/src/hooks/backend.ts
+++ b/packages/demobank-ui/src/hooks/backend.ts
@@ -118,6 +118,7 @@ interface useBackendType {
   sandboxAccountsFetcher: <T>(
     args: [string, number, number, string],
   ) => Promise<HttpResponseOk<T>>;
+  sandboxCashoutFetcher: <T>(endpoint: string[]) => Promise<HttpResponseOk<T>>;
 }
 export function usePublicBackend(): useBackendType {
   const { state } = useBackendContext();
@@ -176,12 +177,21 @@ export function usePublicBackend(): useBackendType {
     },
     [baseUrl],
   );
+  const sandboxCashoutFetcher = useCallback(
+    function fetcherImpl<T>([endpoint, account]: string[]): Promise<
+      HttpResponseOk<T>
+    > {
+      return requestHandler<T>(baseUrl, endpoint);
+    },
+    [baseUrl],
+  );
   return {
     request,
     fetcher,
     paginatedFetcher,
     multiFetcher,
     sandboxAccountsFetcher,
+    sandboxCashoutFetcher,
   };
 }
 
@@ -225,7 +235,6 @@ export function useAuthenticatedBackend(): useBackendType {
     function multiFetcherImpl<T>([endpoints]: string[][]): Promise<
       HttpResponseOk<T>[]
     > {
-      console.log("list size", endpoints.length, endpoints);
       return Promise.all(
         endpoints.map((endpoint) =>
           requestHandler<T>(baseUrl, endpoint, { basicAuth: creds }),
@@ -249,12 +258,24 @@ export function useAuthenticatedBackend(): useBackendType 
{
     [baseUrl],
   );
 
+  const sandboxCashoutFetcher = useCallback(
+    function fetcherImpl<T>([endpoint, account]: string[]): Promise<
+      HttpResponseOk<T>
+    > {
+      return requestHandler<T>(baseUrl, endpoint, {
+        basicAuth: creds,
+        params: { account },
+      });
+    },
+    [baseUrl, creds],
+  );
   return {
     request,
     fetcher,
     paginatedFetcher,
     multiFetcher,
     sandboxAccountsFetcher,
+    sandboxCashoutFetcher,
   };
 }
 
diff --git a/packages/demobank-ui/src/hooks/circuit.ts 
b/packages/demobank-ui/src/hooks/circuit.ts
index 21e5ce852..c7170309b 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -27,8 +27,8 @@ import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils.js";
 import { useAuthenticatedBackend, useMatchMutate } from "./backend.js";
 
 // FIX default import https://github.com/microsoft/TypeScript/issues/49189
-import _useSWR, { SWRHook } from 'swr';
-const useSWR = _useSWR as unknown as SWRHook
+import _useSWR, { SWRHook } from "swr";
+const useSWR = _useSWR as unknown as SWRHook;
 
 export function useAdminAccountAPI(): AdminAccountAPI {
   const { request } = useAuthenticatedBackend();
@@ -118,7 +118,54 @@ export function useCircuitAccountAPI(): CircuitAccountAPI {
     return res;
   };
 
-  return { updateAccount, changePassword };
+  const createCashout = async (
+    data: SandboxBackend.Circuit.CashoutRequest,
+  ): Promise<HttpResponseOk<SandboxBackend.Circuit.CashoutPending>> => {
+    const res = await request<SandboxBackend.Circuit.CashoutPending>(
+      `circuit-api/cashouts`,
+      {
+        method: "POST",
+        data,
+        contentType: "json",
+      },
+    );
+    return res;
+  };
+
+  const confirmCashout = async (
+    cashoutId: string,
+    data: SandboxBackend.Circuit.CashoutConfirm,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(
+      `circuit-api/cashouts/${cashoutId}/confirm`,
+      {
+        method: "POST",
+        data,
+        contentType: "json",
+      },
+    );
+    await mutateAll(/.*circuit-api\/cashout.*/);
+    return res;
+  };
+
+  const abortCashout = async (
+    cashoutId: string,
+  ): Promise<HttpResponseOk<void>> => {
+    const res = await request<void>(`circuit-api/cashouts/${cashoutId}/abort`, 
{
+      method: "POST",
+      contentType: "json",
+    });
+    await mutateAll(/.*circuit-api\/cashout.*/);
+    return res;
+  };
+
+  return {
+    updateAccount,
+    changePassword,
+    createCashout,
+    confirmCashout,
+    abortCashout,
+  };
 }
 
 export interface AdminAccountAPI {
@@ -144,11 +191,14 @@ export interface CircuitAccountAPI {
   changePassword: (
     data: SandboxBackend.Circuit.AccountPasswordChange,
   ) => Promise<HttpResponseOk<void>>;
-}
-
-export interface InstanceTemplateFilter {
-  //FIXME: add filter to the template list
-  position?: string;
+  createCashout: (
+    data: SandboxBackend.Circuit.CashoutRequest,
+  ) => Promise<HttpResponseOk<SandboxBackend.Circuit.CashoutPending>>;
+  confirmCashout: (
+    id: string,
+    data: SandboxBackend.Circuit.CashoutConfirm,
+  ) => Promise<HttpResponseOk<void>>;
+  abortCashout: (id: string) => Promise<HttpResponseOk<void>>;
 }
 
 async function getBusinessStatus(
@@ -217,6 +267,35 @@ export function useBusinessAccountDetails(
   return { loading: true };
 }
 
+export function useRatiosAndFeeConfig(): HttpResponse<
+  SandboxBackend.Circuit.Config,
+  SandboxBackend.SandboxError
+> {
+  const { fetcher } = useAuthenticatedBackend();
+
+  const { data, error } = useSWR<
+    HttpResponseOk<SandboxBackend.Circuit.Config>,
+    RequestError<SandboxBackend.SandboxError>
+  >([`circuit-api/config`], fetcher, {
+    refreshInterval: 0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+    errorRetryCount: 0,
+    errorRetryInterval: 1,
+    shouldRetryOnError: false,
+    keepPreviousData: true,
+  });
+
+  if (data) {
+    data.data.currency = "FIAT";
+  }
+  if (data) return data;
+  if (error) return error.info;
+  return { loading: true };
+}
+
 interface PaginationFilter {
   account?: string;
   page?: number;
@@ -299,17 +378,18 @@ export function useBusinessAccounts(
   return { loading: true };
 }
 
-export function useCashouts(): HttpResponse<
-  (SandboxBackend.Circuit.CashoutStatusResponse & WithId)[],
+export function useCashouts(
+  account: string,
+): HttpResponse<
+  SandboxBackend.Circuit.CashoutStatusResponseWithId[],
   SandboxBackend.SandboxError
 > {
-  const { fetcher, multiFetcher } = useAuthenticatedBackend();
-
+  const { sandboxCashoutFetcher, multiFetcher } = useAuthenticatedBackend();
 
   const { data: list, error: listError } = useSWR<
     HttpResponseOk<SandboxBackend.Circuit.Cashouts>,
     RequestError<SandboxBackend.SandboxError>
-  >([`circuit-api/cashouts`], fetcher, {
+  >([`circuit-api/cashouts`, account], sandboxCashoutFetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -317,7 +397,7 @@ export function useCashouts(): HttpResponse<
     refreshWhenOffline: false,
   });
 
-  const paths = (list?.data.cashouts || []).map(
+  const paths = ((list?.data && list?.data.cashouts) || []).map(
     (cashoutId) => `circuit-api/cashouts/${cashoutId}`,
   );
   const { data: cashouts, error: productError } = useSWR<
@@ -346,3 +426,31 @@ export function useCashouts(): HttpResponse<
   }
   return { loading: true };
 }
+
+export function useCashoutDetails(
+  id: string,
+): HttpResponse<
+  SandboxBackend.Circuit.CashoutStatusResponse,
+  SandboxBackend.SandboxError
+> {
+  const { fetcher } = useAuthenticatedBackend();
+
+  const { data, error } = useSWR<
+    HttpResponseOk<SandboxBackend.Circuit.CashoutStatusResponse>,
+    RequestError<SandboxBackend.SandboxError>
+  >([`circuit-api/cashouts/${id}`], fetcher, {
+    refreshInterval: 0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+    errorRetryCount: 0,
+    errorRetryInterval: 1,
+    shouldRetryOnError: false,
+    keepPreviousData: true,
+  });
+
+  if (data) return data;
+  if (error) return error.info;
+  return { loading: true };
+}
diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx 
b/packages/demobank-ui/src/pages/AccountPage.tsx
index 370605871..ae0c2b1f8 100644
--- a/packages/demobank-ui/src/pages/AccountPage.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage.tsx
@@ -112,40 +112,3 @@ export function AccountPage({ account, onLoadNotOk }: 
Props): VNode {
     </Fragment>
   );
 }
-
-// function Moves({ account }: { account: string }): VNode {
-//   const [tab, setTab] = useState<"transactions" | 
"cashouts">("transactions");
-//   const { i18n } = useTranslationContext();
-//   return (
-//     <article>
-//       <div class="payments">
-//         <div class="tab">
-//           <button
-//             class={tab === "transactions" ? "tablinks active" : "tablinks"}
-//             onClick={(): void => {
-//               setTab("transactions");
-//             }}
-//           >
-//             {i18n.str`Transactions`}
-//           </button>
-//           <button
-//             class={tab === "cashouts" ? "tablinks active" : "tablinks"}
-//             onClick={(): void => {
-//               setTab("cashouts");
-//             }}
-//           >
-//             {i18n.str`Cashouts`}
-//           </button>
-//         </div>
-//         {tab === "transactions" && (
-//         )}
-//         {tab === "cashouts" && (
-//           <div class="active">
-//             <h3>{i18n.str`Latest cashouts`}</h3>
-//             <Cashouts account={account} />
-//           </div>
-//         )}
-//       </div>
-//     </article>
-//   );
-// }
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx 
b/packages/demobank-ui/src/pages/AdminPage.tsx
index f8efddd80..d15ac02c4 100644
--- a/packages/demobank-ui/src/pages/AdminPage.tsx
+++ b/packages/demobank-ui/src/pages/AdminPage.tsx
@@ -14,7 +14,11 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { parsePaytoUri, TranslatedString } from "@gnu-taler/taler-util";
+import {
+  Amounts,
+  parsePaytoUri,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import {
   HttpResponsePaginated,
   RequestError,
@@ -22,7 +26,9 @@ import {
 } from "@gnu-taler/web-util/lib/index.browser";
 import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
+import { Cashouts } from "../components/Cashouts/index.js";
 import { ErrorMessage, usePageContext } from "../context/pageState.js";
+import { useAccountDetails } from "../hooks/access.js";
 import {
   useBusinessAccountDetails,
   useBusinessAccounts,
@@ -60,7 +66,10 @@ interface Props {
 export function AdminPage({ onLoadNotOk }: Props): VNode {
   const [account, setAccount] = useState<string | undefined>();
   const [showDetails, setShowDetails] = useState<string | undefined>();
+  const [showCashouts, setShowCashouts] = useState<string | undefined>();
   const [updatePassword, setUpdatePassword] = useState<string | undefined>();
+  const [removeAccount, setRemoveAccount] = useState<string | undefined>();
+
   const [createAccount, setCreateAccount] = useState(false);
   const { pageStateSetter } = usePageContext();
 
@@ -81,6 +90,23 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
 
   const { customers } = result.data;
 
+  if (showCashouts) {
+    return (
+      <div>
+        <Cashouts account={showCashouts} />
+        <input
+          class="pure-button"
+          type="submit"
+          value={i18n.str`Close`}
+          onClick={async (e) => {
+            e.preventDefault();
+            setShowCashouts(undefined);
+          }}
+        />
+      </div>
+    );
+  }
+
   if (showDetails) {
     return (
       <ShowAccountDetails
@@ -100,6 +126,21 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
       />
     );
   }
+  if (removeAccount) {
+    return (
+      <RemoveAccount
+        account={removeAccount}
+        onLoadNotOk={onLoadNotOk}
+        onUpdateSuccess={() => {
+          showInfoMessage(i18n.str`Account removed`);
+          setRemoveAccount(undefined);
+        }}
+        onClear={() => {
+          setRemoveAccount(undefined);
+        }}
+      />
+    );
+  }
   if (updatePassword) {
     return (
       <UpdateAccountPassword
@@ -164,6 +205,7 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
                   <th>{i18n.str`Username`}</th>
                   <th>{i18n.str`Name`}</th>
                   <th></th>
+                  <th></th>
                 </tr>
               </thead>
               <tbody>
@@ -193,6 +235,28 @@ export function AdminPage({ onLoadNotOk }: Props): VNode {
                           change password
                         </a>
                       </td>
+                      <td>
+                        <a
+                          href="#"
+                          onClick={(e) => {
+                            e.preventDefault();
+                            setShowCashouts(item.username);
+                          }}
+                        >
+                          cashouts
+                        </a>
+                      </td>
+                      <td>
+                        <a
+                          href="#"
+                          onClick={(e) => {
+                            e.preventDefault();
+                            setRemoveAccount(item.username);
+                          }}
+                        >
+                          remove
+                        </a>
+                      </td>
                     </tr>
                   );
                 })}
@@ -536,6 +600,90 @@ export function ShowAccountDetails({
   );
 }
 
+function RemoveAccount({
+  account,
+  onClear,
+  onUpdateSuccess,
+  onLoadNotOk,
+}: {
+  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+  onClear: () => void;
+  onUpdateSuccess: () => void;
+  account: string;
+}): VNode {
+  const { i18n } = useTranslationContext();
+  const result = useAccountDetails(account);
+  const { deleteAccount } = useAdminAccountAPI();
+  const [error, saveError] = useState<ErrorMessage | undefined>();
+
+  if (result.clientError) {
+    if (result.isNotfound) return <div>account not found</div>;
+  }
+  if (!result.ok) {
+    return onLoadNotOk(result);
+  }
+
+  const balance = Amounts.parse(result.data.balance.amount);
+  if (!balance) {
+    return <div>there was an error reading the balance</div>;
+  }
+  const isBalanceEmpty = Amounts.isZero(balance);
+  return (
+    <div>
+      <div>
+        <h1 class="nav welcome-text">
+          <i18n.Translate>Remove account: {account}</i18n.Translate>
+        </h1>
+      </div>
+      {!isBalanceEmpty && (
+        <ErrorBanner
+          error={{
+            title: i18n.str`Can't delete the account`,
+            description: i18n.str`Balance is not empty`,
+          }}
+        />
+      )}
+      {error && (
+        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+      )}
+
+      <p>
+        <div style={{ display: "flex", justifyContent: "space-between" }}>
+          <div>
+            <input
+              class="pure-button"
+              type="submit"
+              value={i18n.str`Cancel`}
+              onClick={async (e) => {
+                e.preventDefault();
+                onClear();
+              }}
+            />
+          </div>
+          <div>
+            <input
+              id="select-exchange"
+              class="pure-button pure-button-primary content"
+              disabled={!isBalanceEmpty}
+              type="submit"
+              value={i18n.str`Confirm`}
+              onClick={async (e) => {
+                e.preventDefault();
+                try {
+                  const r = await deleteAccount(account);
+                  onUpdateSuccess();
+                } catch (error) {
+                  handleError(error, saveError, i18n);
+                }
+              }}
+            />
+          </div>
+        </div>
+      </p>
+    </div>
+  );
+}
+
 /**
  * Create valid account object to update or create
  * Take template as initial values for the form
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx 
b/packages/demobank-ui/src/pages/BankFrame.tsx
index 0fb75b87b..fe7571c38 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -128,7 +128,7 @@ export function BankFrame({
         <StatusBanner />
         {backend.state.status === "loggedIn" ? (
           <div class="top-right">
-            {goToBusinessAccount ? (
+            {goToBusinessAccount && !backend.state.isUserAdministrator ? (
               <MaybeBusinessButton
                 account={backend.state.username}
                 onClick={goToBusinessAccount}
@@ -187,7 +187,7 @@ export function ErrorBanner({
   onClear,
 }: {
   error: ErrorMessage;
-  onClear: () => void;
+  onClear?: () => void;
 }): VNode | null {
   return (
     <div class="informational informational-fail" style={{ marginTop: 8 }}>
@@ -196,15 +196,17 @@ export function ErrorBanner({
           <b>{error.title}</b>
         </p>
         <div>
-          <input
-            type="button"
-            class="pure-button"
-            value="Clear"
-            onClick={(e) => {
-              e.preventDefault();
-              onClear();
-            }}
-          />
+          {onClear && (
+            <input
+              type="button"
+              class="pure-button"
+              value="Clear"
+              onClick={(e) => {
+                e.preventDefault();
+                onClear();
+              }}
+            />
+          )}
         </div>
       </div>
       <p>{error.description}</p>
diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx 
b/packages/demobank-ui/src/pages/BusinessAccount.tsx
index d845c2fa0..6651ef0f7 100644
--- a/packages/demobank-ui/src/pages/BusinessAccount.tsx
+++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx
@@ -13,18 +13,34 @@
  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 { TranslatedString } from "@gnu-taler/taler-util";
+import {
+  AmountJson,
+  Amounts,
+  HttpStatusCode,
+  TranslatedString,
+} from "@gnu-taler/taler-util";
 import {
   HttpResponsePaginated,
+  RequestError,
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
-import { h, VNode } from "preact";
+import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { Cashouts } from "../components/Cashouts/index.js";
 import { useBackendContext } from "../context/backend.js";
-import { usePageContext } from "../context/pageState.js";
+import { ErrorMessage, usePageContext } from "../context/pageState.js";
+import { useAccountDetails } from "../hooks/access.js";
+import {
+  useCashoutDetails,
+  useCashouts,
+  useCircuitAccountAPI,
+  useRatiosAndFeeConfig,
+} from "../hooks/circuit.js";
+import { CashoutStatus, TanChannel, undefinedIfEmpty } from "../utils.js";
 import { ShowAccountDetails, UpdateAccountPassword } from "./AdminPage.js";
+import { ErrorBanner } from "./BankFrame.js";
 import { LoginForm } from "./LoginForm.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
 
 interface Props {
   onClose: () => void;
@@ -40,6 +56,8 @@ export function BusinessAccount({
   const { pageStateSetter } = usePageContext();
   const backend = useBackendContext();
   const [updatePassword, setUpdatePassword] = useState(false);
+  const [newCashout, setNewcashout] = useState(false);
+  const [showCashout, setShowCashout] = useState<string | undefined>();
   function showInfoMessage(info: TranslatedString): void {
     pageStateSetter((prev) => ({
       ...prev,
@@ -51,6 +69,32 @@ export function BusinessAccount({
     return <LoginForm onRegister={onRegister} />;
   }
 
+  if (newCashout) {
+    return (
+      <CreateCashout
+        account={backend.state.username}
+        onLoadNotOk={onLoadNotOk}
+        onCancel={() => {
+          setNewcashout(false);
+        }}
+        onComplete={(id) => {
+          setNewcashout(false);
+          setShowCashout(id);
+        }}
+      />
+    );
+  }
+  if (showCashout) {
+    return (
+      <ShowCashout
+        id={showCashout}
+        onLoadNotOk={onLoadNotOk}
+        onCancel={() => {
+          setShowCashout(undefined);
+        }}
+      />
+    );
+  }
   if (updatePassword) {
     return (
       <UpdateAccountPassword
@@ -82,9 +126,634 @@ export function BusinessAccount({
       <section style={{ marginTop: "2em" }}>
         <div class="active">
           <h3>{i18n.str`Latest cashouts`}</h3>
-          <Cashouts />
+          <Cashouts
+            account={backend.state.username}
+            onSelected={(id) => {
+              setShowCashout(id);
+            }}
+          />
+        </div>
+        <br />
+        <div style={{ display: "flex", justifyContent: "space-between" }}>
+          <div />
+          <input
+            class="pure-button pure-button-primary content"
+            type="submit"
+            value={i18n.str`New cashout`}
+            onClick={async (e) => {
+              e.preventDefault();
+              setNewcashout(true);
+            }}
+          />
         </div>
       </section>
     </div>
   );
 }
+
+interface PropsCashout {
+  account: string;
+  onComplete: (id: string) => void;
+  onCancel: () => void;
+  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+}
+
+type FormType = {
+  isDebit: boolean;
+  amount: string;
+  subject: string;
+  channel: TanChannel;
+};
+type ErrorFrom<T> = {
+  [P in keyof T]+?: string;
+};
+
+function CreateCashout({
+  account,
+  onComplete,
+  onCancel,
+  onLoadNotOk,
+}: PropsCashout): VNode {
+  const { i18n } = useTranslationContext();
+  const ratiosResult = useRatiosAndFeeConfig();
+  const result = useAccountDetails(account);
+  const [error, saveError] = useState<ErrorMessage | undefined>();
+
+  const [form, setForm] = useState<Partial<FormType>>({});
+
+  const { createCashout } = useCircuitAccountAPI();
+  if (!result.ok) return onLoadNotOk(result);
+  if (!ratiosResult.ok) return onLoadNotOk(ratiosResult);
+  const config = ratiosResult.data;
+  const maybeBalance = Amounts.parse(result.data.balance.amount);
+  if (!maybeBalance) return <div>error</div>;
+  const balance = maybeBalance;
+  const zero = Amounts.zeroOfCurrency(balance.currency);
+
+  const sellRate = config.ratios_and_fees.sell_at_ratio;
+  const sellFee = !config.ratios_and_fees.sell_out_fee
+    ? zero
+    : Amounts.fromFloat(config.ratios_and_fees.sell_out_fee, balance.currency);
+
+  if (!sellRate || sellRate < 0) return <div>error rate</div>;
+
+  function truncate(a: AmountJson): AmountJson {
+    const str = Amounts.stringify(a);
+    const idx = str.indexOf(".");
+    if (idx === -1) return a;
+    const truncated = str.substring(0, idx + 3);
+    console.log(str, truncated);
+    return Amounts.parseOrThrow(truncated);
+  }
+
+  const amount = Amounts.parse(`${balance.currency}:${form.amount}`);
+  const amount_debit = !amount
+    ? zero
+    : form.isDebit
+    ? amount
+    : truncate(Amounts.divide(Amounts.add(amount, sellFee).amount, sellRate));
+  const credit_before_fee = !amount
+    ? zero
+    : form.isDebit
+    ? truncate(Amounts.divide(amount, 1 / sellRate))
+    : Amounts.add(amount, sellFee).amount;
+
+  const __amount_credit = Amounts.sub(credit_before_fee, sellFee).amount;
+  const amount_credit = Amounts.parseOrThrow(
+    `${config.currency}:${Amounts.stringifyValue(__amount_credit)}`,
+  );
+
+  const balanceAfter = Amounts.sub(balance, amount_debit).amount;
+
+  function updateForm(newForm: typeof form): void {
+    setForm(newForm);
+  }
+  const errors = undefinedIfEmpty<ErrorFrom<typeof form>>({
+    amount: !form.amount
+      ? i18n.str`required`
+      : !amount
+      ? i18n.str`could not be parsed`
+      : Amounts.cmp(balance, amount_debit) === -1
+      ? i18n.str`balance is not enough`
+      : Amounts.cmp(credit_before_fee, sellFee) === -1
+      ? i18n.str`amount is not enough`
+      : Amounts.isZero(amount_credit)
+      ? i18n.str`amount is not enough`
+      : undefined,
+    channel: !form.channel ? i18n.str`required` : undefined,
+  });
+
+  // setErrors(validationResult);
+
+  return (
+    <div>
+      {error && (
+        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+      )}
+      <h1>New cashout</h1>
+      <form class="pure-form">
+        <fieldset>
+          <label>{i18n.str`Subject`}</label>
+          <input
+            value={form.subject ?? ""}
+            onChange={(e) => {
+              form.subject = e.currentTarget.value;
+              updateForm(structuredClone(form));
+            }}
+          />
+          <ShowInputErrorLabel
+            message={errors?.subject}
+            isDirty={form.subject !== undefined}
+          />
+        </fieldset>
+        <fieldset>
+          <label>
+            {form.isDebit
+              ? i18n.str`Amount to send`
+              : i18n.str`Amount to receive`}
+          </label>
+          <div style={{ width: "max-content" }}>
+            <input
+              type="text"
+              readonly
+              class="currency-indicator"
+              size={balance.currency.length}
+              maxLength={balance.currency.length}
+              tabIndex={-1}
+              value={balance.currency}
+            />
+            &nbsp;
+            <input
+              type="number"
+              // ref={ref}
+              id="withdraw-amount"
+              name="withdraw-amount"
+              value={form.amount ?? ""}
+              onChange={(e): void => {
+                form.amount = e.currentTarget.value;
+                updateForm(structuredClone(form));
+              }}
+            />
+            &nbsp;
+            <label class="toggle">
+              <input
+                class="toggle-checkbox"
+                type="checkbox"
+                onChange={(e): void => {
+                  form.isDebit = !form.isDebit;
+                  updateForm(structuredClone(form));
+                }}
+              />
+              <div class="toggle-switch"></div>
+            </label>
+          </div>
+          <ShowInputErrorLabel
+            message={errors?.amount}
+            isDirty={form.amount !== undefined}
+          />
+        </fieldset>
+        <fieldset>
+          <label>{i18n.str`Conversion rate`}</label>
+          <input value={sellRate} disabled />
+        </fieldset>
+        <fieldset>
+          <label>{i18n.str`Balance now`}</label>
+          <div style={{ width: "max-content" }}>
+            <input
+              type="text"
+              readonly
+              class="currency-indicator"
+              size={balance.currency.length}
+              maxLength={balance.currency.length}
+              tabIndex={-1}
+              value={balance.currency}
+            />
+            &nbsp;
+            <input
+              type="number"
+              id="withdraw-amount"
+              disabled
+              name="withdraw-amount"
+              value={Amounts.stringifyValue(balance)}
+            />
+          </div>
+        </fieldset>
+        <fieldset>
+          <label
+            style={{ fontWeight: "bold", color: "red" }}
+          >{i18n.str`Total cost`}</label>
+          <div style={{ width: "max-content" }}>
+            <input
+              type="text"
+              readonly
+              class="currency-indicator"
+              size={balance.currency.length}
+              maxLength={balance.currency.length}
+              tabIndex={-1}
+              value={balance.currency}
+            />
+            &nbsp;
+            <input
+              type="number"
+              // ref={ref}
+              id="withdraw-amount"
+              disabled
+              name="withdraw-amount"
+              value={amount_debit ? Amounts.stringifyValue(amount_debit) : ""}
+            />
+          </div>
+        </fieldset>
+        <fieldset>
+          <label>{i18n.str`Balance after`}</label>
+          <div style={{ width: "max-content" }}>
+            <input
+              type="text"
+              readonly
+              class="currency-indicator"
+              size={balance.currency.length}
+              maxLength={balance.currency.length}
+              tabIndex={-1}
+              value={balance.currency}
+            />
+            &nbsp;
+            <input
+              type="number"
+              // ref={ref}
+              id="withdraw-amount"
+              disabled
+              name="withdraw-amount"
+              value={balanceAfter ? Amounts.stringifyValue(balanceAfter) : ""}
+            />
+          </div>
+        </fieldset>{" "}
+        {Amounts.isZero(sellFee) ? undefined : (
+          <Fragment>
+            <fieldset>
+              <label>{i18n.str`Transfer before fee`}</label>
+              <div style={{ width: "max-content" }}>
+                <input
+                  type="text"
+                  readonly
+                  class="currency-indicator"
+                  size={balance.currency.length}
+                  maxLength={balance.currency.length}
+                  tabIndex={-1}
+                  value={balance.currency}
+                />
+                &nbsp;
+                <input
+                  // type="number"
+                  style={{ color: "black" }}
+                  disabled
+                  value={Amounts.stringifyValue(credit_before_fee)}
+                />
+              </div>
+            </fieldset>
+
+            <fieldset>
+              <label>{i18n.str`Cashout fee`}</label>
+              <div style={{ width: "max-content" }}>
+                <input
+                  type="text"
+                  readonly
+                  class="currency-indicator"
+                  size={balance.currency.length}
+                  maxLength={balance.currency.length}
+                  tabIndex={-1}
+                  value={balance.currency}
+                />
+                &nbsp;
+                <input
+                  // type="number"
+                  style={{ color: "black" }}
+                  disabled
+                  value={Amounts.stringifyValue(sellFee)}
+                />
+              </div>
+            </fieldset>
+          </Fragment>
+        )}
+        <fieldset>
+          <label
+            style={{ fontWeight: "bold", color: "green" }}
+          >{i18n.str`Total cashout transfer`}</label>
+          <div style={{ width: "max-content" }}>
+            <input
+              type="text"
+              readonly
+              class="currency-indicator"
+              size={balance.currency.length}
+              maxLength={balance.currency.length}
+              tabIndex={-1}
+              value={balance.currency}
+            />
+            &nbsp;
+            <input
+              type="number"
+              // ref={ref}
+              id="withdraw-amount"
+              disabled
+              name="withdraw-amount"
+              value={amount_credit ? Amounts.stringifyValue(amount_credit) : 
""}
+            />
+          </div>
+        </fieldset>
+        <fieldset>
+          <label>{i18n.str`Confirmation channel`}</label>
+
+          <div class="channel">
+            <input
+              class={
+                "pure-button content " +
+                (form.channel === TanChannel.EMAIL
+                  ? "pure-button-primary"
+                  : "pure-button-secondary")
+              }
+              type="submit"
+              value={i18n.str`Email`}
+              onClick={async (e) => {
+                e.preventDefault();
+                form.channel = TanChannel.EMAIL;
+                updateForm(structuredClone(form));
+              }}
+            />
+            <input
+              class={
+                "pure-button content " +
+                (form.channel === TanChannel.SMS
+                  ? "pure-button-primary"
+                  : "pure-button-secondary")
+              }
+              type="submit"
+              value={i18n.str`SMS`}
+              onClick={async (e) => {
+                e.preventDefault();
+                form.channel = TanChannel.SMS;
+                updateForm(structuredClone(form));
+              }}
+            />
+            <input
+              class={
+                "pure-button content " +
+                (form.channel === TanChannel.FILE
+                  ? "pure-button-primary"
+                  : "pure-button-secondary")
+              }
+              type="submit"
+              value={i18n.str`FILE`}
+              onClick={async (e) => {
+                e.preventDefault();
+                form.channel = TanChannel.FILE;
+                updateForm(structuredClone(form));
+              }}
+            />
+          </div>
+          <ShowInputErrorLabel
+            message={errors?.channel}
+            isDirty={form.channel !== undefined}
+          />
+        </fieldset>
+        <br />
+        <div style={{ display: "flex", justifyContent: "space-between" }}>
+          <button
+            class="pure-button pure-button-secondary btn-cancel"
+            onClick={(e) => {
+              e.preventDefault();
+              onCancel();
+            }}
+          >
+            {i18n.str`Cancel`}
+          </button>
+
+          <button
+            class="pure-button pure-button-primary btn-register"
+            type="submit"
+            disabled={!!errors}
+            onClick={async (e) => {
+              e.preventDefault();
+
+              if (errors) return;
+              try {
+                const res = await createCashout({
+                  amount_credit: Amounts.stringify(amount_credit),
+                  amount_debit: Amounts.stringify(amount_debit),
+                  subject: form.subject,
+                  tan_channel: form.channel,
+                });
+                onComplete(res.data.uuid);
+              } catch (error) {
+                if (error instanceof RequestError) {
+                  const errorData: SandboxBackend.SandboxError =
+                    error.info.error;
+                  if (error.info.status === HttpStatusCode.PreconditionFailed) 
{
+                    saveError({
+                      title: i18n.str`The account does not have sufficient 
funds`,
+                      description: errorData.error.description,
+                      debug: JSON.stringify(error.info),
+                    });
+                  } else if (
+                    error.info.status === HttpStatusCode.ServiceUnavailable
+                  ) {
+                    saveError({
+                      title: i18n.str`The bank does not support the TAN 
channel for this operation`,
+                      description: errorData.error.description,
+                      debug: JSON.stringify(error.info),
+                    });
+                  } else if (error.info.status === HttpStatusCode.Conflict) {
+                    saveError({
+                      title: i18n.str`No contact information for this channel`,
+                      description: errorData.error.description,
+                      debug: JSON.stringify(error.info),
+                    });
+                  } else {
+                    saveError({
+                      title: i18n.str`New cashout gave response error`,
+                      description: errorData.error.description,
+                      debug: JSON.stringify(error.info),
+                    });
+                  }
+                } else if (error instanceof Error) {
+                  saveError({
+                    title: i18n.str`Cashout failed, please report`,
+                    description: error.message,
+                  });
+                }
+              }
+            }}
+          >
+            {i18n.str`Create`}
+          </button>
+        </div>
+      </form>
+    </div>
+  );
+}
+
+interface ShowCashoutProps {
+  id: string;
+  onCancel: () => void;
+  onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+}
+function ShowCashout({ id, onCancel, onLoadNotOk }: ShowCashoutProps): VNode {
+  const { i18n } = useTranslationContext();
+  const result = useCashoutDetails(id);
+  const { abortCashout, confirmCashout } = useCircuitAccountAPI();
+  const [code, setCode] = useState<string | undefined>(undefined);
+  const [error, saveError] = useState<ErrorMessage | undefined>();
+  if (!result.ok) return onLoadNotOk(result);
+  const errors = undefinedIfEmpty({
+    code: !code ? i18n.str`required` : undefined,
+  });
+  const isPending = String(result.data.status).toUpperCase() === "PENDING";
+  return (
+    <div>
+      <h1>Cashout details {id}</h1>
+      {error && (
+        <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+      )}
+      <form class="pure-form">
+        <fieldset>
+          <label>
+            <i18n.Translate>Subject</i18n.Translate>
+          </label>
+          <input readOnly value={result.data.subject} />
+        </fieldset>
+        <fieldset>
+          <label>
+            <i18n.Translate>Created</i18n.Translate>
+          </label>
+          <input readOnly value={result.data.creation_time ?? ""} />
+        </fieldset>
+        <fieldset>
+          <label>
+            <i18n.Translate>Confirmed</i18n.Translate>
+          </label>
+          <input readOnly value={result.data.confirmation_time ?? ""} />
+        </fieldset>
+        <fieldset>
+          <label>
+            <i18n.Translate>Debited</i18n.Translate>
+          </label>
+          <input readOnly value={result.data.amount_debit} />
+        </fieldset>
+        <fieldset>
+          <label>
+            <i18n.Translate>Credit</i18n.Translate>
+          </label>
+          <input readOnly value={result.data.amount_credit} />
+        </fieldset>
+        <fieldset>
+          <label>
+            <i18n.Translate>Status</i18n.Translate>
+          </label>
+          <input readOnly value={result.data.status} />
+        </fieldset>
+        {isPending ? (
+          <fieldset>
+            <label>
+              <i18n.Translate>Code</i18n.Translate>
+            </label>
+            <input
+              value={code ?? ""}
+              onChange={(e) => {
+                setCode(e.currentTarget.value);
+              }}
+            />
+            <ShowInputErrorLabel
+              message={errors?.code}
+              isDirty={code !== undefined}
+            />
+          </fieldset>
+        ) : undefined}
+      </form>
+      <br />
+      <div style={{ display: "flex", justifyContent: "space-between" }}>
+        <button
+          class="pure-button pure-button-secondary btn-cancel"
+          onClick={(e) => {
+            e.preventDefault();
+            onCancel();
+          }}
+        >
+          {i18n.str`Back`}
+        </button>
+        {isPending ? (
+          <div>
+            <button
+              type="submit"
+              class="pure-button pure-button-primary button-error"
+              onClick={async (e) => {
+                e.preventDefault();
+                try {
+                  const rest = await abortCashout(id);
+                  onCancel();
+                } catch (error) {
+                  if (error instanceof RequestError) {
+                    const errorData: SandboxBackend.SandboxError =
+                      error.info.error;
+                    if (
+                      error.info.status === HttpStatusCode.PreconditionFailed
+                    ) {
+                      saveError({
+                        title: i18n.str`Cashout was already aborted`,
+                        description: errorData.error.description,
+                        debug: JSON.stringify(error.info),
+                      });
+                    } else {
+                      saveError({
+                        title: i18n.str`Aborting cashout gave response error`,
+                        description: errorData.error.description,
+                        debug: JSON.stringify(error.info),
+                      });
+                    }
+                  } else if (error instanceof Error) {
+                    saveError({
+                      title: i18n.str`Aborting failed, please report`,
+                      description: error.message,
+                    });
+                  }
+                }
+              }}
+            >
+              {i18n.str`Abort`}
+            </button>
+            &nbsp;
+            <button
+              type="submit"
+              disabled={!code}
+              class="pure-button pure-button-primary "
+              onClick={async (e) => {
+                e.preventDefault();
+                try {
+                  if (!code) return;
+                  const rest = await confirmCashout(id, {
+                    tan: code,
+                  });
+                } catch (error) {
+                  if (error instanceof RequestError) {
+                    const errorData: SandboxBackend.SandboxError =
+                      error.info.error;
+                    saveError({
+                      title: i18n.str`Confirmation of cashout gave response 
error`,
+                      description: errorData.error.description,
+                      debug: JSON.stringify(error.info),
+                    });
+                  } else if (error instanceof Error) {
+                    saveError({
+                      title: i18n.str`Confirmation failed, please report`,
+                      description: error.message,
+                    });
+                  }
+                }
+              }}
+            >
+              {i18n.str`Confirm`}
+            </button>
+          </div>
+        ) : (
+          <div />
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx 
b/packages/demobank-ui/src/pages/HomePage.tsx
index 76eb8d515..5af195f48 100644
--- a/packages/demobank-ui/src/pages/HomePage.tsx
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -50,7 +50,6 @@ export function HomePage({ onRegister }: { onRegister: () => 
void }): VNode {
   }
 
   function saveErrorAndLogout(error: PageStateType["error"]): void {
-    console.log("rrot", error);
     saveError(error);
     backend.logOut();
   }
@@ -124,7 +123,6 @@ function handleNotOkResult(
   return function handleNotOkResult2<T, E>(
     result: HttpResponsePaginated<T, E>,
   ): VNode {
-    console.log("qweqwe", JSON.stringify(result, undefined, 2));
     if (result.clientError && result.isUnauthorized) {
       onErrorHandler({
         title: i18n.str`Wrong credentials for "${account}"`,
diff --git a/packages/demobank-ui/src/pages/Routing.tsx 
b/packages/demobank-ui/src/pages/Routing.tsx
index 55317f4ed..48f226574 100644
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ b/packages/demobank-ui/src/pages/Routing.tsx
@@ -19,7 +19,7 @@ import {
   useTranslationContext,
 } from "@gnu-taler/web-util/lib/index.browser";
 import { createHashHistory } from "history";
-import { h, VNode, } from "preact";
+import { h, VNode } from "preact";
 import { Router, route, Route } from "preact-router";
 import { useEffect } from "preact/hooks";
 import { Loading } from "../components/Loading.js";
diff --git a/packages/demobank-ui/src/scss/bank.scss 
b/packages/demobank-ui/src/scss/bank.scss
index 2bd5f317a..16370227b 100644
--- a/packages/demobank-ui/src/scss/bank.scss
+++ b/packages/demobank-ui/src/scss/bank.scss
@@ -278,3 +278,35 @@ h1.nav {
 .pure-form > fieldset > input[disabled] {
   color: black !important;
 }
+.pure-form > fieldset > div > input[disabled] {
+  color: black !important;
+}
+
+.pure-form > fieldset > div.channel > div {
+  display: inline-block;
+  margin: 1em;
+  border: 1px black solid;
+  width: fit-content;
+  padding: 0.4em;
+  cursor: pointer;
+}
+
+.button-success {
+  background: rgb(28, 184, 65);
+  /* this is a green */
+}
+
+.button-error {
+  background: rgb(202, 60, 60);
+  /* this is a maroon */
+}
+
+.button-warning {
+  background: rgb(223, 117, 20);
+  /* this is an orange */
+}
+
+.button-secondary {
+  background: rgb(66, 184, 221);
+  /* this is a light blue */
+}
diff --git a/packages/demobank-ui/src/scss/main.scss 
b/packages/demobank-ui/src/scss/main.scss
index b92260af0..b9a46718f 100644
--- a/packages/demobank-ui/src/scss/main.scss
+++ b/packages/demobank-ui/src/scss/main.scss
@@ -1,4 +1,5 @@
 @use "pure";
 @use "bank";
 @use "demo";
+@use "toggle";
 @use "colors-bank";
diff --git a/packages/demobank-ui/src/scss/toggle.scss 
b/packages/demobank-ui/src/scss/toggle.scss
new file mode 100644
index 000000000..24636da2f
--- /dev/null
+++ b/packages/demobank-ui/src/scss/toggle.scss
@@ -0,0 +1,51 @@
+$green: #56c080;
+
+.toggle {
+  cursor: pointer;
+  display: inline-block;
+}
+.toggle-switch {
+  display: inline-block;
+  background: #ccc;
+  border-radius: 16px;
+  width: 58px;
+  height: 32px;
+  position: relative;
+  vertical-align: middle;
+  transition: background 0.25s;
+  &:before,
+  &:after {
+    content: "";
+  }
+  &:before {
+    display: block;
+    background: linear-gradient(to bottom, #fff 0%, #eee 100%);
+    border-radius: 50%;
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
+    width: 24px;
+    height: 24px;
+    position: absolute;
+    top: 4px;
+    left: 4px;
+    transition: left 0.25s;
+  }
+  .toggle:hover &:before {
+    background: linear-gradient(to bottom, #fff 0%, #fff 100%);
+    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
+  }
+  .toggle-checkbox:checked + & {
+    background: $green;
+    &:before {
+      left: 30px;
+    }
+  }
+}
+.toggle-checkbox {
+  position: absolute;
+  visibility: hidden;
+}
+.toggle-label {
+  margin-left: 5px;
+  position: relative;
+  top: 2px;
+}
diff --git a/packages/demobank-ui/src/utils.ts 
b/packages/demobank-ui/src/utils.ts
index 642b3c68d..49b9ac276 100644
--- a/packages/demobank-ui/src/utils.ts
+++ b/packages/demobank-ui/src/utils.ts
@@ -59,6 +59,21 @@ export type WithIntermediate<Type extends object> = {
     : Type[prop] | undefined;
 };
 
+export enum TanChannel {
+  SMS = "sms",
+  EMAIL = "email",
+  FILE = "file",
+}
+export enum CashoutStatus {
+  // The payment was initiated after a valid
+  // TAN was received by the bank.
+  CONFIRMED = "confirmed",
+
+  // The cashout was created and now waits
+  // for the TAN by the author.
+  PENDING = "pending",
+}
+
 // export function partialWithObjects<T extends object>(obj: T | undefined, () 
=> complete): WithIntermediate<T> {
 //   const root = obj === undefined ? {} : obj;
 //   return Object.entries(root).([key, value]) => {

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