gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] 02/02: more cashout ui


From: gnunet
Subject: [taler-wallet-core] 02/02: more cashout ui
Date: Wed, 22 Nov 2023 19:20:44 +0100

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

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

commit 305c513c2bcc2b25fe57cf0ed9723781944f9f3f
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Wed Nov 22 15:20:06 2023 -0300

    more cashout ui
---
 .../demobank-ui/src/components/Cashouts/index.ts   |  13 +-
 .../demobank-ui/src/components/Cashouts/views.tsx  |  21 ++-
 .../src/components/Transactions/views.tsx          |   6 +-
 packages/demobank-ui/src/hooks/circuit.ts          |  22 +--
 packages/demobank-ui/src/pages/BankFrame.tsx       |  11 +-
 .../src/pages/PaytoWireTransferForm.tsx            |   8 +-
 .../src/pages/WithdrawalConfirmationQuestion.tsx   |   4 +-
 .../src/pages/account/CashoutListForAccount.tsx    |   4 +-
 .../src/pages/account/ShowAccountDetails.tsx       |   5 +-
 .../demobank-ui/src/pages/admin/AccountForm.tsx    |  14 +-
 .../demobank-ui/src/pages/admin/AccountList.tsx    |   2 +-
 packages/demobank-ui/src/pages/admin/AdminHome.tsx |   7 +-
 .../src/pages/business/CreateCashout.tsx           | 174 ++++++++++++++++++---
 .../src/pages/business/ShowCashoutDetails.tsx      |  12 +-
 packages/taler-util/src/http-client/bank-core.ts   |   8 +-
 packages/taler-util/src/http-client/types.ts       |  14 +-
 .../src/cta/Withdraw/test.ts                       |   2 +
 17 files changed, 245 insertions(+), 82 deletions(-)

diff --git a/packages/demobank-ui/src/components/Cashouts/index.ts 
b/packages/demobank-ui/src/components/Cashouts/index.ts
index 6cbb1247d..ca58de98f 100644
--- a/packages/demobank-ui/src/components/Cashouts/index.ts
+++ b/packages/demobank-ui/src/components/Cashouts/index.ts
@@ -14,17 +14,18 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { HttpError, utils } from "@gnu-taler/web-util/browser";
+import { ErrorLoading, HttpError, utils } from "@gnu-taler/web-util/browser";
 import { Loading } from "@gnu-taler/web-util/browser";
 // import { compose, StateViewMap } from "../../utils/index.js";
 // import { wxApi } from "../../wxApi.js";
 import { AbsoluteTime, AmountJson, TalerCoreBankErrorsByMethod, 
TalerCorebankApi, TalerError } from "@gnu-taler/taler-util";
 import { useComponentState } from "./state.js";
 import { FailedView, LoadingUriView, ReadyView } from "./views.js";
+import { h } from "preact";
 
 export interface Props {
   account: string;
-  onSelected: (id: string) => void;
+  onSelected: (id: number) => void;
 }
 
 export type State = State.Loading | State.Failed | State.LoadingUriError | 
State.Ready;
@@ -51,8 +52,8 @@ export namespace State {
   export interface Ready extends BaseInfo {
     status: "ready";
     error: undefined;
-    cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: string })[];
-    onSelected: (id: string) => void;
+    cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: number })[];
+    onSelected: (id: number) => void;
   }
 }
 
