aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-02-06 16:51:15 -0300
committerSebastian <sebasjm@gmail.com>2024-02-06 16:51:42 -0300
commit4eda6ac07c78bcb3c2daa7846b4cd36048f9c7dd (patch)
tree056f0b56bdcf308c0c08d8851c485fcdc444011d /packages/demobank-ui/src
parent6c496c070d47e26034a3e2dd6d14a1a9ea42b729 (diff)
support for x-taler-bank and fix cache invalidation when new account is created
Diffstat (limited to 'packages/demobank-ui/src')
-rw-r--r--packages/demobank-ui/src/Routing.tsx11
-rw-r--r--packages/demobank-ui/src/components/Cashouts/views.tsx20
-rw-r--r--packages/demobank-ui/src/context/config.ts25
-rw-r--r--packages/demobank-ui/src/hooks/access.ts16
-rw-r--r--packages/demobank-ui/src/hooks/circuit.ts24
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx15
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx357
-rw-r--r--packages/demobank-ui/src/pages/SolveChallengePage.tsx6
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx536
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx24
-rw-r--r--packages/demobank-ui/src/pages/admin/AdminHome.tsx6
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx2
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx23
-rw-r--r--packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx12
-rw-r--r--packages/demobank-ui/src/utils.ts99
15 files changed, 708 insertions, 468 deletions
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 (
- <BankFrame account={username}>
+ <BankFrame account={username} routeAccountDetails={privatePages.myAccountDetails}>
<PrivateRouting username={username} isAdmin={isUserAdministrator} />
</BankFrame>
);
@@ -147,7 +147,6 @@ function PublicRounting({
<div class="sm:mx-auto sm:w-full sm:max-w-sm">
<h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${settings.bankName}!`}</h2>
</div>
-
<LoginForm routeRegister={publicPages.register} />
</Fragment>
);
@@ -228,19 +227,19 @@ export const privatePages = {
myAccountPassword: urlPattern(/\/my-password/, () => "#/my-password"),
myAccountCashouts: urlPattern(/\/my-cashouts/, () => "#/my-cashouts"),
accountDetails: urlPattern<{ account: string }>(
- /\/profile\/(?<account>[a-zA-Z0-9]+)\/details/,
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/details/,
({ account }) => `#/profile/${account}/details`,
),
accountChangePassword: urlPattern<{ account: string }>(
- /\/profile\/(?<account>[a-zA-Z0-9]+)\/change-password/,
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/change-password/,
({ account }) => `#/profile/${account}/change-password`,
),
accountDelete: urlPattern<{ account: string }>(
- /\/profile\/(?<account>[a-zA-Z0-9]+)\/delete/,
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/delete/,
({ account }) => `#/profile/${account}/delete`,
),
accountCashouts: urlPattern<{ account: string }>(
- /\/profile\/(?<account>[a-zA-Z0-9]+)\/cashouts/,
+ /\/profile\/(?<account>[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 (
<Attention
type="danger"
- title={i18n.str`Cashout not implemented`}
- ></Attention>
+ title={i18n.str`Cashout are disabled`}
+ >
+ <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
+ </Attention>
);
}
default:
@@ -66,8 +68,10 @@ export function ReadyView({
return (
<Attention
type="danger"
- title={i18n.str`Cashout not implemented`}
- ></Attention>
+ title={i18n.str`Cashout are disabled`}
+ >
+ <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
+ </Attention>
);
}
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 (
<tr
key={idx}
diff --git a/packages/demobank-ui/src/context/config.ts b/packages/demobank-ui/src/context/config.ts
index 5d8a5c73f..1cabab51c 100644
--- a/packages/demobank-ui/src/context/config.ts
+++ b/packages/demobank-ui/src/context/config.ts
@@ -157,8 +157,8 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
async deleteAccount(auth: UserAndToken, cid?: string | undefined) {
const resp = await super.deleteAccount(auth, cid);
if (resp.type === "ok") {
- revalidatePublicAccounts();
- revalidateBusinessAccounts();
+ await revalidatePublicAccounts();
+ await revalidateBusinessAccounts();
}
return resp;
}
@@ -168,8 +168,11 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
) {
const resp = await super.createAccount(auth, body);
if (resp.type === "ok") {
- revalidatePublicAccounts();
- revalidateBusinessAccounts();
+ // admin balance change on new account
+ await revalidateAccountDetails();
+ await revalidateTransactions();
+ await revalidatePublicAccounts();
+ await revalidateBusinessAccounts();
}
return resp;
}
@@ -180,7 +183,7 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
) {
const resp = await super.updateAccount(auth, body, cid);
if (resp.type === "ok") {
- revalidateAccountDetails();
+ await revalidateAccountDetails();
}
return resp;
}
@@ -191,8 +194,8 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
) {
const resp = await super.createTransaction(auth, body, cid);
if (resp.type === "ok") {
- revalidateAccountDetails();
- revalidateTransactions();
+ await revalidateAccountDetails();
+ await revalidateTransactions();
}
return resp;
}
@@ -203,8 +206,8 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
) {
const resp = await super.confirmWithdrawalById(auth, wid, cid);
if (resp.type === "ok") {
- revalidateAccountDetails();
- revalidateTransactions();
+ await revalidateAccountDetails();
+ await revalidateTransactions();
}
return resp;
}
@@ -215,8 +218,8 @@ export class CacheAwareApi extends TalerCoreBankHttpClient {
) {
const resp = await super.createCashout(auth, body, cid);
if (resp.type === "ok") {
- revalidateAccountDetails();
- revalidateCashouts();
+ await revalidateAccountDetails();
+ await revalidateCashouts();
}
return resp;
}
diff --git a/packages/demobank-ui/src/hooks/access.ts b/packages/demobank-ui/src/hooks/access.ts
index 85d030245..e07a3d1b1 100644
--- a/packages/demobank-ui/src/hooks/access.ts
+++ b/packages/demobank-ui/src/hooks/access.ts
@@ -35,7 +35,7 @@ export interface InstanceTemplateFilter {
}
export function revalidateAccountDetails() {
- mutate(
+ return mutate(
(key) => 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 <Fragment />;
}
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<string | undefined>(toAccount);
+
+ const [account, setAccount] = useState<string | undefined>(toAccount);
const [subject, setSubject] = useState<string | undefined>();
const [amount, setAmount] = useState<string | undefined>();
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({
<div class="p-4 sm:p-8">
{!isRawPayto ? (
<div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- for="iban"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Recipient`}</label>
- <div class="mt-2">
- <input
- ref={focus ? doAutoFocus : undefined}
- type="text"
- class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
- disabled={sendingToFixedAccount}
- value={iban ?? ""}
- placeholder="CC0123456789"
- autocomplete="off"
- required
- pattern={ibanRegex}
- onInput={(e): void => {
- setIban(e.currentTarget.value.toUpperCase());
- }}
- />
- <ShowInputErrorLabel
- message={errorsWire?.iban}
- isDirty={iban !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>
- IBAN of the recipient's account
- </i18n.Translate>
- </p>
- </div>
+ {(() => {
+ switch (paytoType) {
+ case "x-taler-bank": {
+ return <TextField
+ id="x-taler-bank"
+ label={i18n.str`Recipient`}
+ help={i18n.str`Id of the recipient's account`}
+ error={errorsWire?.account}
+ onChange={setAccount}
+ value={account}
+ placeholder={i18n.str`username`}
+ focus={focus}
+ disabled={sendingToFixedAccount}
+ />
+ }
+ case "iban": {
+ return <TextField
+ id="iban"
+ label={i18n.str`Recipient`}
+ help={i18n.str`IBAN of the recipient's account`}
+ placeholder={"CC0123456789" as TranslatedString}
+ error={errorsWire?.account}
+ onChange={(v) => setAccount(v.toUpperCase())}
+ value={account}
+ focus={focus}
+ disabled={sendingToFixedAccount}
+ />
+ }
+ default: assertUnreachable(paytoType)
+ }
+ })()}
<div class="sm:col-span-5">
<label
@@ -434,7 +461,13 @@ export function PaytoWireTransferForm({
value={rawPaytoInput ?? ""}
required
title={i18n.str`Uniform resource identifier of the target account`}
- placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`}
+
+ placeholder={((): TranslatedString => {
+ switch (paytoType) {
+ case "x-taler-bank": return i18n.str`payto://x-taler-bank/[bank-host]/[receiver-account]?message=[subject]&amount=[${limit.currency}:X.Y]`
+ case "iban": return i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`
+ }
+ })()}
onInput={(e): void => {
rawPaytoInputSetter(e.currentTarget.value);
}}
@@ -538,13 +571,13 @@ export function InputAmount(
if (
sep_pos !== -1 &&
l - sep_pos - 1 >
- config.currency_specification.num_fractional_input_digits
+ config.currency_specification.num_fractional_input_digits
) {
e.currentTarget.value = e.currentTarget.value.substring(
0,
sep_pos +
- config.currency_specification.num_fractional_input_digits +
- 1,
+ config.currency_specification.num_fractional_input_digits +
+ 1,
);
}
onChange(e.currentTarget.value);
@@ -587,3 +620,147 @@ export function RenderAmount({
</span>
);
}
+
+
+function validateRawPayto(parsed: PaytoUri, limit: AmountJson, host: string, i18n: InternationalizationAPI, type: "iban" | "x-taler-bank"): TranslatedString | undefined {
+ if (!parsed.isKnown) {
+ return i18n.str`The target type is unknown, use "${type}"`
+ }
+ let result: TranslatedString | undefined;
+ switch (type) {
+ case "x-taler-bank": {
+ if (parsed.targetType !== "x-taler-bank") {
+ return i18n.str`Only "x-taler-bank" target are supported`
+ }
+
+ if (parsed.host !== host) {
+ return i18n.str`Only this host is allowed. Use "${host}"`
+ }
+
+ if (!parsed.account) {
+ return i18n.str`Missing account name`
+ }
+ const result = validateTalerBank(parsed.account, i18n)
+ if (result) return result
+ break;
+ }
+ case "iban": {
+ if (parsed.targetType !== "iban") {
+ return i18n.str`Only "IBAN" target are supported`
+ }
+ const result = validateIBAN(parsed.iban, i18n)
+ if (result) return result
+ break;
+ }
+ default: assertUnreachable(type)
+ }
+ if (!parsed.params.amount) {
+ return i18n.str`Missing "amount" parameter to specify the amount to be transferred`
+ }
+ const amount = Amounts.parse(parsed.params.amount)
+ if (!amount) {
+ return i18n.str`The "amount" parameter is not valid`
+ }
+ result = validateAmount(amount, limit, i18n)
+ if (result) return result;
+
+ if (!parsed.params.message) {
+ return i18n.str`Missing the "message" parameter to specify a reference text for the transfer`
+ }
+ const subject = parsed.params.message
+ result = validateSubject(subject, i18n)
+ if (result) return result;
+
+ return undefined
+}
+
+function validateAmount(amount: AmountJson, limit: AmountJson, i18n: InternationalizationAPI): TranslatedString | undefined {
+ if (amount.currency !== limit.currency) {
+ return i18n.str`The only currecy allowed is "${limit.currency}"`
+ }
+ if (Amounts.isZero(amount)) {
+ return i18n.str`Can't transfer zero amount`
+ }
+ if (Amounts.cmp(limit, amount) === -1) {
+ return i18n.str`Balance is not enough`
+ }
+ return undefined
+}
+
+function validateSubject(text: string, i18n: InternationalizationAPI): TranslatedString | undefined {
+ if (text.length < 2) {
+ return i18n.str`Use a longer subject`
+ }
+ return undefined
+}
+
+interface PaytoFieldProps {
+ id: string,
+ label: TranslatedString;
+ help?: TranslatedString;
+ placeholder?: TranslatedString;
+ error: string | undefined;
+ value: string | undefined;
+ rightIcons?: VNode;
+ onChange: (p: string) => void;
+ focus?: boolean;
+ disabled?: boolean;
+}
+
+function Wrapper({ withIcon, children }: { withIcon: boolean, children: ComponentChildren }): VNode {
+ if (withIcon) {
+ return <div class="flex justify-between">
+ {children}
+ </div>
+ }
+ return <Fragment>{children}</Fragment>
+}
+
+export function TextField({
+ id,
+ label,
+ help,
+ focus,
+ disabled,
+ onChange,
+ placeholder,
+ rightIcons,
+ value,
+ error,
+}: PaytoFieldProps): VNode {
+ return <div class="sm:col-span-5">
+ <label
+ for={id}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{label}</label>
+ <div class="mt-2">
+ <Wrapper withIcon={rightIcons !== undefined}>
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="text"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name={id}
+ id={id}
+ disabled={disabled}
+ value={value ?? ""}
+ placeholder={placeholder}
+ autocomplete="off"
+ required
+ onInput={(e): void => {
+ onChange(e.currentTarget.value);
+ }}
+ />
+ {rightIcons}
+ </Wrapper>
+ <ShowInputErrorLabel
+ message={error}
+ isDirty={value !== undefined}
+ />
+ </div>
+ {help &&
+ <p class="mt-2 text-sm text-gray-500">
+ {help}
+ </p>
+ }
+ </div>
+}
diff --git a/packages/demobank-ui/src/pages/SolveChallengePage.tsx b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
index de0ba483f..61decf586 100644
--- a/packages/demobank-ui/src/pages/SolveChallengePage.tsx
+++ b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
@@ -666,8 +666,10 @@ function ShowCashoutDetails({
return (
<Attention
type="danger"
- title={i18n.str`Cashout not implemented`}
- ></Attention>
+ title={i18n.str`Cashout are disabled`}
+ >
+ <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
+ </Attention>
);
}
default:
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index 5d4a5c5db..3aba99cea 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -39,11 +39,11 @@ import {
TanChannel,
undefinedIfEmpty,
validateIBAN,
+ validateTalerBank,
} from "../../utils.js";
-import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
+import { InputAmount, TextField, doAutoFocus } from "../PaytoWireTransferForm.js";
import { getRandomPassword } from "../rnd.js";
-const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
const EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
@@ -90,7 +90,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
onChange: ChangeByPurposeType[PurposeType];
purpose: PurposeType;
}): VNode {
- const { config, hints } = useBankCoreApiContext();
+ const { config, hints, url } = useBankCoreApiContext();
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
const [form, setForm] = useState<AccountFormData>({});
@@ -99,6 +99,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
ErrorMessageMappingFor<typeof defaultValue> | undefined
>(undefined);
+ const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as const : "iban" as const;
+ const cashoutPaytoType: typeof paytoType = "iban" as const;
+
const defaultValue: AccountFormData = {
debit_threshold: Amounts.stringifyValue(
template?.debit_threshold ?? config.default_debit_threshold,
@@ -107,8 +110,8 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
isPublic: template?.is_public,
name: template?.name ?? "",
cashout_payto_uri:
- stringifyIbanPayto(template?.cashout_payto_uri) ?? ("" as PaytoString),
- payto_uri: stringifyIbanPayto(template?.payto_uri) ?? ("" as PaytoString),
+ getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ?? ("" as PaytoString),
+ payto_uri: getAccountId(paytoType, template?.payto_uri) ?? ("" as PaytoString),
email: template?.contact_data?.email ?? "",
phone: template?.contact_data?.phone ?? "",
username: username ?? "",
@@ -117,10 +120,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1;
- const showingCurrentUserInfo =
- credentials.status !== "loggedIn"
- ? false
- : username === credentials.username;
const userIsAdmin =
credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator;
@@ -131,7 +130,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
const isCashoutEnabled = config.allow_conversion;
const editableCashout =
- showingCurrentUserInfo &&
(purpose === "create" ||
(purpose === "update" &&
(config.allow_edit_cashout_payto_uri || userIsAdmin)));
@@ -143,13 +141,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
const hasEmail = !!defaultValue.email || !!form.email;
function updateForm(newForm: typeof defaultValue): void {
- const cashoutParsed = !newForm.cashout_payto_uri
- ? undefined
- : buildPayto("iban", newForm.cashout_payto_uri, undefined);
-
- const internalParsed = !newForm.payto_uri
- ? undefined
- : buildPayto("iban", newForm.payto_uri, undefined);
const trimmedAmountStr = newForm.debit_threshold?.trim();
const parsedAmount = Amounts.parse(
@@ -163,24 +154,20 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
? undefined
: !editableCashout
? undefined
- : !cashoutParsed
- ? i18n.str`Doesn't have the pattern of an IBAN number`
- : !cashoutParsed.isKnown || cashoutParsed.targetType !== "iban"
- ? i18n.str`Only "IBAN" target are supported`
- : !IBAN_REGEX.test(cashoutParsed.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(cashoutParsed.iban, i18n),
+ : !newForm.cashout_payto_uri ? undefined
+ : cashoutPaytoType === "iban" ? validateIBAN(newForm.cashout_payto_uri, i18n) :
+ cashoutPaytoType === "x-taler-bank" ? validateTalerBank(newForm.cashout_payto_uri, i18n) :
+ undefined,
+
payto_uri: !newForm.payto_uri
? undefined
: !editableAccount
? undefined
- : !internalParsed
- ? i18n.str`Doesn't have the pattern of an IBAN number`
- : !internalParsed.isKnown || internalParsed.targetType !== "iban"
- ? i18n.str`Only "IBAN" target are supported`
- : !IBAN_REGEX.test(internalParsed.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : validateIBAN(internalParsed.iban, i18n),
+ : !newForm.payto_uri ? undefined
+ : paytoType === "iban" ? validateIBAN(newForm.payto_uri, i18n) :
+ paytoType === "x-taler-bank" ? validateTalerBank(newForm.payto_uri, i18n) :
+ undefined,
+
email: !newForm.email
? undefined
: !EMAIL_REGEX.test(newForm.email)
@@ -219,14 +206,31 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
if (errors) {
onChange(undefined);
} else {
- const cashout = !newForm.cashout_payto_uri
- ? undefined
- : buildPayto("iban", newForm.cashout_payto_uri, undefined);
+ let cashout;
+ if (newForm.cashout_payto_uri) switch (cashoutPaytoType) {
+ case "x-taler-bank": {
+ cashout = buildPayto("x-taler-bank", url.host, newForm.cashout_payto_uri);
+ break;
+ }
+ case "iban": {
+ cashout = buildPayto("iban", newForm.cashout_payto_uri, undefined);
+ break;
+ }
+ default: assertUnreachable(cashoutPaytoType)
+ }
const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout);
-
- const internal = !newForm.payto_uri
- ? undefined
- : buildPayto("iban", newForm.payto_uri, undefined);
+ let internal;
+ if (newForm.payto_uri) switch (paytoType) {
+ case "x-taler-bank": {
+ internal = buildPayto("x-taler-bank", url.host, newForm.payto_uri);
+ break;
+ }
+ case "iban": {
+ internal = buildPayto("iban", newForm.payto_uri, undefined);
+ break;
+ }
+ default: assertUnreachable(paytoType)
+ }
const internalURI = !internal ? undefined : stringifyPaytoUri(internal);
const threshold = !parsedAmount
@@ -328,7 +332,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
/>
</div>
<p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Account identification</i18n.Translate>
+ <i18n.Translate>Account id for authentication</i18n.Translate>
</p>
</div>
@@ -366,22 +370,26 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
</p>
</div>
- <PaytoField
- type="iban"
- name="internal-account"
- label={i18n.str`Internal IBAN`}
+ <TextField
+ id="internal-account"
+ label={i18n.str`Internal account`}
help={
purpose === "create"
- ? i18n.str`If empty a random account number will be assigned`
- : i18n.str`Account number for bank transfers`
+ ? i18n.str`If empty a random account id will be assigned`
+ : i18n.str`Share this id to receive bank transfers`
}
- value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString}
- disabled={!editableAccount}
+
error={errors?.payto_uri}
onChange={(e) => {
form.payto_uri = e as PaytoString;
updateForm(structuredClone(form));
}}
+ rightIcons={<CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => form.payto_uri ?? defaultValue.payto_uri ?? ""}
+ />}
+ value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString}
+ disabled={!editableAccount}
/>
<div class="sm:col-span-5">
@@ -411,6 +419,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
isDirty={form.email !== undefined}
/>
</div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>To be used when second factor authentication is enabled</i18n.Translate>
+ </p>
</div>
<div class="sm:col-span-5">
@@ -440,102 +451,26 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
isDirty={form.phone !== undefined}
/>
</div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>To be used when second factor authentication is enabled</i18n.Translate>
+ </p>
</div>
- {showingCurrentUserInfo && isCashoutEnabled && (
- <PaytoField
- type="iban"
- name="cashout-account"
- label={i18n.str`Cashout IBAN`}
+ {isCashoutEnabled && (
+ <TextField
+ id="cashout-account"
+ label={i18n.str`Cashout account`}
help={i18n.str`External account number where the money is going to be sent when doing cashouts`}
- value={
- (form.cashout_payto_uri ??
- defaultValue.cashout_payto_uri) as PaytoString
- }
- disabled={!editableCashout}
error={errors?.cashout_payto_uri}
onChange={(e) => {
form.cashout_payto_uri = e as PaytoString;
updateForm(structuredClone(form));
}}
+ value={(form.cashout_payto_uri ?? defaultValue.cashout_payto_uri) as PaytoString}
+ disabled={!editableCashout}
/>
)}
- <div class="sm:col-span-5">
- <label
- for="debit"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Max debt`}</label>
- <InputAmount
- name="debit"
- left
- currency={config.currency}
- value={form.debit_threshold ?? defaultValue.debit_threshold}
- onChange={
- !editableThreshold
- ? undefined
- : (e) => {
- form.debit_threshold = e as AmountString;
- updateForm(structuredClone(form));
- }
- }
- />
- <ShowInputErrorLabel
- message={
- errors?.debit_threshold
- ? String(errors?.debit_threshold)
- : undefined
- }
- isDirty={form.debit_threshold !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>
- How much is user able to transfer after zero balance
- </i18n.Translate>
- </p>
- </div>
-
- {purpose !== "create" || !userIsAdmin ? undefined : (
- <div class="sm:col-span-5">
- <div class="flex items-center justify-between">
- <span class="flex flex-grow flex-col">
- <span
- class="text-sm text-black font-medium leading-6 "
- id="availability-label"
- >
- <i18n.Translate>Is this a payment provider?</i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- data-enabled={
- form.isExchange ?? defaultValue.isExchange
- ? "true"
- : "false"
- }
- 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.isExchange = !form.isExchange;
- updateForm(structuredClone(form));
- }}
- >
- <span
- aria-hidden="true"
- data-enabled={
- form.isExchange ?? defaultValue.isExchange
- ? "true"
- : "false"
- }
- class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
- ></span>
- </button>
- </div>
- </div>
- )}
{/* channel, not shown if old cashout api */}
{OLD_CASHOUT_API ||
config.supported_tan_channels.length === 0 ? undefined : (
@@ -584,7 +519,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
</span>
{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`}
</span>
</span>
<svg
@@ -669,6 +604,38 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
)}
<div class="sm:col-span-5">
+ <label
+ for="debit"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Max debt`}</label>
+ <InputAmount
+ name="debit"
+ left
+ currency={config.currency}
+ value={form.debit_threshold ?? defaultValue.debit_threshold}
+ onChange={
+ !editableThreshold
+ ? undefined
+ : (e) => {
+ form.debit_threshold = e as AmountString;
+ updateForm(structuredClone(form));
+ }
+ }
+ />
+ <ShowInputErrorLabel
+ message={
+ errors?.debit_threshold
+ ? String(errors?.debit_threshold)
+ : undefined
+ }
+ isDirty={form.debit_threshold !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>How much the balance can go below zero.</i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
<span
@@ -703,11 +670,51 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
</button>
</div>
<p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>
- Public accounts have their balance publicly accessible
- </i18n.Translate>
+ <i18n.Translate>Public accounts have their balance publicly accessible</i18n.Translate>
</p>
</div>
+
+ {purpose !== "create" || !userIsAdmin ? undefined : (
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Is this account a payment provider?</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ data-enabled={
+ form.isExchange ?? defaultValue.isExchange
+ ? "true"
+ : "false"
+ }
+ 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.isExchange = !form.isExchange;
+ updateForm(structuredClone(form));
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={
+ form.isExchange ?? defaultValue.isExchange
+ ? "true"
+ : "false"
+ }
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ </div>
+ )}
</div>
</div>
{children}
@@ -715,13 +722,14 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
);
}
-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 "<unkown>";
+ if (type === "iban" && p.targetType === "iban") return p.iban;
+ if (type === "x-taler-bank" && p.targetType === "x-taler-bank") return p.account;
+ return "<unsupported>";
}
{
@@ -762,126 +770,128 @@ function stringifyIbanPayto(s: PaytoString | undefined): string | undefined {
</div> */
}
-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 (
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for={name}
- >
- {label}
- </label>
- <div class="mt-2">
- <div class="flex justify-between">
- <input
- type="text"
- class="mr-4 w-full block-inline disabled:bg-gray-100 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={name}
- id={name}
- disabled={disabled}
- value={value ?? ""}
- onChange={(e) => {
- onChange(e.currentTarget.value);
- }}
- />
- <CopyButton
- class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
- getContent={() => value ?? ""}
- />
- </div>
- <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
- </div>
- <p class="mt-2 text-sm text-gray-500">{help}</p>
- </div>
- );
- }
- if (type === "x-taler-bank") {
- return (
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for={name}
- >
- {label}
- </label>
- <div class="mt-2">
- <div class="flex justify-between">
- <input
- type="text"
- class="mr-4 w-full block-inline disabled:bg-gray-100 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={name}
- id={name}
- disabled={disabled}
- value={value ?? ""}
- />
- <CopyButton
- class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
- getContent={() => value ?? ""}
- />
- </div>
- <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
- </div>
- <p class="mt-2 text-sm text-gray-500">
- {/* <i18n.Translate>internal account id</i18n.Translate> */}
- {help}
- </p>
- </div>
- );
- }
- if (type === "bitcoin") {
- return (
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for={name}
- >
- {label}
- </label>
- <div class="mt-2">
- <div class="flex justify-between">
- <input
- type="text"
- class="mr-4 w-full block-inline disabled:bg-gray-100 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={name}
- id={name}
- disabled={disabled}
- value={value ?? ""}
- />
- <CopyButton
- class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
- getContent={() => value ?? ""}
- />
- <ShowInputErrorLabel
- message={error}
- isDirty={value !== undefined}
- />
- </div>
- </div>
- <p class="mt-2 text-sm text-gray-500">
- {/* <i18n.Translate>bitcoin address</i18n.Translate> */}
- {help}
- </p>
- </div>
- );
- }
- 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 (
+// <div class="sm:col-span-5">
+// <label
+// class="block text-sm font-medium leading-6 text-gray-900"
+// for={name}
+// >
+// {label}
+// </label>
+// <div class="mt-2">
+// <div class="flex justify-between">
+// <input
+// type="text"
+// class="mr-4 w-full block-inline disabled:bg-gray-100 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={name}
+// id={name}
+// disabled={disabled}
+// value={value ?? ""}
+// onChange={(e) => {
+// onChange(e.currentTarget.value);
+// }}
+// />
+// <CopyButton
+// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+// getContent={() => value ?? ""}
+// />
+// </div>
+// <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+// </div>
+// <p class="mt-2 text-sm text-gray-500">{help}</p>
+// </div>
+// );
+// }
+// if (type === "x-taler-bank") {
+// return (
+// <div class="sm:col-span-5">
+// <label
+// class="block text-sm font-medium leading-6 text-gray-900"
+// for={name}
+// >
+// {label}
+// </label>
+// <div class="mt-2">
+// <div class="flex justify-between">
+// <input
+// type="text"
+// class="mr-4 w-full block-inline disabled:bg-gray-100 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={name}
+// id={name}
+// disabled={disabled}
+// value={value ?? ""}
+// onChange={(e) => {
+// onChange(e.currentTarget.value);
+// }}
+// />
+// <CopyButton
+// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+// getContent={() => value ?? ""}
+// />
+// </div>
+// <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+// </div>
+// <p class="mt-2 text-sm text-gray-500">
+// {help}
+// </p>
+// </div>
+// );
+// }
+// if (type === "bitcoin") {
+// return (
+// <div class="sm:col-span-5">
+// <label
+// class="block text-sm font-medium leading-6 text-gray-900"
+// for={name}
+// >
+// {label}
+// </label>
+// <div class="mt-2">
+// <div class="flex justify-between">
+// <input
+// type="text"
+// class="mr-4 w-full block-inline disabled:bg-gray-100 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={name}
+// id={name}
+// disabled={disabled}
+// value={value ?? ""}
+// />
+// <CopyButton
+// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+// getContent={() => value ?? ""}
+// />
+// <ShowInputErrorLabel
+// message={error}
+// isDirty={value !== undefined}
+// />
+// </div>
+// </div>
+// <p class="mt-2 text-sm text-gray-500">
+// {/* <i18n.Translate>bitcoin address</i18n.Translate> */}
+// {help}
+// </p>
+// </div>
+// );
+// }
+// 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 (
<Fragment>
@@ -170,15 +171,20 @@ export function AccountList({
<i18n.Translate>Change password</i18n.Translate>
</a>
<br />
- <a
- href={routeShowCashoutsAccount.url({
- account: item.username,
- })}
- class="text-indigo-600 hover:text-indigo-900"
- >
- <i18n.Translate>Cashouts</i18n.Translate>
- </a>
- <br />
+ {config.allow_conversion ?
+ <Fragment>
+
+ <a
+ href={routeShowCashoutsAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>Cashouts</i18n.Translate>
+ </a>
+ <br />
+ </Fragment>
+ : undefined}
{noBalance ? (
<a
href={routeRemoveAccount.url({
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
index 1a7edd6b9..35106edeb 100644
--- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -170,8 +170,10 @@ function Metrics({ routeDownloadStats }: {
return (
<Attention
type="danger"
- title={i18n.str`Cashout not implemented`}
- ></Attention>
+ title={i18n.str`Cashout are disabled`}
+ >
+ <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
+ </Attention>
);
}
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 (
<Attention
type="danger"
- title={i18n.str`Cashout not implemented`}
- ></Attention>
+ title={i18n.str`Cashout are disabled`}
+ >
+ <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
+ </Attention>
);
}
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));
+ }
}
/>
<ShowInputErrorLabel
@@ -514,7 +515,7 @@ export function CreateCashout({
</dd>
</div>
{Amounts.isZero(sellFee) ||
- Amounts.isZero(calc.beforeFee) ? undefined : (
+ Amounts.isZero(calc.beforeFee) ? undefined : (
<div class="flex items-center justify-between border-t-2 afu pt-4">
<dt class="flex items-center text-sm text-gray-600">
<span>
@@ -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 ? (
<div class="sm:col-span-5">
<Attention
type="warning"
@@ -619,7 +620,7 @@ export function CreateCashout({
)}
{config.supported_tan_channels.indexOf(TanChannel.SMS) ===
- -1 ? undefined : (
+ -1 ? undefined : (
<label
onClick={() => {
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 (
<Attention
type="warning"
- title={i18n.str`Cashouts are not supported`}
- ></Attention>
+ title={i18n.str`Cashout are disabled`}
+ >
+ <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
+ </Attention>
);
default:
assertUnreachable(result);
@@ -87,7 +89,11 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
switch (info.case) {
case HttpStatusCode.NotImplemented: {
return (
- <Attention type="danger" title={i18n.str`Cashout not implemented`} />
+ <Attention type="danger"
+ title={i18n.str`Cashout are disabled`}
+ >
+ <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
+ </Attention>
);
}
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<T> = {
*/
export type WithIntermediate<Type> = {
[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]>
- : 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]>
+ : Type[prop] | undefined;
};
export type RecursivePartial<Type> = {
[P in keyof Type]?: Type[P] extends (infer U)[]
- ? RecursivePartial<U>[]
- : Type[P] extends object
- ? RecursivePartial<Type[P]>
- : Type[P];
+ ? RecursivePartial<U>[]
+ : Type[P] extends object
+ ? RecursivePartial<Type[P]>
+ : Type[P];
};
export type ErrorMessageMappingFor<Type> = {
[prop in keyof Type]+?: Exclude<Type[prop], undefined> extends PaytoString // enumerate known object
- ? TranslatedString
- : Exclude<Type[prop], undefined> extends AmountString
- ? TranslatedString
- : Exclude<Type[prop], undefined> extends TranslatedString
- ? TranslatedString
- : // arrays: every element
- Exclude<Type[prop], undefined> extends (infer U)[]
- ? ErrorMessageMappingFor<U>[]
- : // map: every field
- Exclude<Type[prop], undefined> extends object
- ? ErrorMessageMappingFor<Type[prop]>
- : TranslatedString;
+ ? TranslatedString
+ : Exclude<Type[prop], undefined> extends AmountString
+ ? TranslatedString
+ : Exclude<Type[prop], undefined> extends TranslatedString
+ ? TranslatedString
+ : // arrays: every element
+ Exclude<Type[prop], undefined> extends (infer U)[]
+ ? ErrorMessageMappingFor<U>[]
+ : // map: every field
+ Exclude<Type[prop], undefined> extends object
+ ? ErrorMessageMappingFor<Type[prop]>
+ : 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<typeof useTranslationContext>["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
+}
+