gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: conversion UI


From: gnunet
Subject: [taler-wallet-core] branch master updated: conversion UI
Date: Tue, 27 Feb 2024 05:18:29 +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 ee40a5e25 conversion UI
ee40a5e25 is described below

commit ee40a5e25c44ef478ee13426549e548d2610a215
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Feb 27 01:18:23 2024 -0300

    conversion UI
---
 packages/demobank-ui/src/context/config.ts         |  28 +-
 packages/demobank-ui/src/hooks/circuit.ts          |  61 +-
 .../demobank-ui/src/pages/ConversionConfig.tsx     | 982 ++++++++++++++++-----
 .../src/pages/PaytoWireTransferForm.tsx            | 180 ++--
 .../src/pages/business/CreateCashout.tsx           |   1 +
 packages/web-util/src/components/utils.ts          |  28 +
 6 files changed, 954 insertions(+), 326 deletions(-)

diff --git a/packages/demobank-ui/src/context/config.ts 
b/packages/demobank-ui/src/context/config.ts
index 1cabab51c..529108275 100644
--- a/packages/demobank-ui/src/context/config.ts
+++ b/packages/demobank-ui/src/context/config.ts
@@ -16,7 +16,13 @@
 
 import {
   AccessToken,
+  AmountJson,
+  HttpStatusCode,
   LibtoolVersion,
+  OperationFail,
+  OperationOk,
+  TalerBankConversionApi,
+  TalerBankConversionHttpClient,
   TalerCorebankApi,
   TalerCoreBankHttpClient,
   TalerError,
@@ -44,6 +50,7 @@ import {
 import {
   revalidateBusinessAccounts,
   revalidateCashouts,
+  revalidateConversionInfo,
 } from "../hooks/circuit.js";
 
 /**
@@ -89,7 +96,7 @@ export const BankCoreApiProvider = ({
   const [checked, setChecked] = useState<ConfigResult>();
   const { i18n } = useTranslationContext();
   const url = new URL(baseUrl);
-  const api = new CacheAwareApi(url.href, new BrowserHttpLib());
+  const api = new CacheAwareTalerCoreBankHttpClient(url.href, new 
BrowserHttpLib());
   useEffect(() => {
     api
       .getConfig()
@@ -149,8 +156,20 @@ export const BankCoreApiProvider = ({
     children,
   });
 };
+class CacheAwareTalerBankConversionHttpClient extends 
TalerBankConversionHttpClient {
+  constructor(baseUrl: string, httpClient?: HttpRequestLibrary) {
+    super(baseUrl, httpClient);
+  }
+  async updateConversionRate(auth: AccessToken, body: 
TalerBankConversionApi.ConversionRate) {
+    const resp = await super.updateConversionRate(auth, body);
+    if (resp.type === "ok") {
+      await revalidateConversionInfo();
+    }
+    return resp
+  }
+}
 
-export class CacheAwareApi extends TalerCoreBankHttpClient {
+class CacheAwareTalerCoreBankHttpClient extends TalerCoreBankHttpClient {
   constructor(baseUrl: string, httpClient?: HttpRequestLibrary) {
     super(baseUrl, httpClient);
   }
@@ -223,6 +242,11 @@ export class CacheAwareApi extends TalerCoreBankHttpClient 
{
     }
     return resp;
   }
+
+  getConversionInfoAPI(): TalerBankConversionHttpClient {
+    const api = super.getConversionInfoAPI();
+    return new CacheAwareTalerBankConversionHttpClient(api.baseUrl, 
this.httpLib)
+  }
 }
 
 export const BankCoreApiProviderTesting = ({
diff --git a/packages/demobank-ui/src/hooks/circuit.ts 
b/packages/demobank-ui/src/hooks/circuit.ts
index 7d8884797..2c0a58a5e 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -47,7 +47,7 @@ type EstimatorFunction = (
   fee: AmountJson,
 ) => Promise<TransferCalculation>;
 
-type CashoutEstimators = {
+type ConversionEstimators = {
   estimateByCredit: EstimatorFunction;
   estimateByDebit: EstimatorFunction;
 };
@@ -84,7 +84,53 @@ export function useConversionInfo() {
   return undefined;
 }
 
-export function useEstimator(): CashoutEstimators {
+export function useCashinEstimator(): ConversionEstimators {
+  const { api } = useBankCoreApiContext();
+  return {
+    estimateByCredit: async (fiatAmount, fee) => {
+      const resp = await api.getConversionInfoAPI().getCashinRate({
+        credit: fiatAmount,
+      });
+      if (resp.type === "fail") {
+        // can't happen
+        // not-supported: it should not be able to call this function
+        // wrong-calculation: we are using just one parameter
+        throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+      }
+      const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+      const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+      const beforeFee = Amounts.sub(credit, fee).amount;
+
+      return {
+        debit,
+        beforeFee,
+        credit,
+      };
+    },
+    estimateByDebit: async (regionalAmount, fee) => {
+      const resp = await api.getConversionInfoAPI().getCashinRate({
+        debit: regionalAmount,
+      });
+      if (resp.type === "fail") {
+        // can't happen
+        // not-supported: it should not be able to call this function
+        // wrong-calculation: we are using just one parameter
+        throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+      }
+      const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+      const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+      const beforeFee = Amounts.add(credit, fee).amount;
+
+      return {
+        debit,
+        beforeFee,
+        credit,
+      };
+    },
+  };
+}
+
+export function useCashoutEstimator(): ConversionEstimators {
   const { api } = useBankCoreApiContext();
   return {
     estimateByCredit: async (fiatAmount, fee) => {
@@ -130,6 +176,13 @@ export function useEstimator(): CashoutEstimators {
   };
 }
 
+/**
+ * @deprecated use useCashoutEstimator
+ */
+export function useEstimator(): ConversionEstimators {
+  return useCashoutEstimator()
+}
+
 export function revalidateBusinessAccounts() {
   return mutate((key) => Array.isArray(key) && key[key.length - 1] === 
"getAccounts", undefined, { revalidate: true });
 }
@@ -147,7 +200,7 @@ export function useBusinessAccounts() {
       token,
       {},
       {
-        limit: PAGE_SIZE+1,
+        limit: PAGE_SIZE + 1,
         offset: String(offset),
         order: "asc",
       },
@@ -174,7 +227,7 @@ export function useBusinessAccounts() {
   const isFirstPage = !offset;
 
   const result = data && data.type == "ok" ? 
structuredClone(data.body.accounts) : []
-  if (result.length == PAGE_SIZE+1) {
+  if (result.length == PAGE_SIZE + 1) {
     result.pop()
   }
   const pagination = {
diff --git a/packages/demobank-ui/src/pages/ConversionConfig.tsx 
b/packages/demobank-ui/src/pages/ConversionConfig.tsx
index 73a6ab3ee..efe2d1756 100644
--- a/packages/demobank-ui/src/pages/ConversionConfig.tsx
+++ b/packages/demobank-ui/src/pages/ConversionConfig.tsx
@@ -15,29 +15,32 @@
  */
 
 import {
-  AmountString,
+  AmountJson,
   Amounts,
   HttpStatusCode,
-  OperationOk,
-  OperationResult,
   TalerBankConversionApi,
+  TalerError,
   TranslatedString,
   assertUnreachable
 } from "@gnu-taler/taler-util";
 import {
+  Attention,
+  InternationalizationAPI,
   LocalNotificationBanner,
   ShowInputErrorLabel,
   useLocalNotification,
-  useTranslationContext
+  useTranslationContext,
+  utils
 } from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
+import { useEffect, useState } from "preact/hooks";
 import { useBankCoreApiContext } from "../context/config.js";
 import { useBackendState } from "../hooks/backend.js";
+import { TransferCalculation, useCashinEstimator, useCashoutEstimator, 
useConversionInfo } from "../hooks/circuit.js";
 import { RouteDefinition } from "../route.js";
-import { ProfileNavigation } from "./ProfileNavigation.js";
-import { useState } from "preact/hooks";
 import { undefinedIfEmpty } from "../utils.js";
-import { InputAmount } from "./PaytoWireTransferForm.js";
+import { InputAmount, RenderAmount } from "./PaytoWireTransferForm.js";
+import { ProfileNavigation } from "./ProfileNavigation.js";
 
 interface Props {
   routeMyAccountDetails: RouteDefinition;
@@ -49,285 +52,806 @@ interface Props {
   onUpdateSuccess: () => void;
 }
 
-type FormType<T> = {
-  [k in keyof T]: string | undefined;
+type UIField = {
+  value: string | undefined;
+  onUpdate: (s: string) => void;
+  error: TranslatedString | undefined;
+}
+
+type FormHandler<T> = {
+  [k in keyof T]?:
+  T[k] extends string ? UIField :
+  T[k] extends AmountJson ? UIField :
+  FormHandler<T[k]>;
 }
 
-type ErrorsType<T> = {
-  [k in keyof T]?: TranslatedString;
+type FormValues<T> = {
+  [k in keyof T]:
+  T[k] extends string ? (string | undefined) :
+  T[k] extends AmountJson ? (string | undefined) :
+  FormValues<T[k]>;
 }
 
+type RecursivePartial<T> = {
+  [k in keyof T]?:
+  T[k] extends string ? (string) :
+  T[k] extends AmountJson ? (AmountJson) :
+  RecursivePartial<T[k]>;
+}
 
-type FormHandler<T> = {
-  [k in keyof T]?: {
-    value: string | undefined;
-    onUpdate: (s: string) => void;
-    error: TranslatedString | undefined;
-  }
+type FormErrors<T> = {
+  [k in keyof T]?:
+  T[k] extends string ? (TranslatedString) :
+  T[k] extends AmountJson ? (TranslatedString) :
+  FormErrors<T[k]>;
 }
-function useFormState<T>(defaultValue: FormType<T>, validate: (f: FormType<T>) 
=> ErrorsType<T>): FormHandler<T> {
-  const [form, updateForm] = useState<FormType<T>>(defaultValue)
-
-  const errors = undefinedIfEmpty<ErrorsType<T>>(validate(form))
-
-  const p = (Object.keys(form) as Array<keyof T>)
-  console.log("FORM", p)
-  const handler = p.reduce((prev, fieldName) => {
-    console.log("fie;d", fieldName)
-    const currentValue = form[fieldName]
-    const currentError = errors !== undefined ? errors[fieldName] : undefined
-    prev[fieldName] = {
+
+type FormStatus<T> = {
+  status: "ok",
+  result: T,
+  errors: undefined,
+} | {
+  status: "fail",
+  result: RecursivePartial<T>,
+  errors: FormErrors<T>,
+}
+type FormType = { amount: AmountJson, conv: 
TalerBankConversionApi.ConversionRate }
+
+function constructFormHandler<T>(form: FormValues<T>, updateForm: (d: 
FormValues<T>) => void, errors: FormErrors<T> | undefined): FormHandler<T> {
+  const keys = (Object.keys(form) as Array<keyof T>)
+
+  const handler = keys.reduce((prev, fieldName) => {
+    const currentValue: any = form[fieldName];
+    const currentError: any = errors ? errors[fieldName] : undefined;
+    function updater(newValue: any) {
+      updateForm({ ...form, [fieldName]: newValue })
+    }
+    if (typeof currentValue === "object") {
+      const group = constructFormHandler(currentValue, updater, currentError)
+      // @ts-expect-error asdasd
+      prev[fieldName] = group
+      return prev;
+    }
+    const field: UIField = {
       error: currentError,
       value: currentValue,
-      onUpdate: (newValue) => {
-        updateForm({ ...form, [fieldName]: newValue })
-      }
+      onUpdate: updater
     }
+    // @ts-expect-error asdasd
+    prev[fieldName] = field
     return prev
   }, {} as FormHandler<T>)
 
-  return handler
+  return handler;
 }
 
-/**
- * Show histories of public accounts.
- */
-export function ConversionConfig({
+function useFormState<T>(defaultValue: FormValues<T>, check: (f: 
FormValues<T>) => FormStatus<T>): [FormHandler<T>, FormStatus<T>] {
+  const [form, updateForm] = useState<FormValues<T>>(defaultValue)
+
+  const status = check(form)
+  const handler = constructFormHandler(form, updateForm, status.errors)
+
+  return [handler, status]
+}
+
+function useComponentState({
+  onUpdateSuccess,
+  routeCancel,
+  routeConversionConfig,
   routeMyAccountCashout,
   routeMyAccountDelete,
   routeMyAccountDetails,
   routeMyAccountPassword,
-  routeConversionConfig,
-  routeCancel,
-  onUpdateSuccess,
-}: Props): VNode {
-  const { i18n } = useTranslationContext();
+}: Props): utils.RecursiveState<VNode> {
+
+  const result = useConversionInfo()
+  const info = result && !(result instanceof TalerError) && result.type === 
"ok" ?
+    result.body : undefined;
 
   const { state: credentials } = useBackendState();
   const creds =
     credentials.status !== "loggedIn" || !credentials.isUserAdministrator
       ? undefined
       : credentials;
-  const { api, config } = useBankCoreApiContext();
 
-  const [notification, notify, handleError] = useLocalNotification();
+  if (!info) {
+    return <div>waiting...</div>
+  }
 
   if (!creds) {
     return <div>only admin can setup conversion</div>;
   }
 
-  const form = useFormState<TalerBankConversionApi.ConversionRate>({
-    cashin_min_amount: undefined,
-    cashin_tiny_amount: undefined,
-    cashin_fee: undefined,
-    cashin_ratio: undefined,
-    cashin_rounding_mode: undefined,
-    cashout_min_amount: undefined,
-    cashout_tiny_amount: undefined,
-    cashout_fee: undefined,
-    cashout_ratio: undefined,
-    cashout_rounding_mode: undefined,
-  }, (state) => {
-    return ({
-      cashin_min_amount: !state.cashin_min_amount ? i18n.str`required` :
-        !Amounts.parse(`${config.currency}:${state.cashin_min_amount}`) ? 
i18n.str`invalid` :
-          undefined,
+  return () => {
+    const { i18n } = useTranslationContext();
 
-    })
-  })
-
-
-  async function doUpdate() {
-    if (!creds) return
-    await handleError(async () => {
-      const resp = await api
-        .getConversionInfoAPI()
-        .updateConversionRate(creds.token, {
-
-        } as any)
-      if (resp.type === "ok") {
-        onUpdateSuccess()
-      } else {
-        switch (resp.case) {
-          case HttpStatusCode.Unauthorized: {
-            return notify({
-              type: "error",
-              title: i18n.str`Wrong credentials`,
-              description: resp.detail.hint as TranslatedString,
-              debug: resp.detail,
-            });
-          }
-          case HttpStatusCode.NotImplemented: {
-            return notify({
-              type: "error",
-              title: i18n.str`Conversion is disabled`,
-              description: resp.detail.hint as TranslatedString,
-              debug: resp.detail,
-            });
+    const { api, config } = useBankCoreApiContext();
+
+    const [notification, notify, handleError] = useLocalNotification();
+
+    const initalState: FormValues<FormType> = {
+      amount: "100",
+      conv: {
+        cashin_min_amount: 
info.conversion_rate.cashin_min_amount.split(":")[1],
+        cashin_tiny_amount: 
info.conversion_rate.cashin_tiny_amount.split(":")[1],
+        cashin_fee: info.conversion_rate.cashin_fee.split(":")[1],
+        cashin_ratio: info.conversion_rate.cashin_ratio,
+        cashin_rounding_mode: info.conversion_rate.cashin_rounding_mode,
+        cashout_min_amount: 
info.conversion_rate.cashout_min_amount.split(":")[1],
+        cashout_tiny_amount: 
info.conversion_rate.cashout_tiny_amount.split(":")[1],
+        cashout_fee: info.conversion_rate.cashout_fee.split(":")[1],
+        cashout_ratio: info.conversion_rate.cashout_ratio,
+        cashout_rounding_mode: info.conversion_rate.cashout_rounding_mode,
+      }
+    }
+
+    const [form, status] = useFormState<FormType>(
+      initalState,
+      checkConversionForm(i18n, info.regional_currency, info.fiat_currency)
+    )
+
+    const {
+      estimateByDebit: calculateCashoutFromDebit,
+    } = useCashoutEstimator();
+
+    const {
+      estimateByDebit: calculateCashinFromDebit,
+    } = useCashinEstimator();
+
+    const [calc, setCalc] = useState<{ cashin: TransferCalculation, cashout: 
TransferCalculation }>()
+
+    useEffect(() => {
+      async function doAsync() {
+        await handleError(async () => {
+          if (!info) return;
+          if (!form.amount?.value || form.amount.error) return;
+          const in_amount = 
Amounts.parseOrThrow(`${info.fiat_currency}:${form.amount.value}`)
+          const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee)
+          const cashin = await calculateCashinFromDebit(in_amount, in_fee);
+
+
+          // const out_amount = 
Amounts.parseOrThrow(`${info.regional_currency}:${form.amount.value}`)
+          const out_fee = 
Amounts.parseOrThrow(info.conversion_rate.cashout_fee)
+          const cashout = await calculateCashoutFromDebit(cashin.credit, 
out_fee);
+
+          setCalc({ cashin, cashout });
+        });
+      }
+      doAsync();
+    }, [form.amount?.value, form.conv?.cashin_fee?.value, 
form.conv?.cashout_fee?.value]);
+
+    const [section, setSection] = useState<"detail" | "cashout" | 
"cashin">("detail")
+
+    async function doUpdate() {
+      if (!creds) return
+      await handleError(async () => {
+        if (status.status === "fail") return;
+        const resp = await api
+          .getConversionInfoAPI()
+          .updateConversionRate(creds.token, status.result.conv)
+        if (resp.type === "ok") {
+          setSection("detail")
+        } else {
+          switch (resp.case) {
+            case HttpStatusCode.Unauthorized: {
+              return notify({
+                type: "error",
+                title: i18n.str`Wrong credentials`,
+                description: resp.detail.hint as TranslatedString,
+                debug: resp.detail,
+              });
+            }
+            case HttpStatusCode.NotImplemented: {
+              return notify({
+                type: "error",
+                title: i18n.str`Conversion is disabled`,
+                description: resp.detail.hint as TranslatedString,
+                debug: resp.detail,
+              });
+            }
+            default:
+              assertUnreachable(resp);
           }
-          default:
-            assertUnreachable(resp);
         }
-      }
-    });
-  }
+      });
+    }
+
+    const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio)
+    const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio)
 
+    const both_high = in_ratio > 1 && out_ratio > 1;
+    const both_low = in_ratio < 1 && out_ratio < 1;
 
-  return (
-    <div>
-      <ProfileNavigation current="conversion"
-        routeMyAccountCashout={routeMyAccountCashout}
-        routeMyAccountDelete={routeMyAccountDelete}
-        routeMyAccountDetails={routeMyAccountDetails}
-        routeMyAccountPassword={routeMyAccountPassword}
-        routeConversionConfig={routeConversionConfig}
-      />
+    return (
+      <div>
+        <ProfileNavigation current="conversion"
+          routeMyAccountCashout={routeMyAccountCashout}
+          routeMyAccountDelete={routeMyAccountDelete}
+          routeMyAccountDetails={routeMyAccountDetails}
+          routeMyAccountPassword={routeMyAccountPassword}
+          routeConversionConfig={routeConversionConfig}
+        />
 
-      <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">
         <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">
-            <i18n.Translate>Conversion</i18n.Translate>
-          </h2>
-        </div>
+          <div class="px-4 sm:px-0">
+            <h2 class="text-base font-semibold leading-7 text-gray-900">
+              <i18n.Translate>Conversion</i18n.Translate>
+            </h2>
+            <div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
+              <label
+                data-enabled={section === "detail"}
+                class="relative flex cursor-pointer rounded-lg border bg-white 
p-4 shadow-sm focus:outline-none border-gray-300 -- 
data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 
data-[enabled=true]:ring-indigo-600"
+              >
+                <input
+                  type="radio"
+                  name="project-type"
+                  value="Newsletter"
+                  class="sr-only"
+                  aria-labelledby="project-type-0-label"
+                  aria-describedby="project-type-0-description-0 
project-type-0-description-1"
+                  onChange={() => {
+                    setSection("detail")
+                  }}
+                />
+                <span class="flex flex-1">
+                  <span class="flex flex-col">
+                    <span class="block text-sm  font-medium text-gray-900">
+                      <i18n.Translate>Details</i18n.Translate>
+                    </span>
+                  </span>
+                </span>
+              </label>
 
-        <form
-          class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl 
md:col-span-2"
-          autoCapitalize="none"
-          autoCorrect="off"
-          onSubmit={(e) => {
-            e.preventDefault();
-          }}
-        >
-          <div class="px-6 pt-6">
-            <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
-              <div class="sm:col-span-5">
-                <label
-                  for="cashout_amount_min"
-                  class="block text-sm font-medium leading-6 text-gray-900"
-                >{i18n.str`Minimum amount`}</label>
-                <InputAmount
-                  name="cashout_amount_min"
-                  left
-                  currency={config.currency}
-                  value={form.cashin_min_amount?.value ?? ""}
-                  onChange={form.cashin_min_amount?.onUpdate}
+              <label
+                data-enabled={section === "cashout"}
+                class="relative flex cursor-pointer rounded-lg border bg-white 
p-4 shadow-sm focus:outline-none border-gray-300 -- 
data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 
data-[enabled=true]:ring-indigo-600"
+              >
+                <input
+                  type="radio"
+                  name="project-type"
+                  value="Existing Customers"
+                  class="sr-only"
+                  aria-labelledby="project-type-1-label"
+                  aria-describedby="project-type-1-description-0 
project-type-1-description-1"
+                  onChange={() => {
+                    setSection("cashout")
+                  }}
                 />
-                <ShowInputErrorLabel
-                  message={form.cashin_min_amount?.error}
-                  isDirty={form.cashin_min_amount?.value !== undefined}
+                <span class="flex flex-1">
+                  <span class="flex flex-col">
+                    <span class="block text-sm font-medium text-gray-900">
+                      <i18n.Translate>Config cashout</i18n.Translate>
+                    </span>
+                  </span>
+                </span>
+              </label>
+              <label
+                data-enabled={section === "cashin"}
+                class="relative flex cursor-pointer rounded-lg border bg-white 
p-4 shadow-sm focus:outline-none border-gray-300 -- 
data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 
data-[enabled=true]:ring-indigo-600"
+              >
+                <input
+                  type="radio"
+                  name="project-type"
+                  value="Existing Customers"
+                  class="sr-only"
+                  aria-labelledby="project-type-1-label"
+                  aria-describedby="project-type-1-description-0 
project-type-1-description-1"
+                  onChange={() => {
+                    setSection("cashin")
+                  }}
                 />
-                <p class="mt-2 text-sm text-gray-500">
-                  <i18n.Translate>Only cashout operation above this threshold 
will be allowed</i18n.Translate>
-                </p>
-              </div>
+                <span class="flex flex-1">
+                  <span class="flex flex-col">
+                    <span class="block text-sm font-medium text-gray-900">
+                      <i18n.Translate>Config cashin</i18n.Translate>
+                    </span>
+                  </span>
+                </span>
+              </label>
             </div>
+
           </div>
 
-          <div class="px-6 pt-6">
-            <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
-              <div class="sm:col-span-5">
+          <form
+            class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl 
md:col-span-2"
+            autoCapitalize="none"
+            autoCorrect="off"
+            onSubmit={(e) => {
+              e.preventDefault();
+            }}
+          >
+            {section == "cashin" && <Fragment>
+              <div class="px-6 pt-6">
+                <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                  <div class="sm:col-span-5">
+                    <label
+                      for="cashin_min_amount"
+                      class="block text-sm font-medium leading-6 text-gray-900"
+                    >{i18n.str`Minimum amount`}</label>
+                    <InputAmount
+                      name="cashin_min_amount"
+                      left
+                      currency={config.currency}
+                      value={form.conv?.cashin_min_amount?.value ?? ""}
+                      onChange={form.conv?.cashin_min_amount?.onUpdate}
+                    />
+                    <ShowInputErrorLabel
+                      message={form.conv?.cashin_min_amount?.error}
+                      isDirty={form.conv?.cashin_min_amount?.value !== 
undefined}
+                    />
+                    <p class="mt-2 text-sm text-gray-500">
+                      <i18n.Translate>Only cashout operation above this 
threshold will be allowed</i18n.Translate>
+                    </p>
+                  </div>
+                </div>
+              </div>
+
+              <div class="px-6 pt-6">
+                <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                  <div class="sm:col-span-5">
+                    <label
+                      for="cashin_tiny_amount"
+                      class="block text-sm font-medium leading-6 text-gray-900"
+                    >{i18n.str`Minimum difference`}</label>
+                    <InputAmount
+                      name="cashin_tiny_amount"
+                      left
+                      currency={config.currency}
+                      value={form.conv?.cashin_tiny_amount?.value ?? ""}
+                      onChange={form.conv?.cashin_tiny_amount?.onUpdate}
+                    />
+                    <ShowInputErrorLabel
+                      message={form.conv?.cashin_tiny_amount?.error}
+                      isDirty={form.conv?.cashin_tiny_amount?.value !== 
undefined}
+                    />
+                    <p class="mt-2 text-sm text-gray-500">
+                      <i18n.Translate>Smallest difference between two 
amounts</i18n.Translate>
+                    </p>
+                  </div>
+                </div>
+              </div>
+
+              <div class="px-6 pt-6">
+                <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                  <div class="sm:col-span-5">
+                    <label
+                      for="cashin_fee"
+                      class="block text-sm font-medium leading-6 text-gray-900"
+                    >{i18n.str`Fee`}</label>
+                    <InputAmount
+                      name="cashin_fee"
+                      left
+                      currency={config.currency}
+                      value={form.conv?.cashin_fee?.value ?? ""}
+                      onChange={form.conv?.cashin_fee?.onUpdate}
+                    />
+                    <ShowInputErrorLabel
+                      message={form.conv?.cashin_fee?.error}
+                      isDirty={form.conv?.cashin_fee?.value !== undefined}
+                    />
+                    <p class="mt-2 text-sm text-gray-500">
+                      <i18n.Translate>Operation fee</i18n.Translate>
+                    </p>
+                  </div>
+                </div>
+              </div>
+
+              <div class="px-6 pt-6">
                 <label
-                  for="cashout_amount_tiny"
                   class="block text-sm font-medium leading-6 text-gray-900"
-                >{i18n.str`Minimum difference`}</label>
-                <InputAmount
-                  name="cashout_amount_tiny"
-                  left
-                  currency={config.currency}
-                  value={form.cashin_min_amount?.value ?? ""}
-                  onChange={form.cashin_min_amount?.onUpdate}
-                />
-                <ShowInputErrorLabel
-                  message={form.cashin_min_amount?.error}
-                  isDirty={form.cashin_min_amount?.value !== undefined}
-                />
+                  for="password"
+                >
+                  {i18n.str`Ratio`}
+                </label>
+                <div class="mt-2">
+                  <input
+                    type="number"
+                    class="block w-full rounded-md border-0 py-1.5 
text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+                    name="current"
+                    id="cashin_ratio"
+                    data-error={!!form.conv?.cashin_ratio?.error && 
form.conv?.cashin_ratio?.value !== undefined}
+                    value={form.conv?.cashin_ratio?.value ?? ""}
+                    onChange={(e) => {
+                      form.conv?.cashin_ratio?.onUpdate(e.currentTarget.value);
+                    }}
+                    autocomplete="off"
+                  />
+                  <ShowInputErrorLabel
+                    message={form.conv?.cashin_ratio?.error}
+                    isDirty={form.conv?.cashin_ratio?.value !== undefined}
+                  />
+                </div>
                 <p class="mt-2 text-sm text-gray-500">
-                  <i18n.Translate>Smallest difference between two 
amounts</i18n.Translate>
+                  <i18n.Translate>
+                    Conversion ratio between currencies
+                  </i18n.Translate>
                 </p>
               </div>
-            </div>
-          </div>
 
-          <div class="px-6 pt-6">
-            <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
-              <div class="sm:col-span-5">
+            </Fragment>}
+
+
+
+            {section == "cashout" && <Fragment>
+              <div class="px-6 pt-6">
+                <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                  <div class="sm:col-span-5">
+                    <label
+                      for="cashout_min_amount"
+                      class="block text-sm font-medium leading-6 text-gray-900"
+                    >{i18n.str`Minimum amount`}</label>
+                    <InputAmount
+                      name="cashout_min_amount"
+                      left
+                      currency={config.currency}
+                      value={form.conv?.cashout_min_amount?.value ?? ""}
+                      onChange={form.conv?.cashout_min_amount?.onUpdate}
+                    />
+                    <ShowInputErrorLabel
+                      message={form.conv?.cashout_min_amount?.error}
+                      isDirty={form.conv?.cashout_min_amount?.value !== 
undefined}
+                    />
+                    <p class="mt-2 text-sm text-gray-500">
+                      <i18n.Translate>Only cashout operation above this 
threshold will be allowed</i18n.Translate>
+                    </p>
+                  </div>
+                </div>
+              </div>
+
+              <div class="px-6 pt-6">
+                <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                  <div class="sm:col-span-5">
+                    <label
+                      for="cashout_tiny_amount"
+                      class="block text-sm font-medium leading-6 text-gray-900"
+                    >{i18n.str`Minimum difference`}</label>
+                    <InputAmount
+                      name="cashout_tiny_amount"
+                      left
+                      currency={config.currency}
+                      value={form.conv?.cashout_tiny_amount?.value ?? ""}
+                      onChange={form.conv?.cashout_tiny_amount?.onUpdate}
+                    />
+                    <ShowInputErrorLabel
+                      message={form.conv?.cashout_tiny_amount?.error}
+                      isDirty={form.conv?.cashout_tiny_amount?.value !== 
undefined}
+                    />
+                    <p class="mt-2 text-sm text-gray-500">
+                      <i18n.Translate>Smallest difference between two 
amounts</i18n.Translate>
+                    </p>
+                  </div>
+                </div>
+              </div>
+
+              <div class="px-6 pt-6">
+                <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                  <div class="sm:col-span-5">
+                    <label
+                      for="cashout_fee"
+                      class="block text-sm font-medium leading-6 text-gray-900"
+                    >{i18n.str`Fee`}</label>
+                    <InputAmount
+                      name="cashout_fee"
+                      left
+                      currency={config.currency}
+                      value={form.conv?.cashout_fee?.value ?? ""}
+                      onChange={form.conv?.cashout_fee?.onUpdate}
+                    />
+                    <ShowInputErrorLabel
+                      message={form.conv?.cashout_fee?.error}
+                      isDirty={form.conv?.cashout_fee?.value !== undefined}
+                    />
+                    <p class="mt-2 text-sm text-gray-500">
+                      <i18n.Translate>Operation fee</i18n.Translate>
+                    </p>
+                  </div>
+                </div>
+              </div>
+
+              <div class="px-6 pt-6">
                 <label
-                  for="cashin_fee"
                   class="block text-sm font-medium leading-6 text-gray-900"
-                >{i18n.str`Fee`}</label>
-                <InputAmount
-                  name="cashin_fee"
-                  left
-                  currency={config.currency}
-                  value={form.cashin_min_amount?.value ?? ""}
-                  onChange={form.cashin_fee?.onUpdate}
-                />
-                <ShowInputErrorLabel
-                  message={form.cashin_fee?.error}
-                  isDirty={form.cashin_fee?.value !== undefined}
-                />
+                  for="password"
+                >
+                  {i18n.str`Ratio`}
+                </label>
+                <div class="mt-2">
+                  <input
+                    type="number"
+                    class="block w-full rounded-md border-0 py-1.5 
text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 
data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 
focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+                    name="current"
+                    id="cashout_ratio"
+                    data-error={!!form.conv?.cashout_ratio?.error && 
form.conv?.cashout_ratio?.value !== undefined}
+                    value={form.conv?.cashout_ratio?.value ?? ""}
+                    onChange={(e) => {
+                      
form.conv?.cashout_ratio?.onUpdate(e.currentTarget.value);
+                    }}
+                    autocomplete="off"
+                  />
+                  <ShowInputErrorLabel
+                    message={form.conv?.cashout_ratio?.error}
+                    isDirty={form.conv?.cashout_ratio?.value !== undefined}
+                  />
+                </div>
                 <p class="mt-2 text-sm text-gray-500">
-                  <i18n.Translate>Operation fee</i18n.Translate>
+                  <i18n.Translate>
+                    Conversion ratio between currencies
+                  </i18n.Translate>
                 </p>
               </div>
-            </div>
-          </div>
+            </Fragment>}
+
+
+
+
+            {section == "detail" && <Fragment>
+              <div class="px-6 pt-6">
+                <div class="justify-between items-center flex ">
+                  <dt class="text-sm text-gray-600">
+                    <i18n.Translate>Cashin ratio</i18n.Translate>
+                  </dt>
+                  <dd class="text-sm text-gray-900">
+                    {info.conversion_rate.cashin_ratio}
+                  </dd>
+                </div>
+              </div>
+
+              <div class="px-6 pt-6">
+                <div class="justify-between items-center flex ">
+                  <dt class="text-sm text-gray-600">
+                    <i18n.Translate>Cashout ratio</i18n.Translate>
+                  </dt>
+                  <dd class="text-sm text-gray-900">
+                    {info.conversion_rate.cashout_ratio}
+                  </dd>
+                </div>
+              </div>
+
+              {both_low || both_high ? <div class="p-4">
+                <Attention title={i18n.str`Bad ratios`} type="warning">
+                  <i18n.Translate>
+                    One of the ratios should be higher or equal than 1 an the 
other should be lower or equal than 1.
+                  </i18n.Translate>
+                </Attention>
+              </div> : undefined}
+
+              <div class="px-6 pt-6">
+                <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 
sm:grid-cols-6">
+                  <div class="sm:col-span-5">
+                    <label
+                      for="amount"
+                      class="block text-sm font-medium leading-6 text-gray-900"
+                    >{i18n.str`Test amount`}</label>
+                    <InputAmount
+                      name="amount"
+                      left
+                      currency={info.fiat_currency}
+                      value={form.amount?.value ?? ""}
+                      onChange={form.amount?.onUpdate}
+                    />
+                    <ShowInputErrorLabel
+                      message={form.amount?.error}
+                      isDirty={form.amount?.value !== undefined}
+                    />
+                    <p class="mt-2 text-sm text-gray-500">
+                      <i18n.Translate>Use it to test how the conversion will 
affect the amount.</i18n.Translate>
+                    </p>
+                  </div>
+                </div>
+              </div>
+
+              {!calc ? undefined : (
+                <div class="px-6 pt-6">
+                  <div class="sm:col-span-5">
+                    <dl class="mt-4 space-y-4">
+                      <div class="justify-between items-center flex ">
+                        <dt class="text-sm text-gray-600">
+                          <i18n.Translate>Sending to this bank</i18n.Translate>
+                        </dt>
+                        <dd class="text-sm text-gray-900">
+                          <RenderAmount
+                            value={calc.cashin.debit}
+                            negative
+                            withColor
+                            spec={info.regional_currency_specification}
+                          />
+                        </dd>
+                      </div>
+
+                      {Amounts.isZero(calc.cashin.beforeFee) ? undefined : (
+                        <div class="flex items-center justify-between afu ">
+                          <dt class="flex items-center text-sm text-gray-600">
+                            <span>
+                              <i18n.Translate>Converted</i18n.Translate>
+                            </span>
+                          </dt>
+                          <dd class="text-sm text-gray-900">
+                            <RenderAmount
+                              value={calc.cashin.beforeFee}
+                              spec={info.fiat_currency_specification}
+                            />
+                          </dd>
+                        </div>
+                      )}
+                      <div class="flex justify-between items-center border-t-2 
afu pt-4">
+                        <dt class="text-lg text-gray-900 font-medium">
+                          <i18n.Translate>Cashin after fee</i18n.Translate>
+                        </dt>
+                        <dd class="text-lg text-gray-900 font-medium">
+                          <RenderAmount
+                            value={calc.cashin.credit}
+                            withColor
+                            spec={info.fiat_currency_specification}
+                          />
+                        </dd>
+                      </div>
+                    </dl>
+                  </div>
+
+                  <div class="sm:col-span-5">
+                    <dl class="mt-4 space-y-4">
+                      <div class="justify-between items-center flex ">
+                        <dt class="text-sm text-gray-600">
+                          <i18n.Translate>Sending from this 
bank</i18n.Translate>
+                        </dt>
+                        <dd class="text-sm text-gray-900">
+                          <RenderAmount
+                            value={calc.cashout.debit}
+                            negative
+                            withColor
+                            spec={info.fiat_currency_specification}
+                          />
+                        </dd>
+                      </div>
 
-          <div class="px-6 pt-6">
-            <label
-              class="block text-sm font-medium leading-6 text-gray-900"
-              for="password"
-            >
-              {i18n.str`Ratio`}
-            </label>
-            <div class="mt-2">
-              <input
-                type="number"
-                class="block w-full rounded-md border-0 py-1.5 text-gray-900 
shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 
placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 
sm:text-sm sm:leading-6"
-                name="current"
-                id="cashout_ratio"
-                data-error={!!form.cashin_ratio?.error && 
form.cashout_ratio?.value !== undefined}
-                value={form.cashout_ratio?.value ?? ""}
-                onChange={(e) => {
-                  form.cashout_ratio?.onUpdate(e.currentTarget.value);
-                }}
-                autocomplete="off"
-              />
-              <ShowInputErrorLabel
-                message={form.cashin_ratio?.error}
-                isDirty={form.cashout_ratio?.value !== undefined}
-              />
+                      {Amounts.isZero(calc.cashout.beforeFee) ? undefined : (
+                        <div class="flex items-center justify-between afu">
+                          <dt class="flex items-center text-sm text-gray-600">
+                            <span>
+                              <i18n.Translate>Converted</i18n.Translate>
+                            </span>
+                          </dt>
+                          <dd class="text-sm text-gray-900">
+                            <RenderAmount
+                              value={calc.cashout.beforeFee}
+                              spec={info.regional_currency_specification}
+                            />
+                          </dd>
+                        </div>
+                      )}
+                      <div class="flex justify-between items-center border-t-2 
afu pt-4">
+                        <dt class="text-lg text-gray-900 font-medium">
+                          <i18n.Translate>Cashout after fee</i18n.Translate>
+                        </dt>
+                        <dd class="text-lg text-gray-900 font-medium">
+                          <RenderAmount
+                            value={calc.cashout.credit}
+                            withColor
+                            spec={info.regional_currency_specification}
+                          />
+                        </dd>
+                      </div>
+                    </dl>
+                  </div>
+
+                  {calc && status.status === "ok" && 
Amounts.cmp(status.result.amount, calc.cashout.credit) < 0 ? <div class="p-4">
+                    <Attention title={i18n.str`Bad configuration`} 
type="warning">
+                      <i18n.Translate>
+                        This configuration allows users to cash out more of 
what has been cashed in.
+                      </i18n.Translate>
+                    </Attention>
+                  </div> : undefined}
+                </div>
+              )}
+            </Fragment>}
+
+
+            <div class="flex items-center justify-between mt-4 gap-x-6 
border-t border-gray-900/10 px-4 py-4">
+              <a name="cancel"
+                href={routeCancel.url({})}
+                class="text-sm font-semibold leading-6 text-gray-900"
+              >
+                <i18n.Translate>Cancel</i18n.Translate>
+              </a>
+              {section == "cashin" || section == "cashout" ? <Fragment>
+                <button
+                  type="submit"
+                  name="update conversion"
+                  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={async () => {
+                    doUpdate()
+                  }}
+                >
+                  <i18n.Translate>Update</i18n.Translate>
+                </button>
+              </Fragment> : <div />}
             </div>
-            <p class="mt-2 text-sm text-gray-500">
-              <i18n.Translate>
-                Your current password, for security
-              </i18n.Translate>
-            </p>
-          </div>
 
-          <div class="flex items-center justify-between mt-6 gap-x-6 border-t 
border-gray-900/10 px-4 py-4 sm:px-8">
-            <a name="cancel"
-              href={routeCancel.url({})}
-              class="text-sm font-semibold leading-6 text-gray-900"
-            >
-              <i18n.Translate>Cancel</i18n.Translate>
-            </a>
-            <button
-              type="submit"
-              name="update conversion"
-              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={async () => {
-                doUpdate()
-              }}
-            >
-              <i18n.Translate>Update</i18n.Translate>
-            </button>
-          </div>
-        </form>
+
+          </form>
+        </div>
       </div>
-    </div>
-  );
+    );
+
+  }
+}
+
+/**
+ * Show histories of public accounts.
+ */
+export const ConversionConfig = utils.recursive(useComponentState);
+
+function checkConversionForm(i18n: InternationalizationAPI, regional: string, 
fiat: string) {
+  return function check(state: FormValues<FormType>): FormStatus<FormType> {
+
+    const cashin_min_amount = 
Amounts.parse(`${fiat}:${state.conv.cashin_min_amount}`)
+    const cashin_tiny_amount = 
Amounts.parse(`${regional}:${state.conv.cashin_tiny_amount}`)
+    const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`)
+
+    const cashout_min_amount = 
Amounts.parse(`${regional}:${state.conv.cashout_min_amount}`)
+    const cashout_tiny_amount = 
Amounts.parse(`${fiat}:${state.conv.cashout_tiny_amount}`)
+    const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`)
+
+    const am = Amounts.parse(`${fiat}:${state.amount}`)
+
+    const cashin_ratio = Number.parseFloat(state.conv.cashin_ratio ?? "")
+    const cashout_ratio = Number.parseFloat(state.conv.cashout_ratio ?? "")
+
+    const errors = undefinedIfEmpty<FormErrors<FormType>>({
+      conv: undefinedIfEmpty<FormErrors<FormType["conv"]>>({
+        cashin_min_amount: !state.conv.cashin_min_amount ? i18n.str`required` :
+          !cashin_min_amount ? i18n.str`invalid` :
+            undefined,
+        cashin_tiny_amount: !state.conv.cashin_tiny_amount ? 
i18n.str`required` :
+          !cashin_tiny_amount ? i18n.str`invalid` :
+            undefined,
+        cashin_fee: !state.conv.cashin_fee ? i18n.str`required` :
+          !cashin_fee ? i18n.str`invalid` :
+            undefined,
+
+        cashout_min_amount: !state.conv.cashout_min_amount ? 
i18n.str`required` :
+          !cashout_min_amount ? i18n.str`invalid` :
+            undefined,
+        cashout_tiny_amount: !state.conv.cashin_tiny_amount ? 
i18n.str`required` :
+          !cashout_tiny_amount ? i18n.str`invalid` :
+            undefined,
+        cashout_fee: !state.conv.cashin_fee ? i18n.str`required` :
+          !cashout_fee ? i18n.str`invalid` :
+            undefined,
+
+        cashin_rounding_mode: !state.conv.cashin_rounding_mode ? 
i18n.str`required` : undefined,
+        cashout_rounding_mode: !state.conv.cashout_rounding_mode ? 
i18n.str`required` : undefined,
+
+        cashin_ratio: !state.conv.cashin_ratio ? i18n.str`required` : 
Number.isNaN(cashin_ratio) ? i18n.str`invalid` : undefined,
+        cashout_ratio: !state.conv.cashout_ratio ? i18n.str`required` : 
Number.isNaN(cashout_ratio) ? i18n.str`invalid` : undefined,
+      }),
+
+      amount: !state.amount ? i18n.str`required` :
+        !am ? i18n.str`invalid` :
+          undefined,
+    })
+
+    const result: RecursivePartial<FormType> = {
+      amount: am,
+      conv: {
+        cashin_fee: !errors?.conv?.cashin_fee ? Amounts.stringify(cashin_fee!) 
: undefined,
+        cashin_min_amount: !errors?.conv?.cashin_min_amount ? 
Amounts.stringify(cashin_min_amount!) : undefined,
+        cashin_ratio: !errors?.conv?.cashin_ratio ? String(cashin_ratio!) : 
undefined,
+        cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode ? 
(state.conv.cashin_rounding_mode!) : undefined,
+        cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount ? 
Amounts.stringify(cashin_tiny_amount!) : undefined,
+        cashout_fee: !errors?.conv?.cashout_fee ? 
Amounts.stringify(cashout_fee!) : undefined,
+        cashout_min_amount: !errors?.conv?.cashout_min_amount ? 
Amounts.stringify(cashout_min_amount!) : undefined,
+        cashout_ratio: !errors?.conv?.cashout_ratio ? String(cashout_ratio!) : 
undefined,
+        cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode ? 
(state.conv.cashout_rounding_mode!) : undefined,
+        cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount ? 
Amounts.stringify(cashout_tiny_amount!) : undefined,
+      }
+
+    }
+    return errors === undefined ?
+      { status: "ok", result: result as FormType, errors } :
+      { status: "fail", result, errors }
+  }
 }
 
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 00b6767ac..177bf3c20 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -238,12 +238,69 @@ export function PaytoWireTransferForm({
        */}
       <div class="">
         <h2 class="text-base font-semibold leading-7 
text-gray-900">{title}</h2>
-        <div>
-          <div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
+        <div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
+          <label
+            class={
+              "relative flex cursor-pointer rounded-lg border bg-white p-4 
shadow-sm focus:outline-none" +
+              (!isRawPayto
+                ? "border-indigo-600 ring-2 ring-indigo-600"
+                : "border-gray-300")
+            }
+          >
+            <input
+              type="radio"
+              name="project-type"
+              value="Newsletter"
+              class="sr-only"
+              aria-labelledby="project-type-0-label"
+              aria-describedby="project-type-0-description-0 
project-type-0-description-1"
+              onChange={() => {
+                if (parsed && parsed.isKnown) {
+                  switch (parsed.targetType) {
+                    case "iban": {
+                      setAccount(parsed.iban);
+                      break;
+                    }
+                    case "x-taler-bank": {
+                      setAccount(parsed.account);
+                      break;
+                    }
+                    case "bitcoin": {
+                      break;
+                    }
+                    default: {
+                      assertUnreachable(parsed)
+                    }
+                  }
+                  const amountStr = parsed.params["amount"] ?? 
`${config.currency}:0`;
+                  if (amountStr) {
+                    const amount = Amounts.parse(parsed.params["amount"]);
+                    if (amount) {
+                      setAmount(Amounts.stringifyValue(amount));
+                    }
+                  }
+                  const subject = parsed.params["message"];
+                  if (subject) {
+                    setSubject(subject);
+                  }
+                }
+                setIsRawPayto(false);
+              }}
+            />
+            <span class="flex flex-1">
+              <span class="flex flex-col">
+                <span class="block text-sm  font-medium text-gray-900">
+                  <i18n.Translate>Using a form</i18n.Translate>
+                </span>
+              </span>
+            </span>
+          </label>
+
+          {sendingToFixedAccount ? undefined : (
             <label
               class={
                 "relative flex cursor-pointer rounded-lg border bg-white p-4 
shadow-sm focus:outline-none" +
-                (!isRawPayto
+                (isRawPayto
                   ? "border-indigo-600 ring-2 ring-indigo-600"
                   : "border-gray-300")
               }
@@ -251,111 +308,52 @@ export function PaytoWireTransferForm({
               <input
                 type="radio"
                 name="project-type"
-                value="Newsletter"
+                value="Existing Customers"
                 class="sr-only"
-                aria-labelledby="project-type-0-label"
-                aria-describedby="project-type-0-description-0 
project-type-0-description-1"
+                aria-labelledby="project-type-1-label"
+                aria-describedby="project-type-1-description-0 
project-type-1-description-1"
                 onChange={() => {
-                  if (parsed && parsed.isKnown) {
-                    switch (parsed.targetType) {
-                      case "iban": {
-                        setAccount(parsed.iban);
-                        break;
-                      }
+                  if (account) {
+                    let payto;
+                    switch (paytoType) {
                       case "x-taler-bank": {
-                        setAccount(parsed.account);
+                        payto = buildPayto("x-taler-bank", url.host, account);
+                        if (parsedAmount) {
+                          payto.params["amount"] =
+                            Amounts.stringify(parsedAmount);
+                        }
+                        if (subject) {
+                          payto.params["message"] = subject;
+                        }
                         break;
                       }
-                      case "bitcoin": {
+                      case "iban": {
+                        payto = buildPayto("iban", account, undefined);
+                        if (parsedAmount) {
+                          payto.params["amount"] =
+                            Amounts.stringify(parsedAmount);
+                        }
+                        if (subject) {
+                          payto.params["message"] = subject;
+                        }
                         break;
                       }
-                      default: {
-                        assertUnreachable(parsed)
-                      }
-                    }
-                    const amountStr = parsed.params["amount"] ?? 
`${config.currency}:0`;
-                    if (amountStr) {
-                      const amount = Amounts.parse(parsed.params["amount"]);
-                      if (amount) {
-                        setAmount(Amounts.stringifyValue(amount));
-                      }
-                    }
-                    const subject = parsed.params["message"];
-                    if (subject) {
-                      setSubject(subject);
+                      default: assertUnreachable(paytoType)
                     }
+                    rawPaytoInputSetter(stringifyPaytoUri(payto));
                   }
-                  setIsRawPayto(false);
+                  setIsRawPayto(true);
                 }}
               />
               <span class="flex flex-1">
                 <span class="flex flex-col">
-                  <span class="block text-sm  font-medium text-gray-900">
-                    <i18n.Translate>Using a form</i18n.Translate>
+                  <span class="block text-sm font-medium text-gray-900">
+                    <i18n.Translate>Import payto:// URI</i18n.Translate>
                   </span>
                 </span>
               </span>
             </label>
-
-            {sendingToFixedAccount ? undefined : (
-              <label
-                class={
-                  "relative flex cursor-pointer rounded-lg border bg-white p-4 
shadow-sm focus:outline-none" +
-                  (isRawPayto
-                    ? "border-indigo-600 ring-2 ring-indigo-600"
-                    : "border-gray-300")
-                }
-              >
-                <input
-                  type="radio"
-                  name="project-type"
-                  value="Existing Customers"
-                  class="sr-only"
-                  aria-labelledby="project-type-1-label"
-                  aria-describedby="project-type-1-description-0 
project-type-1-description-1"
-                  onChange={() => {
-                    if (account) {
-                      let payto;
-                      switch (paytoType) {
-                        case "x-taler-bank": {
-                          payto = buildPayto("x-taler-bank", url.host, 
account);
-                          if (parsedAmount) {
-                            payto.params["amount"] =
-                              Amounts.stringify(parsedAmount);
-                          }
-                          if (subject) {
-                            payto.params["message"] = subject;
-                          }
-                          break;
-                        }
-                        case "iban": {
-                          payto = buildPayto("iban", account, undefined);
-                          if (parsedAmount) {
-                            payto.params["amount"] =
-                              Amounts.stringify(parsedAmount);
-                          }
-                          if (subject) {
-                            payto.params["message"] = subject;
-                          }
-                          break;
-                        }
-                        default: assertUnreachable(paytoType)
-                      }
-                      rawPaytoInputSetter(stringifyPaytoUri(payto));
-                    }
-                    setIsRawPayto(true);
-                  }}
-                />
-                <span class="flex flex-1">
-                  <span class="flex flex-col">
-                    <span class="block text-sm font-medium text-gray-900">
-                      <i18n.Translate>Import payto:// URI</i18n.Translate>
-                    </span>
-                  </span>
-                </span>
-              </label>
-            )}
-          </div>
+          )}
         </div>
       </div>
 
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx 
b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index 1b51e3222..7adacb775 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -329,6 +329,7 @@ export function CreateCashout({
   const cashoutAccountName = !cashoutAccount
     ? undefined
     : cashoutAccount.targetPath;
+
   return (
     <div>
       <LocalNotificationBanner notification={notification} />
diff --git a/packages/web-util/src/components/utils.ts 
b/packages/web-util/src/components/utils.ts
index 34693f7d7..75c3fc0fe 100644
--- a/packages/web-util/src/components/utils.ts
+++ b/packages/web-util/src/components/utils.ts
@@ -12,6 +12,7 @@ export function compose<SType extends { status: string }, 
PType>(
   hook: (p: PType) => RecursiveState<SType>,
   viewMap: StateViewMap<SType>,
 ): (p: PType) => VNode {
+
   function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
     function ComposedComponent(): VNode {
       const state = stateHook();
@@ -35,6 +36,33 @@ export function compose<SType extends { status: string }, 
PType>(
   };
 }
 
+export function recursive<PType>(
+  hook: (p: PType) => RecursiveState<VNode>,
+): (p: PType) => VNode {
+
+  function withHook(stateHook: () => RecursiveState<VNode>): () => VNode {
+    function ComposedComponent(): VNode {
+      const state = stateHook();
+
+      if (typeof state === "function") {
+        const subComponent = withHook(state);
+        return createElement(subComponent, {});
+      }
+
+      return state;
+    }
+
+    return ComposedComponent;
+  }
+
+  return (p: PType) => {
+    const h = withHook(() => hook(p));
+    return h();
+  };
+}
+
+
+
 /**
  *
  * @param obj VNode

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