From 4eda6ac07c78bcb3c2daa7846b4cd36048f9c7dd Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 6 Feb 2024 16:51:15 -0300 Subject: support for x-taler-bank and fix cache invalidation when new account is created --- packages/demobank-ui/src/Routing.tsx | 11 +- .../demobank-ui/src/components/Cashouts/views.tsx | 20 +- packages/demobank-ui/src/context/config.ts | 25 +- packages/demobank-ui/src/hooks/access.ts | 16 +- packages/demobank-ui/src/hooks/circuit.ts | 24 +- packages/demobank-ui/src/pages/PaymentOptions.tsx | 15 +- .../src/pages/PaytoWireTransferForm.tsx | 357 ++++++++++---- .../demobank-ui/src/pages/SolveChallengePage.tsx | 6 +- .../demobank-ui/src/pages/admin/AccountForm.tsx | 536 +++++++++++---------- .../demobank-ui/src/pages/admin/AccountList.tsx | 24 +- packages/demobank-ui/src/pages/admin/AdminHome.tsx | 6 +- .../src/pages/admin/CreateNewAccount.tsx | 2 - .../src/pages/business/CreateCashout.tsx | 23 +- .../src/pages/business/ShowCashoutDetails.tsx | 12 +- packages/demobank-ui/src/utils.ts | 99 ++-- 15 files changed, 708 insertions(+), 468 deletions(-) (limited to 'packages/demobank-ui/src') diff --git a/packages/demobank-ui/src/Routing.tsx b/packages/demobank-ui/src/Routing.tsx index 9f9475210..00811f2a7 100644 --- a/packages/demobank-ui/src/Routing.tsx +++ b/packages/demobank-ui/src/Routing.tsx @@ -57,7 +57,7 @@ export function Routing(): VNode { if (backend.state.status === "loggedIn") { const { isUserAdministrator, username } = backend.state; return ( - + ); @@ -147,7 +147,6 @@ function PublicRounting({

{i18n.str`Welcome to ${settings.bankName}!`}

- ); @@ -228,19 +227,19 @@ export const privatePages = { myAccountPassword: urlPattern(/\/my-password/, () => "#/my-password"), myAccountCashouts: urlPattern(/\/my-cashouts/, () => "#/my-cashouts"), accountDetails: urlPattern<{ account: string }>( - /\/profile\/(?[a-zA-Z0-9]+)\/details/, + /\/profile\/(?[a-zA-Z0-9_-]+)\/details/, ({ account }) => `#/profile/${account}/details`, ), accountChangePassword: urlPattern<{ account: string }>( - /\/profile\/(?[a-zA-Z0-9]+)\/change-password/, + /\/profile\/(?[a-zA-Z0-9_-]+)\/change-password/, ({ account }) => `#/profile/${account}/change-password`, ), accountDelete: urlPattern<{ account: string }>( - /\/profile\/(?[a-zA-Z0-9]+)\/delete/, + /\/profile\/(?[a-zA-Z0-9_-]+)\/delete/, ({ account }) => `#/profile/${account}/delete`, ), accountCashouts: urlPattern<{ account: string }>( - /\/profile\/(?[a-zA-Z0-9]+)\/cashouts/, + /\/profile\/(?[a-zA-Z0-9_-]+)\/cashouts/, ({ account }) => `#/profile/${account}/cashouts`, ), startOperation: urlPattern<{ wopid: string }>( diff --git a/packages/demobank-ui/src/components/Cashouts/views.tsx b/packages/demobank-ui/src/components/Cashouts/views.tsx index d036ec7d2..80eea6379 100644 --- a/packages/demobank-ui/src/components/Cashouts/views.tsx +++ b/packages/demobank-ui/src/components/Cashouts/views.tsx @@ -39,8 +39,10 @@ export function FailedView({ error }: State.Failed) { return ( + title={i18n.str`Cashout are disabled`} + > + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); } default: @@ -66,8 +68,10 @@ export function ReadyView({ return ( + title={i18n.str`Cashout are disabled`} + > + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); } default: @@ -82,8 +86,8 @@ export function ReadyView({ cur.creation_time.t_s === "never" ? "" : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy", { - locale: dateLocale, - }); + locale: dateLocale, + }); if (!prev[d]) { prev[d] = []; } @@ -141,8 +145,8 @@ export function ReadyView({ item.creation_time.t_s === "never" ? "" : format(item.creation_time.t_s * 1000, "HH:mm:ss", { - locale: dateLocale, - }); + locale: dateLocale, + }); return ( Array.isArray(key) && key[key.length - 1] === "getAccount", undefined, { revalidate: true }, @@ -62,9 +62,7 @@ export function useAccountDetails(account: string) { } export function revalidateWithdrawalDetails() { - mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById", - ); + return mutate((key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById", undefined, { revalidate: true }); } export function useWithdrawalDetails(wid: string) { @@ -111,8 +109,8 @@ export function useWithdrawalDetails(wid: string) { } export function revalidateTransactionDetails() { - mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "getTransactionById", + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getTransactionById", undefined, { revalidate: true } ); } export function useTransactionDetails(account: string, tid: number) { @@ -150,8 +148,8 @@ export function useTransactionDetails(account: string, tid: number) { } export function revalidatePublicAccounts() { - mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts", + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts", undefined, { revalidate: true } ); } export function usePublicAccounts( @@ -221,7 +219,7 @@ export function usePublicAccounts( } export function revalidateTransactions() { - mutate( + return mutate( (key) => Array.isArray(key) && key[key.length - 1] === "getTransactions", undefined, { revalidate: true }, diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 2b0781465..88ca7b947 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -52,7 +52,7 @@ type CashoutEstimators = { }; export function revalidateConversionInfo() { - mutate( + return mutate( (key) => Array.isArray(key) && key[key.length - 1] === "getConversionInfoAPI", ); @@ -130,7 +130,7 @@ export function useEstimator(): CashoutEstimators { } export function revalidateBusinessAccounts() { - mutate((key) => Array.isArray(key) && key[key.length - 1] === "getAccounts"); + return mutate((key) => Array.isArray(key) && key[key.length - 1] === "getAccounts", undefined, { revalidate: true }); } export function useBusinessAccounts() { const { state: credentials } = useBackendState(); @@ -199,9 +199,9 @@ function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId { return c !== undefined; } export function revalidateOnePendingCashouts() { - mutate( + return mutate( (key) => - Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts", + Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts", undefined, { revalidate: true } ); } export function useOnePendingCashouts(account: string) { @@ -215,13 +215,11 @@ export function useOnePendingCashouts(account: string) { if (list.type !== "ok") { return list; } - const pendingCashout = list.body.cashouts.find( - (c) => c.status === "pending", - ); + const pendingCashout = list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined; if (!pendingCashout) return opFixedSuccess(list.httpResp, undefined); const cashoutInfo = await api.getCashoutById( { username, token }, - pendingCashout?.cashout_id, + pendingCashout.cashout_id, ); if (cashoutInfo.type !== "ok") { return cashoutInfo; @@ -261,7 +259,7 @@ export function useOnePendingCashouts(account: string) { } export function revalidateCashouts() { - mutate((key) => Array.isArray(key) && key[key.length - 1] === "useCashouts"); + return mutate((key) => Array.isArray(key) && key[key.length - 1] === "useCashouts"); } export function useCashouts(account: string) { const { state: credentials } = useBackendState(); @@ -312,8 +310,8 @@ export function useCashouts(account: string) { } export function revalidateCashoutDetails() { - mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById", + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById", undefined, { revalidate: true } ); } export function useCashoutDetails(cashoutId: number | undefined) { @@ -361,8 +359,8 @@ export type LastMonitor = { previous: TalerCoreBankResultByMethod<"getMonitor">; }; export function revalidateLastMonitorInfo() { - mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo", + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo", undefined, { revalidate: true } ); } export function useLastMonitorInfo( diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx index 39b31a094..a508845e1 100644 --- a/packages/demobank-ui/src/pages/PaymentOptions.tsx +++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx @@ -33,18 +33,19 @@ function ShowOperationPendingTag({ }): VNode { const { i18n } = useTranslationContext(); const result = useWithdrawalDetails(woid); + const loading = !result const error = - !result || result instanceof TalerError || result.type === "fail"; - const completed = - !error && - (result.body.status === "aborted" || result.body.status === "confirmed"); + !loading && (result instanceof TalerError || result.type === "fail"); + const pending = + !loading && !error && + (result.body.status === "pending" || result.body.status === "selected"); useEffect(() => { - if (completed && onOperationAlreadyCompleted) { + if (!loading && !pending && onOperationAlreadyCompleted) { onOperationAlreadyCompleted(); } - }, [completed]); + }, [pending]); - if (error || completed) { + if (error || !pending) { return ; } diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx index 3643e1f6b..54ceb81a9 100644 --- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx +++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx @@ -23,28 +23,30 @@ import { FRAC_SEPARATOR, HttpStatusCode, PaytoString, + PaytoUri, TalerErrorCode, TranslatedString, assertUnreachable, buildPayto, parsePaytoUri, - stringifyPaytoUri, + stringifyPaytoUri } from "@gnu-taler/taler-util"; import { + InternationalizationAPI, LocalNotificationBanner, ShowInputErrorLabel, notifyInfo, useLocalNotification, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Ref, VNode, h } from "preact"; +import { ComponentChildren, Fragment, Ref, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { useBankState } from "../hooks/bank-state.js"; import { RouteDefinition } from "../route.js"; -import { undefinedIfEmpty, validateIBAN } from "../utils.js"; +import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js"; export function PaytoWireTransferForm({ focus, @@ -65,11 +67,11 @@ export function PaytoWireTransferForm({ }): VNode { const [isRawPayto, setIsRawPayto] = useState(false); const { state: credentials } = useBackendState(); - const { api } = useBankCoreApiContext(); + const { api, config, url } = useBankCoreApiContext(); const sendingToFixedAccount = toAccount !== undefined; - // FIXME: support other destination that just IBAN - const [iban, setIban] = useState(toAccount); + + const [account, setAccount] = useState(toAccount); const [subject, setSubject] = useState(); const [amount, setAmount] = useState(); const [, updateBankState] = useBankState(); @@ -78,49 +80,35 @@ export function PaytoWireTransferForm({ undefined, ); const { i18n } = useTranslationContext(); - const ibanRegex = "^[A-Z][A-Z][0-9]+$"; const trimmedAmountStr = amount?.trim(); const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`); - const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; const [notification, notify, handleError] = useLocalNotification(); + const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as const : "iban" as const; + const errorsWire = undefinedIfEmpty({ - iban: !iban + account: !account ? i18n.str`Required` - : !IBAN_REGEX.test(iban) - ? i18n.str`IBAN should have just uppercased letters and numbers` - : validateIBAN(iban, i18n), - subject: !subject ? i18n.str`Required` : undefined, + : paytoType === "iban" ? validateIBAN(account, i18n) : + paytoType === "x-taler-bank" ? validateTalerBank(account, i18n) : + undefined, + subject: !subject ? i18n.str`Required` : validateSubject(subject, i18n), amount: !trimmedAmountStr ? i18n.str`Required` : !parsedAmount ? i18n.str`Not valid` - : Amounts.isZero(parsedAmount) - ? i18n.str`Should be greater than 0` - : Amounts.cmp(limit, parsedAmount) === -1 - ? i18n.str`Balance is not enough` - : undefined, + : validateAmount(parsedAmount, limit, i18n), }); const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput); + const errorsPayto = undefinedIfEmpty({ rawPaytoInput: !rawPaytoInput ? i18n.str`Required` - : !parsed - ? i18n.str`Does not follow the pattern` - : !parsed.isKnown || parsed.targetType !== "iban" - ? i18n.str`Only "IBAN" target are supported` - : !parsed.params.amount - ? i18n.str`Use the "amount" parameter to specify the amount to be transferred` - : Amounts.parse(parsed.params.amount) === undefined - ? i18n.str`The amount is not valid` - : !parsed.params.message - ? i18n.str`Use the "message" parameter to specify a reference text for the transfer` - : !IBAN_REGEX.test(parsed.iban) - ? i18n.str`IBAN should have just uppercased letters and numbers` - : validateIBAN(parsed.iban, i18n), + : !parsed ? i18n.str`Does not follow the pattern` + : validateRawPayto(parsed, limit, url.host, i18n, paytoType), }); async function doSend() { @@ -128,18 +116,30 @@ export function PaytoWireTransferForm({ let sendingAmount: AmountString | undefined; if (credentials.status !== "loggedIn") return; - if (rawPaytoInput) { - const p = parsePaytoUri(rawPaytoInput); + if (isRawPayto) { + const p = parsePaytoUri(rawPaytoInput!); if (!p) return; sendingAmount = p.params.amount as AmountString; delete p.params.amount; // if this payto is valid then it already have message payto_uri = stringifyPaytoUri(p); } else { - if (!iban || !subject) return; - const ibanPayto = buildPayto("iban", iban, undefined); - ibanPayto.params.message = encodeURIComponent(subject); - payto_uri = stringifyPaytoUri(ibanPayto); + if (!account || !subject) return; + let payto; + switch (paytoType) { + case "x-taler-bank": { + payto = buildPayto("x-taler-bank", url.host, account); + break; + } + case "iban": { + payto = buildPayto("iban", account, undefined); + break; + } + default: assertUnreachable(paytoType) + } + + payto.params.message = encodeURIComponent(subject); + payto_uri = stringifyPaytoUri(payto); sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString; } const puri = payto_uri; @@ -212,7 +212,7 @@ export function PaytoWireTransferForm({ notifyInfo(i18n.str`Wire transfer created!`); onSuccess(); setAmount(undefined); - setIban(undefined); + setAccount(undefined); setSubject(undefined); rawPaytoInputSetter(undefined); }); @@ -243,13 +243,24 @@ export function PaytoWireTransferForm({ aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onChange={() => { - if ( - parsed && - parsed.isKnown && - parsed.targetType === "iban" - ) { - setIban(parsed.iban); - const amountStr = parsed.params["amount"]; + 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) { @@ -290,14 +301,32 @@ export function PaytoWireTransferForm({ aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => { - if (iban) { - const payto = buildPayto("iban", iban, undefined); - if (parsedAmount) { - payto.params["amount"] = - Amounts.stringify(parsedAmount); - } - if (subject) { - payto.params["message"] = subject; + 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)); } @@ -328,39 +357,37 @@ export function PaytoWireTransferForm({
{!isRawPayto ? (
-
- -
- { - setIban(e.currentTarget.value.toUpperCase()); - }} - /> - -
-

- - IBAN of the recipient's account - -

-
+ {(() => { + switch (paytoType) { + case "x-taler-bank": { + return + } + case "iban": { + return setAccount(v.toUpperCase())} + value={account} + focus={focus} + disabled={sendingToFixedAccount} + /> + } + default: assertUnreachable(paytoType) + } + })()}

- Account identification + Account id for authentication

@@ -366,22 +370,26 @@ export function AccountForm({

- { form.payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} + rightIcons={ form.payto_uri ?? defaultValue.payto_uri ?? ""} + />} + value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString} + disabled={!editableAccount} />
@@ -411,6 +419,9 @@ export function AccountForm({ isDirty={form.email !== undefined} />
+

+ To be used when second factor authentication is enabled +

@@ -440,102 +451,26 @@ export function AccountForm({ isDirty={form.phone !== undefined} />
+

+ To be used when second factor authentication is enabled +

- {showingCurrentUserInfo && isCashoutEnabled && ( - { form.cashout_payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} + value={(form.cashout_payto_uri ?? defaultValue.cashout_payto_uri) as PaytoString} + disabled={!editableCashout} /> )} -
- - { - form.debit_threshold = e as AmountString; - updateForm(structuredClone(form)); - } - } - /> - -

- - How much is user able to transfer after zero balance - -

-
- - {purpose !== "create" || !userIsAdmin ? undefined : ( -
-
- - - Is this a payment provider? - - - -
-
- )} {/* channel, not shown if old cashout api */} {OLD_CASHOUT_API || config.supported_tan_channels.length === 0 ? undefined : ( @@ -584,7 +519,7 @@ export function AccountForm({ {purpose !== "show" && !hasEmail && - i18n.str`Add a email in your profile to enable this option`} + i18n.str`Add an email in your profile to enable this option`} ({ )} +
+ + { + form.debit_threshold = e as AmountString; + updateForm(structuredClone(form)); + } + } + /> + +

+ How much the balance can go below zero. +

+
+
@@ -703,11 +670,51 @@ export function AccountForm({

- - Public accounts have their balance publicly accessible - + Public accounts have their balance publicly accessible

+ + {purpose !== "create" || !userIsAdmin ? undefined : ( +
+
+ + + Is this account a payment provider? + + + +
+
+ )} {children} @@ -715,13 +722,14 @@ export function AccountForm({ ); } -function stringifyIbanPayto(s: PaytoString | undefined): string | undefined { +function getAccountId(type: "iban" | "x-taler-bank", s: PaytoString | undefined): string | undefined { if (s === undefined) return undefined; const p = parsePaytoUri(s); if (p === undefined) return undefined; - if (!p.isKnown) return undefined; - if (p.targetType !== "iban") return undefined; - return p.iban; + if (!p.isKnown) return ""; + if (type === "iban" && p.targetType === "iban") return p.iban; + if (type === "x-taler-bank" && p.targetType === "x-taler-bank") return p.account; + return ""; } { @@ -762,126 +770,128 @@ function stringifyIbanPayto(s: PaytoString | undefined): string | undefined { */ } -function PaytoField({ - name, - label, - help, - type, - value, - disabled, - onChange, - error, -}: { - error: TranslatedString | undefined; - name: string; - label: TranslatedString; - help: TranslatedString; - onChange: (s: string) => void; - type: "iban" | "x-taler-bank" | "bitcoin"; - disabled?: boolean; - value: string | undefined; -}): VNode { - if (type === "iban") { - return ( -
- -
-
- { - onChange(e.currentTarget.value); - }} - /> - value ?? ""} - /> -
- -
-

{help}

-
- ); - } - if (type === "x-taler-bank") { - return ( -
- -
-
- - value ?? ""} - /> -
- -
-

- {/* internal account id */} - {help} -

-
- ); - } - if (type === "bitcoin") { - return ( -
- -
-
- - value ?? ""} - /> - -
-
-

- {/* bitcoin address */} - {help} -

-
- ); - } - assertUnreachable(type); -} +// function PaytoField({ +// name, +// label, +// help, +// type, +// value, +// disabled, +// onChange, +// error, +// }: { +// error: TranslatedString | undefined; +// name: string; +// label: TranslatedString; +// help: TranslatedString; +// onChange: (s: string) => void; +// type: "iban" | "x-taler-bank" | "bitcoin"; +// disabled?: boolean; +// value: string | undefined; +// }): VNode { +// if (type === "iban") { +// return ( +//
+// +//
+//
+// { +// onChange(e.currentTarget.value); +// }} +// /> +// value ?? ""} +// /> +//
+// +//
+//

{help}

+//
+// ); +// } +// if (type === "x-taler-bank") { +// return ( +//
+// +//
+//
+// { +// onChange(e.currentTarget.value); +// }} +// /> +// value ?? ""} +// /> +//
+// +//
+//

+// {help} +//

+//
+// ); +// } +// if (type === "bitcoin") { +// return ( +//
+// +//
+//
+// +// value ?? ""} +// /> +// +//
+//
+//

+// {/* bitcoin address */} +// {help} +//

+//
+// ); +// } +// assertUnreachable(type); +// } diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx index 41d54c43d..5528b5226 100644 --- a/packages/demobank-ui/src/pages/admin/AccountList.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx @@ -62,6 +62,7 @@ export function AccountList({ } } + const { accounts } = result.data.body; return ( @@ -170,15 +171,20 @@ export function AccountList({ Change password
- - Cashouts - -
+ {config.allow_conversion ? + + + + Cashouts + +
+
+ : undefined} {noBalance ? ( + title={i18n.str`Cashout are disabled`} + > + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); } default: diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx index c4e4266f9..23d5a1e90 100644 --- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -29,7 +29,6 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { mutate } from "swr"; import { useBankCoreApiContext } from "../../context/config.js"; import { useBackendState } from "../../hooks/backend.js"; import { RouteDefinition } from "../../route.js"; @@ -70,7 +69,6 @@ export function CreateNewAccount({ const resp = await api.createAccount(token, submitAccount); if (resp.type === "ok") { - mutate(() => true); // clean account list notifyInfo( i18n.str`Account created with password "${submitAccount.password}". The user must change the password on the next login.`, ); diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index 8ec34276f..6d538575b 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -140,8 +140,10 @@ export function CreateCashout({ return ( + title={i18n.str`Cashout are disabled`} + > + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); } default: @@ -188,8 +190,7 @@ export function CreateCashout({ * depending on the isDebit flag */ const inputAmount = Amounts.parseOrThrow( - `${form.isDebit ? regional_currency : fiat_currency}:${ - !form.amount ? "0" : form.amount + `${form.isDebit ? regional_currency : fiat_currency}:${!form.amount ? "0" : form.amount }`, ); @@ -291,7 +292,7 @@ export function CreateCashout({ case HttpStatusCode.NotImplemented: return notify({ type: "error", - title: i18n.str`Cashouts are not supported`, + title: i18n.str`Cashout are disabled`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); @@ -471,9 +472,9 @@ export function CreateCashout({ cashoutDisabled ? undefined : (value) => { - form.amount = value; - updateForm(structuredClone(form)); - } + form.amount = value; + updateForm(structuredClone(form)); + } } /> {Amounts.isZero(sellFee) || - Amounts.isZero(calc.beforeFee) ? undefined : ( + Amounts.isZero(calc.beforeFee) ? undefined : (
@@ -547,7 +548,7 @@ export function CreateCashout({ {/* channel, not shown if new cashout api */} {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels - .length === 0 ? ( + .length === 0 ? (
{ if (!resultAccount.body.contact_data?.phone) return; diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx index 7b251d3ca..1e70886ad 100644 --- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx +++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx @@ -69,8 +69,10 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode { return ( + title={i18n.str`Cashout are disabled`} + > + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); default: assertUnreachable(result); @@ -87,7 +89,11 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode { switch (info.case) { case HttpStatusCode.NotImplemented: { return ( - + + Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode. + ); } default: diff --git a/packages/demobank-ui/src/utils.ts b/packages/demobank-ui/src/utils.ts index 4413ce814..ab0b60d72 100644 --- a/packages/demobank-ui/src/utils.ts +++ b/packages/demobank-ui/src/utils.ts @@ -23,6 +23,7 @@ import { } from "@gnu-taler/taler-util"; import { ErrorNotification, + InternationalizationAPI, notify, notifyError, useTranslationContext, @@ -72,36 +73,36 @@ export type PartialButDefined = { */ export type WithIntermediate = { [prop in keyof Type]: Type[prop] extends PaytoString - ? Type[prop] | undefined - : Type[prop] extends AmountString - ? Type[prop] | undefined - : Type[prop] extends TranslatedString - ? Type[prop] | undefined - : Type[prop] extends object - ? WithIntermediate - : Type[prop] | undefined; + ? Type[prop] | undefined + : Type[prop] extends AmountString + ? Type[prop] | undefined + : Type[prop] extends TranslatedString + ? Type[prop] | undefined + : Type[prop] extends object + ? WithIntermediate + : Type[prop] | undefined; }; export type RecursivePartial = { [P in keyof Type]?: Type[P] extends (infer U)[] - ? RecursivePartial[] - : Type[P] extends object - ? RecursivePartial - : Type[P]; + ? RecursivePartial[] + : Type[P] extends object + ? RecursivePartial + : Type[P]; }; export type ErrorMessageMappingFor = { [prop in keyof Type]+?: Exclude extends PaytoString // enumerate known object - ? TranslatedString - : Exclude extends AmountString - ? TranslatedString - : Exclude extends TranslatedString - ? TranslatedString - : // arrays: every element - Exclude extends (infer U)[] - ? ErrorMessageMappingFor[] - : // map: every field - Exclude extends object - ? ErrorMessageMappingFor - : TranslatedString; + ? TranslatedString + : Exclude extends AmountString + ? TranslatedString + : Exclude extends TranslatedString + ? TranslatedString + : // arrays: every element + Exclude extends (infer U)[] + ? ErrorMessageMappingFor[] + : // map: every field + Exclude extends object + ? ErrorMessageMappingFor + : TranslatedString; }; export enum TanChannel { @@ -367,26 +368,30 @@ export const COUNTRY_TABLE = { * If the remainder is 1, the check digit test is passed and the IBAN might be valid. * */ +const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; export function validateIBAN( - iban: string, - i18n: ReturnType["i18n"], + account: string, + i18n: InternationalizationAPI, ): TranslatedString | undefined { + if (!IBAN_REGEX.test(account)) { + return i18n.str`IBAN only have uppercased letters and numbers` + } // Check total length - if (iban.length < 4) - return i18n.str`IBAN numbers usually have more that 4 digits`; - if (iban.length > 34) - return i18n.str`IBAN numbers usually have less that 34 digits`; + if (account.length < 4) + return i18n.str`IBAN numbers have more that 4 digits`; + if (account.length > 34) + return i18n.str`IBAN numbers have less that 34 digits`; const A_code = "A".charCodeAt(0); const Z_code = "Z".charCodeAt(0); - const IBAN = iban.toUpperCase(); + const IBAN = account.toUpperCase(); // check supported country const code = IBAN.substring(0, 2); const found = code in COUNTRY_TABLE; if (!found) return i18n.str`IBAN country code not found`; // 2.- Move the four initial characters to the end of the string - const step2 = IBAN.substring(4) + iban.substring(0, 4); + const step2 = IBAN.substring(4) + account.substring(0, 4); const step3 = Array.from(step2) .map((letter) => { const code = letter.charCodeAt(0); @@ -411,3 +416,33 @@ function calculate_iban_checksum(str: string): number { } return result; } + +const USERNAME_REGEX = /^[A-Za-z][A-Za-z0-9]*$/; + +export function validateTalerBank( + account: string, + i18n: InternationalizationAPI, +): TranslatedString | undefined { + if (!USERNAME_REGEX.test(account)) { + return i18n.str`Account only have letters and numbers` + } + return undefined +} + +export function validateRawIBAN( + payto: string, + i18n: InternationalizationAPI, +): TranslatedString | undefined { + return undefined +} + + + +export function validateRawTalerBank( + payto: string, + currentHost: string, + i18n: InternationalizationAPI, +): TranslatedString | undefined { + return undefined +} + -- cgit v1.2.3