@@ -66,7 +67,9 @@ export interface Transaction {
 
 const viewMapping: utils.StateViewMap<State> = {
   loading: Loading,
-  "loading-error": LoadingUriView,
+  "loading-error": ({error}) => {
+    return h(ErrorLoading, {error, showDetail:true});
+  },
   "failed": FailedView,
   ready: ReadyView,
 };
diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx 
b/packages/demobank-ui/src/components/Cashouts/views.tsx
index 76a3a90df..651a7a034 100644
--- a/packages/demobank-ui/src/components/Cashouts/views.tsx
+++ b/packages/demobank-ui/src/components/Cashouts/views.tsx
@@ -14,13 +14,13 @@
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
 
-import { Fragment, h, VNode } from "preact";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { State } from "./index.js";
+import { Amounts, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
+import { Attention, ErrorLoading, Loading, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { format } from "date-fns";
-import { Amounts, assertUnreachable } from "@gnu-taler/taler-util";
+import { Fragment, h, VNode } from "preact";
 import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
-import { Attention } from "@gnu-taler/web-util/browser";
+import { State } from "./index.js";
+import { useConversionInfo } from "../../hooks/circuit.js";
 
 export function LoadingUriView({ error }: State.LoadingUriError): VNode {
   const { i18n } = useTranslationContext();
@@ -52,6 +52,13 @@ export function FailedView({ error }: State.Failed) {
 
 export function ReadyView({ cashouts, onSelected }: State.Ready): VNode {
   const { i18n } = useTranslationContext();
+  const resp = useConversionInfo();
+  if (!resp) {
+    return <Loading />
+  }
+  if (resp instanceof TalerError) {
+    return <ErrorLoading error={resp} />
+  }
   if (!cashouts.length) return <div />
   const txByDate = cashouts.reduce((prev, cur) => {
     const d = cur.creation_time.t_s === "never"
@@ -122,8 +129,8 @@ export function ReadyView({ cashouts, onSelected }: 
State.Ready): VNode {
                       </dl> */}
                     </td>
                     <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-gray-500">{confirmationTime}</td>
-                    <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-red-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)} 
/></td>
-                    <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-green-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} 
/></td>
+                    <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-red-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_debit)}  
spec={resp.body.regional_currency_specification} /></td>
+                    <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-green-600"><RenderAmount value={Amounts.parseOrThrow(item.amount_credit)} 
spec={resp.body.fiat_currency_specification} /></td>
 
                     <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-gray-500">{item.status}</td>
                     <td class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-gray-500 break-all min-w-md">
diff --git a/packages/demobank-ui/src/components/Transactions/views.tsx 
b/packages/demobank-ui/src/components/Transactions/views.tsx
index 72dd43415..1613cb06a 100644
--- a/packages/demobank-ui/src/components/Transactions/views.tsx
+++ b/packages/demobank-ui/src/components/Transactions/views.tsx
@@ -19,6 +19,7 @@ import { format } from "date-fns";
 import { Fragment, h, VNode } from "preact";
 import { doAutoFocus, RenderAmount } from 
"../../pages/PaytoWireTransferForm.js";
 import { State } from "./index.js";
+import { useBankCoreApiContext } from "../../context/config.js";
 
 export function LoadingUriView({ error }: State.LoadingUriError): VNode {
   const { i18n } = useTranslationContext();
@@ -32,6 +33,7 @@ export function LoadingUriView({ error }: 
State.LoadingUriError): VNode {
 
 export function ReadyView({ transactions, onNext, onPrev }: State.Ready): 
VNode {
   const { i18n } = useTranslationContext();
+  const {config} = useBankCoreApiContext();
   if (!transactions.length) return <div />
   const txByDate = transactions.reduce((prev, cur) => {
     const d = cur.when.t_ms === "never"
@@ -78,7 +80,7 @@ export function ReadyView({ transactions, onNext, onPrev }: 
State.Ready): VNode
                         <dd class="mt-1 truncate text-gray-700">
                           {item.negative ? i18n.str`sent` : 
i18n.str`received`} {item.amount ? (
                             <span data-negative={item.negative ? "true" : 
"false"} class="data-[negative=false]:text-green-600 
data-[negative=true]:text-red-600">
-                              <RenderAmount value={item.amount} />
+                              <RenderAmount value={item.amount} 
spec={config.currency_specification}/>
                             </span>
                           ) : (
                             <span style={{ color: "grey" 
}}>&lt;{i18n.str`invalid value`}&gt;</span>
@@ -99,7 +101,7 @@ export function ReadyView({ transactions, onNext, onPrev }: 
State.Ready): VNode
                     </td>
                     <td data-negative={item.negative ? "true" : "false"}
                       class="hidden sm:table-cell px-3 py-3.5 text-sm 
text-gray-500 ">
-                      {item.amount ? (<RenderAmount value={item.amount} 
negative={item.negative} withColor />
+                      {item.amount ? (<RenderAmount value={item.amount} 
negative={item.negative} withColor spec={config.currency_specification}/>
                       ) : (
                         <span style={{ color: "grey" }}>&lt;{i18n.str`invalid 
value`}&gt;</span>
                       )}
diff --git a/packages/demobank-ui/src/hooks/circuit.ts 
b/packages/demobank-ui/src/hooks/circuit.ts
index c0164d60a..01c62c409 100644
--- a/packages/demobank-ui/src/hooks/circuit.ts
+++ b/packages/demobank-ui/src/hooks/circuit.ts
@@ -83,7 +83,7 @@ export function useEstimator(): CashoutEstimators {
       }
       const credit = Amounts.parseOrThrow(resp.body.amount_credit);
       const debit = Amounts.parseOrThrow(resp.body.amount_debit);
-      const beforeFee = Amounts.add(credit, fee).amount;
+      const beforeFee = Amounts.sub(credit, fee).amount;
 
       return {
         debit,
@@ -92,8 +92,8 @@ export function useEstimator(): CashoutEstimators {
       };
     },
     estimateByDebit: async (regionalAmount, fee) => {
-      const resp = await api.getConversionInfoAPI().getCashoutRate({ 
-        debit: regionalAmount 
+      const resp = await api.getConversionInfoAPI().getCashoutRate({
+        debit: regionalAmount
       });
       if (resp.type === "fail") {
         // can't happen
@@ -170,7 +170,7 @@ export function useBusinessAccounts() {
   return undefined;
 }
 
-type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: string }
+type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number }
 function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
   return c !== undefined
 }
@@ -182,11 +182,15 @@ export function useCashouts(account: string) {
   async function fetcher([username, token]: [string, AccessToken]) {
     const list = await api.getAccountCashouts({ username, token })
     if (list.type !== "ok") {
+      console.error(list)
       return list;
     }
     const all: Array<CashoutWithId | undefined> = await 
Promise.all(list.body.cashouts.map(c => {
       return api.getCashoutById({ username, token }, c.cashout_id).then(r => {
-        if (r.type === "fail") return undefined
+        if (r.type === "fail") {
+          console.error("failed", r)
+          return undefined
+        }
         return { ...r.body, id: c.cashout_id }
       })
     }))
@@ -196,7 +200,7 @@ export function useCashouts(account: string) {
   }
 
   const { data, error } = useSWR<OperationOk<{ cashouts: CashoutWithId[] }> | 
TalerCoreBankErrorsByMethod<"getAccountCashouts">, TalerHttpError>(
-    !config.allow_conversion ? false : [account, token, "getAccountCashouts"], 
fetcher, {
+    !config.allow_conversion ? undefined : [account, token, 
"getAccountCashouts"], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -213,17 +217,17 @@ export function useCashouts(account: string) {
   return undefined;
 }
 
-export function useCashoutDetails(cashoutId: string) {
+export function useCashoutDetails(cashoutId: number | undefined) {
   const { state: credentials } = useBackendState();
   const creds = credentials.status !== "loggedIn" ? undefined : credentials
   const { api } = useBankCoreApiContext();
 
-  async function fetcher([username, token, id]: [string, AccessToken, string]) 
{
+  async function fetcher([username, token, id]: [string, AccessToken, number]) 
{
     return api.getCashoutById({ username, token }, id)
   }
 
   const { data, error } = 
useSWR<TalerCoreBankResultByMethod<"getCashoutById">, TalerHttpError>(
-    [creds?.username, creds?.token, cashoutId, "getCashoutById"], fetcher, {
+    cashoutId === undefined ? undefined : [creds?.username, creds?.token, 
cashoutId, "getCashoutById"], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx 
b/packages/demobank-ui/src/pages/BankFrame.tsx
index 5fef04b66..34c39e9d3 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -23,6 +23,7 @@ import { useBackendState } from "../hooks/backend.js";
 import { getAllBooleanPreferences, getLabelForPreferences, usePreferences } 
from "../hooks/preferences.js";
 import { RenderAmount } from "./PaytoWireTransferForm.js";
 import { useSettingsContext } from "../context/settings.js";
+import { useBankCoreApiContext } from "../context/config.js";
 
 const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : 
undefined;
 const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -45,8 +46,8 @@ export function BankFrame({
   useEffect(() => {
     if (error) {
       const desc = (error instanceof Error ? error.stack : String(error)) as 
TranslatedString
+      console.log(error)
       if (error instanceof Error) {
-        console.log(error)
         notifyException(i18n.str`Internal error, please report.`, error)
       } else {
         notifyError(i18n.str`Internal error, please report.`, String(error) as 
TranslatedString)
@@ -118,9 +119,9 @@ export function BankFrame({
 
     <Footer
       testingUrl={
-        (typeof localStorage !== "undefined") && 
localStorage.getItem("bank-base-url") ? 
-        localStorage.getItem("bank-base-url") ?? undefined : 
-        undefined}
+        (typeof localStorage !== "undefined") && 
localStorage.getItem("bank-base-url") ?
+          localStorage.getItem("bank-base-url") ?? undefined :
+          undefined}
       GIT_HASH={GIT_HASH}
       VERSION={VERSION}
     />
@@ -172,6 +173,7 @@ function WelcomeAccount({ account: accountName }: { 
account: string }): VNode {
 
 function AccountBalance({ account }: { account: string }): VNode {
   const result = useAccountDetails(account);
+  const { config } = useBankCoreApiContext();
   if (!result) {
     return <Loading />
   }
@@ -183,5 +185,6 @@ function AccountBalance({ account }: { account: string }): 
VNode {
   return <RenderAmount
     value={Amounts.parseOrThrow(result.body.balance.amount)}
     negative={result.body.balance.credit_debit_indicator === "debit"}
+    spec={config.currency_specification}
   />
 }
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx 
b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index a6282c947..e035c7fed 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -18,6 +18,7 @@ import {
   AmountJson,
   AmountString,
   Amounts,
+  CurrencySpecification,
   Logger,
   PaytoString,
   TranslatedString,
@@ -470,13 +471,12 @@ export function InputAmount(
   );
 }
 
-export function RenderAmount({ value, negative, withColor }: { value: 
AmountJson, negative?: boolean, withColor?: boolean }): VNode {
-  const { config } = useBankCoreApiContext()
+export function RenderAmount({ value, spec, negative, withColor }: { spec: 
CurrencySpecification; value: AmountJson, negative?: boolean, withColor?: 
boolean }): VNode {
   const neg = !!negative //convert to true or false
   const str = Amounts.stringifyValue(value)
   const sep_pos = str.indexOf(FRAC_SEPARATOR)
-  if (sep_pos !== -1 && str.length - sep_pos - 1 > 
config.currency_specification.num_fractional_normal_digits) {
-    const limit = sep_pos + 
config.currency_specification.num_fractional_normal_digits + 1
+  if (sep_pos !== -1 && str.length - sep_pos - 1 > 
spec.num_fractional_normal_digits) {
+    const limit = sep_pos + spec.num_fractional_normal_digits + 1
     const normal = str.substring(0, limit)
     const small = str.substring(limit)
     return <span data-negative={withColor ? neg : undefined} 
class="whitespace-nowrap data-[negative=false]:text-green-600 
data-[negative=true]:text-red-600">
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx 
b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index be8ff8b58..bfb118c6c 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -70,7 +70,7 @@ export function WithdrawalConfirmationQuestion({
   }, []);
   const [notification, notify, handleError] = useLocalNotification()
 
-  const { api } = useBankCoreApiContext()
+  const { config, api } = useBankCoreApiContext()
   const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
   const answer = parseInt(captchaAnswer ?? "", 10);
   const [busy, setBusy] = useState<Record<string, undefined>>()
@@ -289,7 +289,7 @@ export function WithdrawalConfirmationQuestion({
                   <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 
sm:px-0">
                     <dt class="text-sm font-medium leading-6 
text-gray-900">Amount</dt>
                     <dd class="mt-1 text-sm leading-6 text-gray-700 
sm:col-span-2 sm:mt-0">
-                      <RenderAmount value={details.amount} />
+                      <RenderAmount value={details.amount} 
spec={config.currency_specification} />
                     </dd>
                   </div>
                 </dl>
diff --git a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx 
b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
index 293b821e2..f2972ed65 100644
--- a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
+++ b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -9,7 +9,7 @@ import { CreateCashout } from "../business/CreateCashout.js";
 interface Props {
   account: string,
   onClose: () => void,
-  onSelected: (cid: string) => void
+  onSelected: (cid: number) => void
 }
 
 export function CashoutListForAccount({ account, onSelected, onClose }: 
Props): VNode {
@@ -29,7 +29,7 @@ export function CashoutListForAccount({ account, onSelected, 
onClose }: Props):
       </h1>
     }
 
-    <CreateCashout onCancel={() => { }} onComplete={() => { }} 
account={account} />
+    <CreateCashout focus onCancel={onClose} onComplete={() => { }} 
account={account} />
 
     <Cashouts
       account={account}
diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx 
b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
index 7a4fbddf5..ab5ceb8d5 100644
--- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -52,7 +52,10 @@ export function ShowAccountDetails({
   async function doUpdate() {
     if (!update || !submitAccount || !creds) return;
     await handleError(async () => {
-      const resp = await api.updateAccount(creds, {
+      const resp = await api.updateAccount({
+        token: creds.token,
+        username: account,
+      }, {
         cashout_address: submitAccount.cashout_payto_uri,
         challenge_contact_data: undefinedIfEmpty({
           email: submitAccount.contact_data?.email,
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx 
b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 5d8e3797a..b38d40012 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -61,12 +61,12 @@ export function AccountForm({
               : validateIBAN(parsed.iban, i18n)) as PaytoString,
       contact_data: undefinedIfEmpty({
         email: !newForm.contact_data?.email
-          ? i18n.str`required`
+          ? undefined
           : !EMAIL_REGEX.test(newForm.contact_data.email)
             ? i18n.str`it should be an email`
             : undefined,
         phone: !newForm.contact_data?.phone
-          ? i18n.str`required`
+          ? undefined
           : !newForm.contact_data.phone.startsWith("+")
             ? i18n.str`should start with +`
             : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
@@ -152,7 +152,7 @@ export function AccountForm({
                 name="name"
                 data-error={!!errors?.name && form.name !== undefined}
                 id="name"
-                disabled={purpose !== "create"}
+                disabled={purpose === "show"}
                 value={form.name ?? ""}
                 onChange={(e) => {
                   form.name = e.currentTarget.value;
@@ -189,7 +189,7 @@ export function AccountForm({
                 name="email"
                 id="email"
                 data-error={!!errors?.contact_data?.email && 
form.contact_data?.email !== undefined}
-                disabled={purpose !== "create"}
+                disabled={purpose === "show"}
                 value={form.contact_data?.email ?? ""}
                 onChange={(e) => {
                   if (form.contact_data) {
@@ -273,7 +273,11 @@ export function AccountForm({
               <i18n.Translate>account number where the money is going to be 
sent when doing cashouts</i18n.Translate>
             </p>
           </div>
-
+          <div class="sm:col-span-5">
+            <pre>
+              {JSON.stringify(errors, undefined, 2)}
+            </pre>
+          </div>
         </div>
       </div>
       {children}
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx 
b/packages/demobank-ui/src/pages/admin/AccountList.tsx
index 8c018120d..7d6cfaf7d 100644
--- a/packages/demobank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -105,7 +105,7 @@ export function AccountList({ onRemoveAccount, 
onShowAccountDetails, onUpdateAcc
                           i18n.str`unknown`
                         ) : (
                           <span class="amount">
-                            <RenderAmount value={balance} 
negative={balanceIsDebit} />
+                            <RenderAmount value={balance} 
negative={balanceIsDebit} spec={config.currency_specification} />
                           </span>
                         )}
                       </td>
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx 
b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
index 795a2c6d0..e9fa1dc47 100644
--- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -7,6 +7,7 @@ import { useLastMonitorInfo } from "../../hooks/circuit.js";
 import { RenderAmount } from "../PaytoWireTransferForm.js";
 import { WireTransfer } from "../WireTransfer.js";
 import { AccountList } from "./AccountList.js";
+import { useBankCoreApiContext } from "../../context/config.js";
 
 /**
  * Query account information and show QR code if there is pending withdrawal
@@ -42,7 +43,6 @@ function Metrics(): VNode {
   const [metricType, setMetricType] = 
useState<TalerCorebankApi.MonitorTimeframeParam>(TalerCorebankApi.MonitorTimeframeParam.day);
 
   const resp = useLastMonitorInfo(new Date(), metricType);
-  console.log(resp)
   if (!resp) return <Fragment />;
   if (resp instanceof TalerError) {
     return <ErrorLoading error={resp} />
@@ -162,6 +162,7 @@ function Metrics(): VNode {
 
 function MetricValue({ current, previous }: { current: AmountString | 
undefined, previous: AmountString | undefined }): VNode {
   const { i18n } = useTranslationContext()
+  const {config} = useBankCoreApiContext();
   const cmp = current && previous ? Amounts.cmp(current, previous) : 0;
   const currAmount = !current ? undefined : 
Number.parseFloat(Amounts.stringifyValue(current))
   const prevAmount = !previous ? undefined : 
Number.parseFloat(Amounts.stringifyValue(previous))
@@ -173,11 +174,11 @@ function MetricValue({ current, previous }: { current: 
AmountString | undefined,
   const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%`
   return <dd class="mt-1 flex justify-between md:block lg:flex">
     <div class="flex justify-start  items-baseline text-2xl font-semibold 
text-indigo-600">
-      {!current ? "-" : <RenderAmount value={Amounts.parseOrThrow(current)} />}
+      {!current ? "-" : <RenderAmount value={Amounts.parseOrThrow(current)} 
spec={config.currency_specification} />}
     </div>
     <div class="flex justify-end items-baseline text-2xl font-semibold 
text-indigo-600">
       <small class="ml-2 text-sm font-medium text-gray-500">
-        <i18n.Translate>from</i18n.Translate> {!previous ? "-" : <RenderAmount 
value={Amounts.parseOrThrow(previous)} />}
+        <i18n.Translate>from</i18n.Translate> {!previous ? "-" : <RenderAmount 
value={Amounts.parseOrThrow(previous)}  spec={config.currency_specification}/>}
       </small>
     </div>
 
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx 
b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index c5f4ebc4e..2f77f3960 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -26,6 +26,7 @@ import {
   Loading,
   LocalNotificationBanner,
   ShowInputErrorLabel,
+  notifyInfo,
   useLocalNotification,
   useTranslationContext
 } from "@gnu-taler/web-util/browser";
@@ -46,12 +47,13 @@ import {
 import { LoginForm } from "../LoginForm.js";
 import { InputAmount, RenderAmount, doAutoFocus } from 
"../PaytoWireTransferForm.js";
 import { assertUnreachable } from "../WithdrawalOperationPage.js";
+import { getRandomPassword, getRandomUsername } from "../rnd.js";
 
 interface Props {
   account: string;
   focus?: boolean,
   onComplete: (id: string) => void;
-  onCancel: () => void;
+  onCancel?: () => void;
 }
 
 type FormType = {
@@ -77,7 +79,10 @@ export function CreateCashout({
     estimateByCredit: calculateFromCredit,
     estimateByDebit: calculateFromDebit,
   } = useEstimator();
-  const { config } = useBankCoreApiContext()
+  const { state: credentials } = useBackendState();
+  const creds = credentials.status !== "loggedIn" ? undefined : credentials
+
+  const { api, config } = useBankCoreApiContext()
   const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, amount: 
"2" });
   const [notification, notify, handleError] = useLocalNotification()
   const info = useConversionInfo();
@@ -119,7 +124,7 @@ export function CreateCashout({
     debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold)
   }
 
-  const {fiat_currency, regional_currency} = info.body
+  const { fiat_currency, regional_currency, fiat_currency_specification, 
regional_currency_specification } = info.body
   const regionalZero = Amounts.zeroOfCurrency(regional_currency);
   const fiatZero = Amounts.zeroOfCurrency(fiat_currency);
   const limit = account.balanceIsDebit
@@ -128,7 +133,6 @@ export function CreateCashout({
 
   const zeroCalc = { debit: regionalZero, credit: fiatZero, beforeFee: 
fiatZero };
   const [calc, setCalc] = useState(zeroCalc);
-  console.log(calc)
   const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee);
   const sellRate = conversionInfo.cashout_ratio
   /**
@@ -159,6 +163,7 @@ export function CreateCashout({
     setForm(newForm);
   }
   const errors = undefinedIfEmpty<ErrorFrom<typeof form>>({
+    subject: !form.subject ? i18n.str`required` : undefined,
     amount: !form.amount
       ? i18n.str`required`
       : !inputAmount
@@ -174,6 +179,70 @@ export function CreateCashout({
   });
   const trimmedAmountStr = form.amount?.trim();
 
+  async function createCashout() {
+    const request_uid = encodeCrock(getRandomBytes(32))
+    await handleError(async () => {
+      if (!creds || !form.subject || !form.channel) return;
+
+      const resp = await api.createCashout(creds, {
+        request_uid,
+        amount_credit: Amounts.stringify(calc.credit),
+        amount_debit: Amounts.stringify(calc.debit),
+        subject: form.subject,
+        tan_channel: form.channel,
+      })
+      if (resp.type === "ok") {
+        notifyInfo(i18n.str`Cashout created`)
+      } else {
+        switch (resp.case) {
+          case "account-not-found": return notify({
+            type: "error",
+            title: i18n.str`Account not found`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          });
+          case "request-already-used": return notify({
+            type: "error",
+            title: i18n.str`Duplicated request detected, check if the 
operation succeded or try again.`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          });
+          case "incorrect-exchange-rate": return notify({
+            type: "error",
+            title: i18n.str`The exchange rate was incorrectly applied`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          });
+          case "no-contact-info": return notify({
+            type: "error",
+            title: i18n.str`Missing contact info before to create the cashout`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          });
+          case "no-enough-balance": return notify({
+            type: "error",
+            title: i18n.str`The account does not have sufficient funds`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          });
+          case "cashout-not-supported": return notify({
+            type: "error",
+            title: i18n.str`Cashouts are not supported`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          });
+          case "tan-failed": return notify({
+            type: "error",
+            title: i18n.str`Sending the confirmation code failed.`,
+            description: resp.detail.hint as TranslatedString,
+            debug: resp.detail,
+          });
+        }
+        assertUnreachable(resp)
+      }
+    })
+  }
+
   return (
     <div>
       <LocalNotificationBanner notification={notification} />
@@ -192,18 +261,18 @@ export function CreateCashout({
 
             <div class="flex items-center justify-between border-t-2 afu pt-4">
               <dt class="flex items-center text-sm text-gray-600">
-                <span><i18n.Translate>Current balance</i18n.Translate></span>
+                <span><i18n.Translate>Balance</i18n.Translate></span>
               </dt>
               <dd class="text-sm text-gray-900">
-                <RenderAmount value={account.balance} />
+                <RenderAmount value={account.balance} 
spec={regional_currency_specification} />
               </dd>
             </div>
             <div class="flex items-center justify-between border-t-2 afu pt-4">
               <dt class="flex items-center text-sm text-gray-600">
-                <span><i18n.Translate>Cashout fee</i18n.Translate></span>
+                <span><i18n.Translate>Fee</i18n.Translate></span>
               </dt>
               <dd class="text-sm text-gray-900">
-                <RenderAmount value={sellFee} />
+                <RenderAmount value={sellFee} 
spec={fiat_currency_specification} />
               </dd>
             </div>
           </dl>
@@ -226,7 +295,7 @@ export function CreateCashout({
                   class="block text-sm font-medium leading-6 text-gray-900"
                   for="subject"
                 >
-                  {i18n.str`Subject`}
+                  {i18n.str`Transfer subject`}
                 </label>
                 <div class="mt-2">
                   <input
@@ -253,14 +322,24 @@ export function CreateCashout({
 
               {/* amount */}
               <div class="sm:col-span-5">
-                <label
-                  class="block text-sm font-medium leading-6 text-gray-900"
-                  for="amount"
-                >
-                  {form.isDebit
-                    ? i18n.str`Amount to send`
-                    : i18n.str`Amount to receive`}
-                </label>
+                <div class="flex justify-between">
+                  <label
+                    class="block text-sm font-medium leading-6 text-gray-900"
+                    for="amount"
+                  >
+                    {form.isDebit
+                      ? i18n.str`Amount to send`
+                      : i18n.str`Amount to receive`}
+                  </label>
+                  <button type="button" data-enabled={form.isDebit} 
class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 
w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent 
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 
focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" 
aria-labelledby="availability-label" aria-describedby="availability-description"
+                    onClick={() => {
+                      form.isDebit = !form.isDebit
+                      updateForm(structuredClone(form))
+                    }}>
+                    <span aria-hidden="true" data-enabled={form.isDebit} 
class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none 
inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition 
duration-200 ease-in-out"></span>
+                  </button>
+
+                </div>
                 <div class="mt-2">
                   <InputAmount
                     name="amount"
@@ -287,7 +366,7 @@ export function CreateCashout({
                     <div class="justify-between items-center flex ">
                       <dt class="text-sm text-gray-600"><i18n.Translate>Total 
cost</i18n.Translate></dt>
                       <dd class="text-sm text-gray-900">
-                        <RenderAmount value={calc.debit} negative withColor />
+                        <RenderAmount value={calc.debit} negative withColor 
spec={regional_currency_specification} />
                       </dd>
                     </div>
 
@@ -302,7 +381,7 @@ export function CreateCashout({
                 </a> */}
                       </dt>
                       <dd class="text-sm text-gray-900">
-                        <RenderAmount value={balanceAfter} />
+                        <RenderAmount value={balanceAfter} 
spec={regional_currency_specification} />
                       </dd>
                     </div>
                     {Amounts.isZero(sellFee) || Amounts.isZero(calc.beforeFee) 
? undefined : (
@@ -316,14 +395,14 @@ export function CreateCashout({
                 </a> */}
                         </dt>
                         <dd class="text-sm text-gray-900">
-                          <RenderAmount value={calc.beforeFee} />
+                          <RenderAmount value={calc.beforeFee} 
spec={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>Total cashout transfer</i18n.Translate></dt>
                       <dd class="text-lg text-gray-900 font-medium">
-                        <RenderAmount value={calc.credit} withColor />
+                        <RenderAmount value={calc.credit} withColor 
spec={fiat_currency_specification} />
                       </dd>
                     </div>
                   </dl>
@@ -331,6 +410,55 @@ export function CreateCashout({
               )}
 
               {/* channel */}
+              <div class="sm:col-span-5">
+              <label
+                  class="block text-sm font-medium leading-6 text-gray-900"
+                  for="channel"
+                >
+                  {i18n.str`Confirmation the operation using`}
+                </label>
+
+                <div class="mt-2 max-w-xl text-sm text-gray-500">
+                  <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
+
+                    <label onClick={()=>{
+                      form.channel = TanChannel.EMAIL
+                      updateForm(structuredClone(form))
+                    }} data-selected={form.channel === TanChannel.EMAIL} 
class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm 
focus:outline-none border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600">
+                      <input type="radio" name="channel" value="Newsletter" 
class="sr-only" />
+                      <span class="flex flex-1">
+                        <span class="flex flex-col">
+                          <span id="project-type-0-label" class="block text-sm 
font-medium text-gray-900 ">
+                            <i18n.Translate>Email</i18n.Translate>
+                          </span>
+                        </span>
+                      </span>
+                      <svg data-selected={form.channel === TanChannel.EMAIL} 
class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 
20" fill="currentColor" aria-hidden="true">
+                        <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 
000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 
10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+                      </svg>
+                    </label>
+
+                    <label onClick={()=>{
+                      form.channel = TanChannel.SMS
+                      updateForm(structuredClone(form))
+                    }} data-selected={form.channel === TanChannel.SMS} 
class="relative flex cursor-pointer rounded-lg border bg-white  p-4 shadow-sm 
focus:outline-none border-gray-300 data-[selected=true]:ring-2 
data-[selected=true]:ring-indigo-600">
+                      <input type="radio" name="channel" value="Existing 
Customers" class="sr-only"  />
+                      <span class="flex flex-1">
+                        <span class="flex flex-col">
+                          <span id="project-type-1-label" class="block text-sm 
font-medium text-gray-900">
+                            <i18n.Translate>SMS</i18n.Translate>
+                          </span>
+                        </span>
+                      </span>
+                      <svg data-selected={form.channel === TanChannel.SMS} 
class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 
20" fill="currentColor" aria-hidden="true">
+                        <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 
000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 
10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+                      </svg>
+                    </label>
+
+                  </div>
+                </div>
+
+              </div>
             </div>
           </div>
 
@@ -348,10 +476,10 @@ export function CreateCashout({
               disabled={!!errors}
               onClick={(e) => {
                 e.preventDefault()
-                // doChangePassword()
+                createCashout()
               }}
             >
-              <i18n.Translate>Change</i18n.Translate>
+              <i18n.Translate>Cashout</i18n.Translate>
             </button>
           </div>
         </form>
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx 
b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
index ddfc18a0c..52ff713e2 100644
--- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -53,7 +53,9 @@ export function ShowCashoutDetails({
   const { state } = useBackendState();
   const creds = state.status !== "loggedIn" ? undefined : state
   const { api } = useBankCoreApiContext()
-  const result = useCashoutDetails(id);
+  const cid = Number.parseInt(id, 10)
+
+  const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid);
   const [code, setCode] = useState<string | undefined>(undefined);
   const [notification, notify, handleError] = useLocalNotification()
 
@@ -72,6 +74,10 @@ export function ShowCashoutDetails({
       default: assertUnreachable(result)
     }
   }
+  if (Number.isNaN(cid)) {
+    //TODO: better error message
+    return <div>cashout id should be a number</div>
+  }
   const errors = undefinedIfEmpty({
     code: !code ? i18n.str`required` : undefined,
   });
@@ -165,7 +171,7 @@ export function ShowCashoutDetails({
                 e.preventDefault();
                 if (!creds) return;
                 await handleError(async () => {
-                  const resp = await api.abortCashoutById(creds, id);
+                  const resp = await api.abortCashoutById(creds, cid);
                   if (resp.type === "ok") {
                     onCancel();
                   } else {
@@ -207,7 +213,7 @@ export function ShowCashoutDetails({
                 e.preventDefault();
                 if (!creds || !code) return;
                 await handleError(async () => {
-                  const resp = await api.confirmCashoutById(creds, id, {
+                  const resp = await api.confirmCashoutById(creds, cid, {
                     tan: code,
                   });
                   if (resp.type === "ok") {
diff --git a/packages/taler-util/src/http-client/bank-core.ts 
b/packages/taler-util/src/http-client/bank-core.ts
index d7bf6be29..273fb97c6 100644
--- a/packages/taler-util/src/http-client/bank-core.ts
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -442,7 +442,7 @@ export class TalerCoreBankHttpClient {
       body,
     });
     switch (resp.status) {
-      case HttpStatusCode.Accepted: return opSuccess(resp, 
codecForCashoutPending())
+      case HttpStatusCode.Ok: return opSuccess(resp, codecForCashoutPending())
       case HttpStatusCode.NotFound: return opKnownFailure("account-not-found", 
resp)
       case HttpStatusCode.Conflict: {
         const body = await resp.json()
@@ -465,7 +465,7 @@ export class TalerCoreBankHttpClient {
    * 
https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-abort
    * 
    */
-  async abortCashoutById(auth: UserAndToken, cid: string) {
+  async abortCashoutById(auth: UserAndToken, cid: number) {
     const url = new URL(`accounts/${auth.username}/cashouts/${cid}/abort`, 
this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "POST",
@@ -487,7 +487,7 @@ export class TalerCoreBankHttpClient {
    * 
https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-confirm
    * 
    */
-  async confirmCashoutById(auth: UserAndToken, cid: string, body: 
TalerCorebankApi.CashoutConfirmRequest) {
+  async confirmCashoutById(auth: UserAndToken, cid: number, body: 
TalerCorebankApi.CashoutConfirmRequest) {
     const url = new URL(`accounts/${auth.username}/cashouts/${cid}/confirm`, 
this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "POST",
@@ -522,7 +522,7 @@ export class TalerCoreBankHttpClient {
    * 
https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts-$CASHOUT_ID
    * 
    */
-  async getCashoutById(auth: UserAndToken, cid: string) {
+  async getCashoutById(auth: UserAndToken, cid: number) {
     const url = new URL(`accounts/${auth.username}/cashouts/${cid}`, 
this.baseUrl);
     const resp = await this.httpLib.fetch(url.href, {
       method: "GET",
diff --git a/packages/taler-util/src/http-client/types.ts 
b/packages/taler-util/src/http-client/types.ts
index 0ecc08b33..4c8a146a6 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -404,7 +404,7 @@ export const codecForBankAccountGetWithdrawalResponse =
 export const codecForCashoutPending =
   (): Codec<TalerCorebankApi.CashoutPending> =>
     buildCodecForObject<TalerCorebankApi.CashoutPending>()
-      .property("cashout_id", codecForString())
+      .property("cashout_id", codecForNumber())
       .build("TalerCorebankApi.CashoutPending");
 
 export const codecForCashoutConversionResponse =
@@ -428,7 +428,7 @@ export const codecForCashouts = (): 
Codec<TalerCorebankApi.Cashouts> =>
 
 export const codecForCashoutInfo = (): Codec<TalerCorebankApi.CashoutInfo> =>
   buildCodecForObject<TalerCorebankApi.CashoutInfo>()
-    .property("cashout_id", codecForString())
+    .property("cashout_id", codecForNumber())
     .property(
       "status",
       codecForEither(
@@ -448,7 +448,7 @@ export const codecForGlobalCashouts =
 export const codecForGlobalCashoutInfo =
   (): Codec<TalerCorebankApi.GlobalCashoutInfo> =>
     buildCodecForObject<TalerCorebankApi.GlobalCashoutInfo>()
-      .property("cashout_id", codecForString())
+      .property("cashout_id", codecForNumber())
       .property("username", codecForString())
       .property(
         "status",
@@ -465,7 +465,7 @@ export const codecForCashoutStatusResponse =
     buildCodecForObject<TalerCorebankApi.CashoutStatusResponse>()
       .property("amount_credit", codecForAmountString())
       .property("amount_debit", codecForAmountString())
-      .property("confirmation_time", codecForTimestamp)
+      .property("confirmation_time", codecOptional(codecForTimestamp))
       .property("creation_time", codecForTimestamp)
       // .property("credit_payto_uri", codecForPaytoString())
       .property(
@@ -1462,7 +1462,7 @@ export namespace TalerCorebankApi {
   export interface CashoutPending {
     // ID identifying the operation being created
     // and now waiting for the TAN confirmation.
-    cashout_id: string;
+    cashout_id: number;
   }
 
   export interface CashoutConfirmRequest {
@@ -1476,7 +1476,7 @@ export namespace TalerCorebankApi {
   }
 
   export interface CashoutInfo {
-    cashout_id: string;
+    cashout_id: number;
     status: "pending" | "aborted" | "confirmed";
   }
   export interface GlobalCashouts {
@@ -1484,7 +1484,7 @@ export namespace TalerCorebankApi {
     cashouts: GlobalCashoutInfo[];
   }
   export interface GlobalCashoutInfo {
-    cashout_id: string;
+    cashout_id: number;
     username: string;
     status: "pending" | "aborted" | "confirmed";
   }
diff --git a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts 
b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
index e5eaa3c14..108f5d005 100644
--- a/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
+++ b/packages/taler-wallet-webextension/src/cta/Withdraw/test.ts
@@ -159,6 +159,7 @@ describe("Withdraw CTA states", () => {
         amountEffective: "ARS:2" as AmountString,
         paytoUris: ["payto://"],
         tosAccepted: true,
+        withdrawalAccountList: [],
         ageRestrictionOptions: [],
         numCoins: 42,
       },
@@ -223,6 +224,7 @@ describe("Withdraw CTA states", () => {
         amountEffective: "ARS:2" as AmountString,
         paytoUris: ["payto://"],
         tosAccepted: false,
+        withdrawalAccountList: [],
         ageRestrictionOptions: [],
         numCoins: 42,
       },

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