summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-03-11 14:56:25 -0300
committerSebastian <sebasjm@gmail.com>2024-03-11 14:57:48 -0300
commit37f46f4d6b821d163c3e4db5c374b1120212ac74 (patch)
tree641c5bccd6d1b77fa440e67b80543eec9378ef4a
parent4cbe754aca72b6edee922e3a84f251030293f088 (diff)
downloadwallet-core-37f46f4d6b821d163c3e4db5c374b1120212ac74.tar.gz
wallet-core-37f46f4d6b821d163c3e4db5c374b1120212ac74.tar.bz2
wallet-core-37f46f4d6b821d163c3e4db5c374b1120212ac74.zip
obs and cancel request, plus lint
-rw-r--r--packages/bank-ui/src/Routing.tsx69
-rw-r--r--packages/bank-ui/src/app.tsx2
-rw-r--r--packages/bank-ui/src/components/Cashouts/views.tsx37
-rw-r--r--packages/bank-ui/src/components/Time.tsx51
-rw-r--r--packages/bank-ui/src/components/Transactions/index.ts24
-rw-r--r--packages/bank-ui/src/components/Transactions/state.ts62
-rw-r--r--packages/bank-ui/src/components/Transactions/views.tsx50
-rw-r--r--packages/bank-ui/src/context/config.ts108
-rw-r--r--packages/bank-ui/src/hooks/account.ts30
-rw-r--r--packages/bank-ui/src/hooks/bank-state.ts9
-rw-r--r--packages/bank-ui/src/hooks/form.ts125
-rw-r--r--packages/bank-ui/src/hooks/preferences.ts3
-rw-r--r--packages/bank-ui/src/hooks/regional.ts58
-rw-r--r--packages/bank-ui/src/pages/AccountPage/index.ts12
-rw-r--r--packages/bank-ui/src/pages/AccountPage/views.tsx18
-rw-r--r--packages/bank-ui/src/pages/BankFrame.tsx166
-rw-r--r--packages/bank-ui/src/pages/LoginForm.tsx60
-rw-r--r--packages/bank-ui/src/pages/OperationState/index.ts16
-rw-r--r--packages/bank-ui/src/pages/OperationState/state.ts6
-rw-r--r--packages/bank-ui/src/pages/OperationState/views.tsx17
-rw-r--r--packages/bank-ui/src/pages/PaymentOptions.tsx31
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx304
-rw-r--r--packages/bank-ui/src/pages/ProfileNavigation.tsx8
-rw-r--r--packages/bank-ui/src/pages/PublicHistoriesPage.tsx11
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.tsx22
-rw-r--r--packages/bank-ui/src/pages/RegistrationPage.tsx39
-rw-r--r--packages/bank-ui/src/pages/ShowNotifications.tsx55
-rw-r--r--packages/bank-ui/src/pages/SolveChallengePage.tsx43
-rw-r--r--packages/bank-ui/src/pages/WalletWithdrawForm.tsx62
-rw-r--r--packages/bank-ui/src/pages/WireTransfer.tsx10
-rw-r--r--packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx34
-rw-r--r--packages/bank-ui/src/pages/WithdrawalOperationPage.tsx2
-rw-r--r--packages/bank-ui/src/pages/account/CashoutListForAccount.tsx5
-rw-r--r--packages/bank-ui/src/pages/account/ShowAccountDetails.tsx10
-rw-r--r--packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx17
-rw-r--r--packages/bank-ui/src/pages/admin/AccountForm.tsx162
-rw-r--r--packages/bank-ui/src/pages/admin/AccountList.tsx13
-rw-r--r--packages/bank-ui/src/pages/admin/AdminHome.tsx41
-rw-r--r--packages/bank-ui/src/pages/admin/CreateNewAccount.tsx11
-rw-r--r--packages/bank-ui/src/pages/admin/DownloadStats.tsx11
-rw-r--r--packages/bank-ui/src/pages/admin/RemoveAccount.tsx4
-rw-r--r--packages/bank-ui/src/pages/regional/ConversionConfig.tsx1464
-rw-r--r--packages/bank-ui/src/pages/regional/CreateCashout.tsx104
-rw-r--r--packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx34
-rw-r--r--packages/bank-ui/src/route.ts43
-rw-r--r--packages/bank-ui/src/stories.test.ts7
-rw-r--r--packages/bank-ui/src/utils.ts82
47 files changed, 2133 insertions, 1419 deletions
diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx
index 75f070e4b..fbf5aa9ec 100644
--- a/packages/bank-ui/src/Routing.tsx
+++ b/packages/bank-ui/src/Routing.tsx
@@ -22,6 +22,7 @@ import {
import { Fragment, VNode, h } from "preact";
import {
+ AbsoluteTime,
AccessToken,
HttpStatusCode,
TranslatedString,
@@ -30,13 +31,13 @@ import {
import { useEffect } from "preact/hooks";
import { useBankCoreApiContext } from "./context/config.js";
import { useNavigationContext } from "./context/navigation.js";
-import { useSettingsContext } from "./context/settings.js";
import { useSessionState } from "./hooks/session.js";
import { AccountPage } from "./pages/AccountPage/index.js";
import { BankFrame } from "./pages/BankFrame.js";
import { LoginForm } from "./pages/LoginForm.js";
import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js";
import { RegistrationPage } from "./pages/RegistrationPage.js";
+import { ShowNotifications } from "./pages/ShowNotifications.js";
import { SolveChallengePage } from "./pages/SolveChallengePage.js";
import { WireTransfer } from "./pages/WireTransfer.js";
import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js";
@@ -58,7 +59,10 @@ export function Routing(): VNode {
if (session.state.status === "loggedIn") {
const { isUserAdministrator, username } = session.state;
return (
- <BankFrame account={username} routeAccountDetails={privatePages.myAccountDetails}>
+ <BankFrame
+ account={username}
+ routeAccountDetails={privatePages.myAccountDetails}
+ >
<PrivateRouting username={username} isAdmin={isUserAdministrator} />
</BankFrame>
);
@@ -90,7 +94,6 @@ function PublicRounting({
}: {
onLoggedUser: (username: string, token: AccessToken) => void;
}): VNode {
- const settings = useSettingsContext();
const { i18n } = useTranslationContext();
const location = useCurrentLocation(publicPages);
const { navigateTo } = useNavigationContext();
@@ -109,12 +112,11 @@ function PublicRounting({
async function doAutomaticLogin(username: string, password: string) {
await handleError(async () => {
- const resp = await authenticator(username)
- .createAccessToken(password, {
- scope: "readwrite",
- duration: { d_us: "forever" },
- refreshable: true,
- });
+ const resp = await authenticator(username).createAccessToken(password, {
+ scope: "readwrite",
+ duration: { d_us: "forever" },
+ refreshable: true,
+ });
if (resp.type === "ok") {
onLoggedUser(username, resp.body.access_token);
} else {
@@ -125,6 +127,7 @@ function PublicRounting({
title: i18n.str`Wrong credentials for "${username}"`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -132,6 +135,7 @@ function PublicRounting({
title: i18n.str`Account not found`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
default:
assertUnreachable(resp);
@@ -198,14 +202,12 @@ export const privatePages = {
() => "#/account/charge-wallet",
),
homeWireTransfer: urlPattern<{
- account?: string,
- subject?: string,
- amount?: string,
- }>(
- /\/account\/wire-transfer/,
- () => "#/account/wire-transfer",
- ),
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>(/\/account\/wire-transfer/, () => "#/account/wire-transfer"),
home: urlPattern(/\/account/, () => "#/account"),
+ notifications: urlPattern(/\/notifications/, () => "#/notifications"),
solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
cashoutCreate: urlPattern(/\/new-cashout/, () => "#/new-cashout"),
cashoutDetails: urlPattern<{ cid: string }>(
@@ -213,9 +215,9 @@ export const privatePages = {
({ cid }) => `#/cashout/${cid}`,
),
wireTranserCreate: urlPattern<{
- account?: string,
- subject?: string,
- amount?: string,
+ account?: string;
+ subject?: string;
+ amount?: string;
}>(
/\/wire-transfer\/(?<account>[a-zA-Z0-9]+)/,
({ account }) => `#/wire-transfer/${account}`,
@@ -278,7 +280,6 @@ function PrivateRouting({
switch (location.name) {
case "operationDetails": {
-
return (
<WithdrawalOperationPage
operationId={location.values.wopid}
@@ -293,7 +294,6 @@ function PrivateRouting({
);
}
case "startOperation": {
-
return (
<WithdrawalOperationPage
operationId={location.values.wopid}
@@ -563,17 +563,19 @@ function PrivateRouting({
);
}
case "conversionConfig": {
- return <ConversionConfig
- routeMyAccountCashout={privatePages.myAccountCashouts}
- routeMyAccountDelete={privatePages.myAccountDelete}
- routeMyAccountDetails={privatePages.myAccountDetails}
- routeMyAccountPassword={privatePages.myAccountPassword}
- routeConversionConfig={privatePages.conversionConfig}
- routeCancel={privatePages.home}
- onUpdateSuccess={() => {
- navigateTo(privatePages.home.url({}))
- }}
- />;
+ return (
+ <ConversionConfig
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ routeCancel={privatePages.home}
+ onUpdateSuccess={() => {
+ navigateTo(privatePages.home.url({}));
+ }}
+ />
+ );
}
case "homeWireTransfer": {
return (
@@ -598,6 +600,9 @@ function PrivateRouting({
/>
);
}
+ case "notifications": {
+ return <ShowNotifications />;
+ }
default:
assertUnreachable(location);
}
diff --git a/packages/bank-ui/src/app.tsx b/packages/bank-ui/src/app.tsx
index 3a7fafccf..893942059 100644
--- a/packages/bank-ui/src/app.tsx
+++ b/packages/bank-ui/src/app.tsx
@@ -88,7 +88,7 @@ export function App() {
</TranslationProvider>
</SettingsProvider>
);
-};
+}
// @ts-expect-error creating a new property for window object
window.setGlobalLogLevelFromString = setGlobalLogLevelFromString;
diff --git a/packages/bank-ui/src/components/Cashouts/views.tsx b/packages/bank-ui/src/components/Cashouts/views.tsx
index 7f16d5840..22b8d8c1b 100644
--- a/packages/bank-ui/src/components/Cashouts/views.tsx
+++ b/packages/bank-ui/src/components/Cashouts/views.tsx
@@ -17,7 +17,6 @@
import {
AbsoluteTime,
Amounts,
- Duration,
HttpStatusCode,
TalerError,
assertUnreachable,
@@ -32,19 +31,19 @@ import { Fragment, VNode, h } from "preact";
import { useConversionInfo } from "../../hooks/regional.js";
import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js";
-import { State } from "./index.js";
import { Time } from "../Time.js";
+import { State } from "./index.js";
export function FailedView({ error }: State.Failed) {
const { i18n } = useTranslationContext();
switch (error.case) {
case HttpStatusCode.NotImplemented: {
return (
- <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 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>
);
}
@@ -69,11 +68,11 @@ export function ReadyView({
switch (resp.case) {
case HttpStatusCode.NotImplemented: {
return (
- <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 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>
);
}
@@ -89,8 +88,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] = [];
}
@@ -156,9 +155,12 @@ export function ReadyView({
>
<td class="relative py-2 pl-2 pr-2 text-sm ">
<div class="font-medium text-gray-900">
- <Time format="HH:mm:ss"
- timestamp={AbsoluteTime.fromProtocolTimestamp(item.creation_time)}
- // relative={Duration.fromSpec({ days: 1 })}
+ <Time
+ format="HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ item.creation_time,
+ )}
+ // relative={Duration.fromSpec({ days: 1 })}
/>
</div>
{
@@ -200,7 +202,6 @@ export function ReadyView({
</td>
<td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
-
{item.subject}
</td>
</a>
diff --git a/packages/bank-ui/src/components/Time.tsx b/packages/bank-ui/src/components/Time.tsx
index 39ce33f60..5c8afe212 100644
--- a/packages/bank-ui/src/components/Time.tsx
+++ b/packages/bank-ui/src/components/Time.tsx
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2022 Taler Systems S.A.
+ (C) 2022-2024 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
@@ -16,16 +16,21 @@
import { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { formatISO, format, formatDuration, intervalToDuration } from "date-fns";
+import {
+ formatISO,
+ format,
+ formatDuration,
+ intervalToDuration,
+} from "date-fns";
import { Fragment, h, VNode } from "preact";
/**
- *
+ *
* @param timestamp time to be formatted
* @param relative duration threshold, if the difference is lower
* the timestamp will be formatted as relative time from "now"
- *
- * @returns
+ *
+ * @returns
*/
export function Time({
timestamp,
@@ -33,34 +38,38 @@ export function Time({
format: formatString,
}: {
timestamp: AbsoluteTime | undefined;
- relative?: Duration,
+ relative?: Duration;
format: string;
}): VNode {
- const { i18n, dateLocale } = useTranslationContext()
- if (!timestamp) return <Fragment />
+ const { i18n, dateLocale } = useTranslationContext();
+ if (!timestamp) return <Fragment />;
if (timestamp.t_ms === "never") {
- return <time >{i18n.str`never`}</time>
+ return <time>{i18n.str`never`}</time>;
}
const now = AbsoluteTime.now();
- const diff = AbsoluteTime.difference(now, timestamp)
+ const diff = AbsoluteTime.difference(now, timestamp);
if (relative && now.t_ms !== "never" && Duration.cmp(diff, relative) === -1) {
const d = intervalToDuration({
start: now.t_ms,
- end: timestamp.t_ms
- })
- d.seconds = 0
- const duration = formatDuration(d, { locale: dateLocale })
- const isFuture = AbsoluteTime.cmp(now, timestamp) < 0
+ end: timestamp.t_ms,
+ });
+ d.seconds = 0;
+ const duration = formatDuration(d, { locale: dateLocale });
+ const isFuture = AbsoluteTime.cmp(now, timestamp) < 0;
if (isFuture) {
- return <time dateTime={formatISO(timestamp.t_ms)}>
- <i18n.Translate>in {duration}</i18n.Translate>
- </time>
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ <i18n.Translate>in {duration}</i18n.Translate>
+ </time>
+ );
} else {
- return <time dateTime={formatISO(timestamp.t_ms)}>
- <i18n.Translate>{duration} ago</i18n.Translate>
- </time>
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ <i18n.Translate>{duration} ago</i18n.Translate>
+ </time>
+ );
}
}
return (
diff --git a/packages/bank-ui/src/components/Transactions/index.ts b/packages/bank-ui/src/components/Transactions/index.ts
index c8bb1e108..4cad6f306 100644
--- a/packages/bank-ui/src/components/Transactions/index.ts
+++ b/packages/bank-ui/src/components/Transactions/index.ts
@@ -23,11 +23,13 @@ import { RouteDefinition } from "../../route.js";
export interface Props {
account: string;
- routeCreateWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
- }> | undefined;
+ routeCreateWireTransfer:
+ | RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>
+ | undefined;
}
export type State = State.Loading | State.LoadingUriError | State.Ready;
@@ -49,11 +51,13 @@ export namespace State {
export interface Ready extends BaseInfo {
status: "ready";
error: undefined;
- routeCreateWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
- }> | undefined;
+ routeCreateWireTransfer:
+ | RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>
+ | undefined;
transactions: Transaction[];
onGoStart?: () => void;
onGoNext?: () => void;
diff --git a/packages/bank-ui/src/components/Transactions/state.ts b/packages/bank-ui/src/components/Transactions/state.ts
index 3e9103b59..e792ddfa0 100644
--- a/packages/bank-ui/src/components/Transactions/state.ts
+++ b/packages/bank-ui/src/components/Transactions/state.ts
@@ -23,7 +23,10 @@ import {
import { useTransactions } from "../../hooks/account.js";
import { Props, State, Transaction } from "./index.js";
-export function useComponentState({ account, routeCreateWireTransfer }: Props): State {
+export function useComponentState({
+ account,
+ routeCreateWireTransfer,
+}: Props): State {
const txResult = useTransactions(account);
if (!txResult) {
return {
@@ -38,36 +41,35 @@ export function useComponentState({ account, routeCreateWireTransfer }: Props):
};
}
- const transactions =
- txResult.result
- .map((tx) => {
- const negative = tx.direction === "debit";
- const cp = parsePaytoUri(
- negative ? tx.creditor_payto_uri : tx.debtor_payto_uri,
- );
- const counterpart =
- (cp === undefined || !cp.isKnown
- ? undefined
- : cp.targetType === "iban"
- ? cp.iban
- : cp.targetType === "x-taler-bank"
- ? cp.account
- : cp.targetType === "bitcoin"
- ? `${cp.targetPath.substring(0, 6)}...`
- : undefined) ?? "unknown";
+ const transactions = txResult.result
+ .map((tx) => {
+ const negative = tx.direction === "debit";
+ const cp = parsePaytoUri(
+ negative ? tx.creditor_payto_uri : tx.debtor_payto_uri,
+ );
+ const counterpart =
+ (cp === undefined || !cp.isKnown
+ ? undefined
+ : cp.targetType === "iban"
+ ? cp.iban
+ : cp.targetType === "x-taler-bank"
+ ? cp.account
+ : cp.targetType === "bitcoin"
+ ? `${cp.targetPath.substring(0, 6)}...`
+ : undefined) ?? "unknown";
- const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
- const amount = Amounts.parse(tx.amount);
- const subject = tx.subject;
- return {
- negative,
- counterpart,
- when,
- amount,
- subject,
- };
- })
- .filter((x): x is Transaction => x !== undefined);
+ const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
+ const amount = Amounts.parse(tx.amount);
+ const subject = tx.subject;
+ return {
+ negative,
+ counterpart,
+ when,
+ amount,
+ subject,
+ };
+ })
+ .filter((x): x is Transaction => x !== undefined);
return {
status: "ready",
diff --git a/packages/bank-ui/src/components/Transactions/views.tsx b/packages/bank-ui/src/components/Transactions/views.tsx
index 7da9fc5a9..417b34c71 100644
--- a/packages/bank-ui/src/components/Transactions/views.tsx
+++ b/packages/bank-ui/src/components/Transactions/views.tsx
@@ -19,9 +19,8 @@ import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useBankCoreApiContext } from "../../context/config.js";
import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
-import { State } from "./index.js";
-import { Duration } from "@gnu-taler/taler-util";
import { Time } from "../Time.js";
+import { State } from "./index.js";
export function ReadyView({
transactions,
@@ -30,24 +29,26 @@ export function ReadyView({
onGoStart,
}: State.Ready): VNode {
const { i18n, dateLocale } = useTranslationContext();
- const { config } = useBankCoreApiContext()
+ const { config } = useBankCoreApiContext();
if (!transactions.length) {
- return <div class="px-4 mt-4">
- <div class="sm:flex sm:items-center">
- <div class="sm:flex-auto">
- <h1 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>Transactions history</i18n.Translate>
- </h1>
+ return (
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Transactions history</i18n.Translate>
+ </h1>
+ </div>
</div>
- </div>
- <Attention type="low" title={i18n.str`No transactions yet.`}>
- <i18n.Translate>
- You can start sending a wire transfer or withdrawing to your wallet.
- </i18n.Translate>
- </Attention>
- </div>;
+ <Attention type="low" title={i18n.str`No transactions yet.`}>
+ <i18n.Translate>
+ You can start sending a wire transfer or withdrawing to your wallet.
+ </i18n.Translate>
+ </Attention>
+ </div>
+ );
}
const txByDate = transactions.reduce(
@@ -116,9 +117,10 @@ export function ReadyView({
>
<td class="relative py-2 pl-2 pr-2 text-sm ">
<div class="font-medium text-gray-900">
- <Time format="HH:mm:ss"
+ <Time
+ format="HH:mm:ss"
timestamp={item.when}
- // relative={Duration.fromSpec({ days: 1 })}
+ // relative={Duration.fromSpec({ days: 1 })}
/>
</div>
<dl class="font-normal sm:hidden">
@@ -153,7 +155,9 @@ export function ReadyView({
</dt>
<dd class="mt-1 truncate text-gray-500 sm:hidden">
{item.negative ? i18n.str`to` : i18n.str`from`}{" "}
- {!routeCreateWireTransfer ? item.counterpart :
+ {!routeCreateWireTransfer ? (
+ item.counterpart
+ ) : (
<a
name={`transfer to ${item.counterpart}`}
href={routeCreateWireTransfer.url({
@@ -163,7 +167,7 @@ export function ReadyView({
>
{item.counterpart}
</a>
- }
+ )}
</dd>
<dd class="mt-1 text-gray-500 sm:hidden">
<pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
@@ -190,7 +194,9 @@ export function ReadyView({
)}
</td>
<td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">
- {!routeCreateWireTransfer ? item.counterpart :
+ {!routeCreateWireTransfer ? (
+ item.counterpart
+ ) : (
<a
name={`wire transfer to ${item.counterpart}`}
href={routeCreateWireTransfer.url({
@@ -200,7 +206,7 @@ export function ReadyView({
>
{item.counterpart}
</a>
- }
+ )}
</td>
<td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
{item.subject}
diff --git a/packages/bank-ui/src/context/config.ts b/packages/bank-ui/src/context/config.ts
index cb0d599aa..f8be80a6c 100644
--- a/packages/bank-ui/src/context/config.ts
+++ b/packages/bank-ui/src/context/config.ts
@@ -26,11 +26,12 @@ import {
TalerError,
assertUnreachable,
CacheEvictor,
+ ObservabilityEvent,
} from "@gnu-taler/taler-util";
import {
BrowserFetchHttpLib,
ErrorLoading,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import {
ComponentChildren,
@@ -63,6 +64,8 @@ export type Type = {
conversion: TalerBankConversionHttpClient;
authenticator: (user: string) => TalerAuthenticationHttpClient;
hints: VersionHint[];
+ onBackendActivity: (fn: Listener) => Unsuscriber;
+ cancelRequest: (eventId: string) => void;
};
// FIXME: below
@@ -78,6 +81,25 @@ export enum VersionHint {
CASHOUT_BEFORE_2FA,
}
+const observers = new Array<(e: ObservabilityEvent) => void>();
+type Listener = (e: ObservabilityEvent) => void;
+type Unsuscriber = () => void;
+
+const activity = Object.freeze({
+ notify: (data: ObservabilityEvent) =>
+ observers.forEach((observer) => observer(data)),
+ subscribe: (func: Listener): Unsuscriber => {
+ observers.push(func);
+ return () => {
+ observers.forEach((observer, index) => {
+ if (observer === func) {
+ observers.splice(index, 1);
+ }
+ });
+ };
+ },
+});
+
export type ConfigResult =
| undefined
| { type: "ok"; config: TalerCorebankApi.Config; hints: VersionHint[] }
@@ -96,7 +118,8 @@ export const BankCoreApiProvider = ({
const [checked, setChecked] = useState<ConfigResult>();
const { i18n } = useTranslationContext();
- const { bankClient, conversionClient, authClient } = buildApiClient(new URL(baseUrl))
+ const { bankClient, conversionClient, authClient, cancelRequest } =
+ buildApiClient(new URL(baseUrl));
useEffect(() => {
bankClient
@@ -150,8 +173,10 @@ export const BankCoreApiProvider = ({
url: new URL(bankClient.baseUrl),
config: checked.config,
bank: bankClient,
+ onBackendActivity: activity.subscribe,
conversion: conversionClient,
authenticator: authClient,
+ cancelRequest,
hints: checked.hints,
};
return h(Context.Provider, {
@@ -162,8 +187,8 @@ export const BankCoreApiProvider = ({
/**
* build http client with cache breaker due to SWR
- * @param url
- * @returns
+ * @param url
+ * @returns
*/
function buildApiClient(url: URL) {
const httpFetch = new BrowserFetchHttpLib({
@@ -172,15 +197,32 @@ function buildApiClient(url: URL) {
});
const httpLib = new ObservableHttpClientLibrary(httpFetch, {
observe(ev) {
- console.log(ev)
- }
- })
+ activity.notify(ev);
+ },
+ });
- const bankClient = new TalerCoreBankHttpClient(url.href, httpLib, evictBankSwrCache);
- const conversionClient = new TalerBankConversionHttpClient(bankClient.getConversionInfoAPI().href, httpLib, evictConversionSwrCache);
- const authClient = (user: string) => new TalerAuthenticationHttpClient(bankClient.getAuthenticationAPI(user).href, user, httpLib);
+ function cancelRequest(id: string) {
+ httpLib.cancelRequest(id);
+ }
- return { bankClient, conversionClient, authClient }
+ const bankClient = new TalerCoreBankHttpClient(
+ url.href,
+ httpLib,
+ evictBankSwrCache,
+ );
+ const conversionClient = new TalerBankConversionHttpClient(
+ bankClient.getConversionInfoAPI().href,
+ httpLib,
+ evictConversionSwrCache,
+ );
+ const authClient = (user: string) =>
+ new TalerAuthenticationHttpClient(
+ bankClient.getAuthenticationAPI(user).href,
+ user,
+ httpLib,
+ );
+
+ return { bankClient, conversionClient, authClient, cancelRequest };
}
export const BankCoreApiProviderTesting = ({
@@ -206,7 +248,6 @@ export const BankCoreApiProviderTesting = ({
});
};
-
const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
async notifySuccess(op) {
switch (op) {
@@ -215,7 +256,7 @@ const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
revalidatePublicAccounts(),
revalidateBusinessAccounts(),
]);
- return
+ return;
}
case TalerCoreBankCacheEviction.CREATE_ACCOUNT: {
// admin balance change on new account
@@ -224,27 +265,25 @@ const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
revalidateTransactions(),
revalidatePublicAccounts(),
revalidateBusinessAccounts(),
- ])
+ ]);
return;
}
case TalerCoreBankCacheEviction.UPDATE_ACCOUNT: {
- await Promise.all([
- revalidateAccountDetails(),
- ])
+ await Promise.all([revalidateAccountDetails()]);
return;
}
case TalerCoreBankCacheEviction.CREATE_TRANSACTION: {
await Promise.all([
revalidateAccountDetails(),
revalidateTransactions(),
- ])
+ ]);
return;
}
case TalerCoreBankCacheEviction.CONFIRM_WITHDRAWAL: {
await Promise.all([
revalidateAccountDetails(),
revalidateTransactions(),
- ])
+ ]);
return;
}
case TalerCoreBankCacheEviction.CREATE_CASHOUT: {
@@ -252,7 +291,7 @@ const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
revalidateAccountDetails(),
revalidateCashouts(),
revalidateTransactions(),
- ])
+ ]);
return;
}
case TalerCoreBankCacheEviction.UPDATE_PASSWORD:
@@ -260,20 +299,21 @@ const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
case TalerCoreBankCacheEviction.CREATE_WITHDRAWAL:
return;
default:
- assertUnreachable(op)
+ assertUnreachable(op);
}
- }
-}
+ },
+};
-const evictConversionSwrCache: CacheEvictor<TalerBankConversionCacheEviction> = {
- async notifySuccess(op) {
- switch (op) {
- case TalerBankConversionCacheEviction.UPDATE_RATE: {
- await revalidateConversionInfo();
- return
+const evictConversionSwrCache: CacheEvictor<TalerBankConversionCacheEviction> =
+ {
+ async notifySuccess(op) {
+ switch (op) {
+ case TalerBankConversionCacheEviction.UPDATE_RATE: {
+ await revalidateConversionInfo();
+ return;
+ }
+ default:
+ assertUnreachable(op);
}
- default:
- assertUnreachable(op)
- }
- }
-} \ No newline at end of file
+ },
+ };
diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts
index aa0745253..5fe12573c 100644
--- a/packages/bank-ui/src/hooks/account.ts
+++ b/packages/bank-ui/src/hooks/account.ts
@@ -62,7 +62,11 @@ export function useAccountDetails(account: string) {
}
export function revalidateWithdrawalDetails() {
- return mutate((key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById", undefined, { revalidate: true });
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById",
+ undefined,
+ { revalidate: true },
+ );
}
export function useWithdrawalDetails(wid: string) {
@@ -110,7 +114,9 @@ export function useWithdrawalDetails(wid: string) {
export function revalidateTransactionDetails() {
return mutate(
- (key) => Array.isArray(key) && key[key.length - 1] === "getTransactionById", undefined, { revalidate: true }
+ (key) => Array.isArray(key) && key[key.length - 1] === "getTransactionById",
+ undefined,
+ { revalidate: true },
);
}
export function useTransactionDetails(account: string, tid: number) {
@@ -149,7 +155,9 @@ export function useTransactionDetails(account: string, tid: number) {
export async function revalidatePublicAccounts() {
return mutate(
- (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts", undefined, { revalidate: true }
+ (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts",
+ undefined,
+ { revalidate: true },
);
}
export function usePublicAccounts(
@@ -193,9 +201,10 @@ export function usePublicAccounts(
data && data.type === "ok" && data.body.public_accounts.length <= PAGE_SIZE;
const isFirstPage = !offset;
- const result = data && data.type == "ok" ? structuredClone(data.body.public_accounts) : []
- if (result.length == PAGE_SIZE+1) {
- result.pop()
+ const result =
+ data && data.type == "ok" ? structuredClone(data.body.public_accounts) : [];
+ if (result.length == PAGE_SIZE + 1) {
+ result.pop();
}
const pagination = {
result,
@@ -243,7 +252,7 @@ export function useTransactions(account: string, initial?: number) {
return await api.getTransactions(
{ username, token },
{
- limit: PAGE_SIZE +1 ,
+ limit: PAGE_SIZE + 1,
offset: txid ? String(txid) : undefined,
order: "dec",
},
@@ -267,9 +276,10 @@ export function useTransactions(account: string, initial?: number) {
data && data.type === "ok" && data.body.transactions.length <= PAGE_SIZE;
const isFirstPage = !offset;
- const result = data && data.type == "ok" ? structuredClone(data.body.transactions) : []
- if (result.length == PAGE_SIZE+1) {
- result.pop()
+ const result =
+ data && data.type == "ok" ? structuredClone(data.body.transactions) : [];
+ if (result.length == PAGE_SIZE + 1) {
+ result.pop();
}
const pagination = {
result,
diff --git a/packages/bank-ui/src/hooks/bank-state.ts b/packages/bank-ui/src/hooks/bank-state.ts
index 83bb009cf..1d8c4f9e6 100644
--- a/packages/bank-ui/src/hooks/bank-state.ts
+++ b/packages/bank-ui/src/hooks/bank-state.ts
@@ -118,7 +118,7 @@ const codecForChallengeConfirmWithdrawal =
.property("request", codecForString())
.build("ConfirmWithdrawalChallenge");
-const codecForAppLocation = codecForString as () => Codec<AppLocation>
+const codecForAppLocation = codecForString as () => Codec<AppLocation>;
const codecForChallengeCashout = (): Codec<CashoutChallenge> =>
buildCodecForObject<CashoutChallenge>()
@@ -141,8 +141,6 @@ const codecForChallenge = (): Codec<ChallengeInProgess> =>
.alternative("update-password", codecForChallengeUpdatePassword())
.build("ChallengeInProgess");
-
-
interface BankState {
currentWithdrawalOperationId: string | undefined;
currentChallenge: ChallengeInProgess | undefined;
@@ -163,10 +161,10 @@ const BANK_STATE_KEY = buildStorageKey("bank-app-state", codecForBankState());
/**
* Client state saved in local storage.
- *
+ *
* This information is saved in the client because
* the backend server session API is not enough.
- *
+ *
* @returns tuple of [state, update(), reset()]
*/
export function useBankState(): [
@@ -185,4 +183,3 @@ export function useBankState(): [
}
return [value, updateField, reset];
}
-
diff --git a/packages/bank-ui/src/hooks/form.ts b/packages/bank-ui/src/hooks/form.ts
index 26354b108..afa4912eb 100644
--- a/packages/bank-ui/src/hooks/form.ts
+++ b/packages/bank-ui/src/hooks/form.ts
@@ -14,87 +14,102 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AmountJson, TalerBankConversionApi, TranslatedString } from "@gnu-taler/taler-util";
+import { AmountJson, TranslatedString } from "@gnu-taler/taler-util";
import { useState } from "preact/hooks";
export type UIField = {
value: string | undefined;
onUpdate: (s: string) => void;
error: TranslatedString | undefined;
-}
+};
type FormHandler<T> = {
- [k in keyof T]?:
- T[k] extends string ? UIField :
- T[k] extends AmountJson ? UIField :
- FormHandler<T[k]>;
-}
+ [k in keyof T]?: T[k] extends string
+ ? UIField
+ : T[k] extends AmountJson
+ ? UIField
+ : FormHandler<T[k]>;
+};
export type FormValues<T> = {
- [k in keyof T]:
- T[k] extends string ? (string | undefined) :
- T[k] extends AmountJson ? (string | undefined) :
- FormValues<T[k]>;
-}
+ [k in keyof T]: T[k] extends string
+ ? string | undefined
+ : T[k] extends AmountJson
+ ? string | undefined
+ : FormValues<T[k]>;
+};
export type RecursivePartial<T> = {
- [k in keyof T]?:
- T[k] extends string ? (string) :
- T[k] extends AmountJson ? (AmountJson) :
- RecursivePartial<T[k]>;
-}
+ [k in keyof T]?: T[k] extends string
+ ? string
+ : T[k] extends AmountJson
+ ? AmountJson
+ : RecursivePartial<T[k]>;
+};
export type FormErrors<T> = {
- [k in keyof T]?:
- T[k] extends string ? (TranslatedString) :
- T[k] extends AmountJson ? (TranslatedString) :
- FormErrors<T[k]>;
-}
-
-export type FormStatus<T> = {
- status: "ok",
- result: T,
- errors: undefined,
-} | {
- status: "fail",
- result: RecursivePartial<T>,
- errors: FormErrors<T>,
-}
-
-
-function constructFormHandler<T>(form: FormValues<T>, updateForm: (d: FormValues<T>) => void, errors: FormErrors<T> | undefined): FormHandler<T> {
- const keys = (Object.keys(form) as Array<keyof T>)
+ [k in keyof T]?: T[k] extends string
+ ? TranslatedString
+ : T[k] extends AmountJson
+ ? TranslatedString
+ : FormErrors<T[k]>;
+};
+
+export type FormStatus<T> =
+ | {
+ status: "ok";
+ result: T;
+ errors: undefined;
+ }
+ | {
+ status: "fail";
+ result: RecursivePartial<T>;
+ errors: FormErrors<T>;
+ };
+
+function constructFormHandler<T>(
+ form: FormValues<T>,
+ updateForm: (d: FormValues<T>) => void,
+ errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+ const keys = Object.keys(form) as Array<keyof T>;
const handler = keys.reduce((prev, fieldName) => {
- const currentValue: any = form[fieldName];
- const currentError: any = errors ? errors[fieldName] : undefined;
- function updater(newValue: any) {
- updateForm({ ...form, [fieldName]: newValue })
+ const currentValue: unknown = form[fieldName];
+ const currentError: unknown = errors ? errors[fieldName] : undefined;
+ function updater(newValue: unknown) {
+ updateForm({ ...form, [fieldName]: newValue });
}
if (typeof currentValue === "object") {
- const group = constructFormHandler(currentValue, updater, currentError)
- // @ts-expect-error asdasd
- prev[fieldName] = group
+ // @ts-expect-error FIXME better typing
+ const group = constructFormHandler(currentValue, updater, currentError);
+ // @ts-expect-error FIXME better typing
+ prev[fieldName] = group;
return prev;
}
const field: UIField = {
+ // @ts-expect-error FIXME better typing
error: currentError,
+ // @ts-expect-error FIXME better typing
value: currentValue,
- onUpdate: updater
- }
- // @ts-expect-error asdasd
- prev[fieldName] = field
- return prev
- }, {} as FormHandler<T>)
+ onUpdate: updater,
+ };
+ // @ts-expect-error FIXME better typing
+ prev[fieldName] = field;
+ return prev;
+ }, {} as FormHandler<T>);
return handler;
}
-export function useFormState<T>(defaultValue: FormValues<T>, check: (f: FormValues<T>) => FormStatus<T>): [FormHandler<T>, FormStatus<T>] {
- const [form, updateForm] = useState<FormValues<T>>(defaultValue)
+export function useFormState<T>(
+ defaultValue: FormValues<T>,
+ check: (f: FormValues<T>) => FormStatus<T>,
+): [FormHandler<T>, FormStatus<T>] {
+ const [form, updateForm] = useState<FormValues<T>>(defaultValue);
- const status = check(form)
- const handler = constructFormHandler(form, updateForm, status.errors)
+ const status = check(form);
+ const handler = constructFormHandler(form, updateForm, status.errors);
- return [handler, status]
-} \ No newline at end of file
+ return [handler, status];
+}
diff --git a/packages/bank-ui/src/hooks/preferences.ts b/packages/bank-ui/src/hooks/preferences.ts
index 454dc8d80..bb3dcb153 100644
--- a/packages/bank-ui/src/hooks/preferences.ts
+++ b/packages/bank-ui/src/hooks/preferences.ts
@@ -61,7 +61,7 @@ const BANK_PREFERENCES_KEY = buildStorageKey(
);
/**
* User preferences.
- *
+ *
* @returns tuple of [state, update()]
*/
export function usePreferences(): [
@@ -109,4 +109,3 @@ export function getLabelForPreferences(
return i18n.str`Show debug info`;
}
}
-
diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts
index bf948d293..51f3edad4 100644
--- a/packages/bank-ui/src/hooks/regional.ts
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -31,18 +31,20 @@ import {
TalerHttpError,
opFixedSuccess,
} from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
import _useSWR, { SWRHook, mutate } from "swr";
import { useBankCoreApiContext } from "../context/config.js";
-import { useState } from "preact/hooks";
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
const useSWR = _useSWR as unknown as SWRHook;
-export type TransferCalculation = {
- debit: AmountJson;
- credit: AmountJson;
- beforeFee: AmountJson;
-} | "amount-is-too-small";
+export type TransferCalculation =
+ | {
+ debit: AmountJson;
+ credit: AmountJson;
+ beforeFee: AmountJson;
+ }
+ | "amount-is-too-small";
type EstimatorFunction = (
amount: AmountJson,
fee: AmountJson,
@@ -95,7 +97,7 @@ export function useCashinEstimator(): ConversionEstimators {
if (resp.type === "fail") {
switch (resp.case) {
case HttpStatusCode.Conflict: {
- return "amount-is-too-small"
+ return "amount-is-too-small";
}
// this below can't happen
case HttpStatusCode.NotImplemented: //it should not be able to call this function
@@ -120,7 +122,7 @@ export function useCashinEstimator(): ConversionEstimators {
if (resp.type === "fail") {
switch (resp.case) {
case HttpStatusCode.Conflict: {
- return "amount-is-too-small"
+ return "amount-is-too-small";
}
// this below can't happen
case HttpStatusCode.NotImplemented: //it should not be able to call this function
@@ -142,7 +144,7 @@ export function useCashinEstimator(): ConversionEstimators {
}
export function useCashoutEstimator(): ConversionEstimators {
- const { bank, conversion } = useBankCoreApiContext();
+ const { conversion } = useBankCoreApiContext();
return {
estimateByCredit: async (fiatAmount, fee) => {
const resp = await conversion.getCashoutRate({
@@ -151,7 +153,7 @@ export function useCashoutEstimator(): ConversionEstimators {
if (resp.type === "fail") {
switch (resp.case) {
case HttpStatusCode.Conflict: {
- return "amount-is-too-small"
+ return "amount-is-too-small";
}
// this below can't happen
case HttpStatusCode.NotImplemented: //it should not be able to call this function
@@ -176,7 +178,7 @@ export function useCashoutEstimator(): ConversionEstimators {
if (resp.type === "fail") {
switch (resp.case) {
case HttpStatusCode.Conflict: {
- return "amount-is-too-small"
+ return "amount-is-too-small";
}
// this below can't happen
case HttpStatusCode.NotImplemented: //it should not be able to call this function
@@ -201,11 +203,15 @@ export function useCashoutEstimator(): ConversionEstimators {
* @deprecated use useCashoutEstimator
*/
export function useEstimator(): ConversionEstimators {
- return useCashoutEstimator()
+ return useCashoutEstimator();
}
export async function revalidateBusinessAccounts() {
- return mutate((key) => Array.isArray(key) && key[key.length - 1] === "getAccounts", undefined, { revalidate: true });
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getAccounts",
+ undefined,
+ { revalidate: true },
+ );
}
export function useBusinessAccounts() {
const { state: credentials } = useSessionState();
@@ -247,9 +253,10 @@ export function useBusinessAccounts() {
data && data.type === "ok" && data.body.accounts.length <= PAGE_SIZE;
const isFirstPage = !offset;
- const result = data && data.type == "ok" ? structuredClone(data.body.accounts) : []
+ const result =
+ data && data.type == "ok" ? structuredClone(data.body.accounts) : [];
if (result.length == PAGE_SIZE + 1) {
- result.pop()
+ result.pop();
}
const pagination = {
result,
@@ -276,7 +283,9 @@ function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
export function revalidateOnePendingCashouts() {
return mutate(
(key) =>
- Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts", undefined, { revalidate: true }
+ Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts",
+ undefined,
+ { revalidate: true },
);
}
export function useOnePendingCashouts(account: string) {
@@ -290,7 +299,8 @@ export function useOnePendingCashouts(account: string) {
if (list.type !== "ok") {
return list;
}
- const pendingCashout = list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined;
+ const pendingCashout =
+ list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined;
if (!pendingCashout) return opFixedSuccess(undefined);
const cashoutInfo = await api.getCashoutById(
{ username, token },
@@ -334,7 +344,9 @@ export function useOnePendingCashouts(account: string) {
}
export function revalidateCashouts() {
- return 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 } = useSessionState();
@@ -357,7 +369,7 @@ export function useCashouts(account: string) {
}),
);
const cashouts = all.filter(notUndefined);
- return { type: "ok" as const, body: { cashouts }};
+ return { type: "ok" as const, body: { cashouts } };
}
const { data, error } = useSWR<
| OperationOk<{ cashouts: CashoutWithId[] }>
@@ -386,7 +398,9 @@ export function useCashouts(account: string) {
export function revalidateCashoutDetails() {
return mutate(
- (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById", undefined, { revalidate: true }
+ (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById",
+ undefined,
+ { revalidate: true },
);
}
export function useCashoutDetails(cashoutId: number | undefined) {
@@ -435,7 +449,9 @@ export type LastMonitor = {
};
export function revalidateLastMonitorInfo() {
return mutate(
- (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo", undefined, { revalidate: true }
+ (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo",
+ undefined,
+ { revalidate: true },
);
}
export function useLastMonitorInfo(
diff --git a/packages/bank-ui/src/pages/AccountPage/index.ts b/packages/bank-ui/src/pages/AccountPage/index.ts
index 7776fbaa3..757346c5c 100644
--- a/packages/bank-ui/src/pages/AccountPage/index.ts
+++ b/packages/bank-ui/src/pages/AccountPage/index.ts
@@ -88,14 +88,14 @@ export namespace State {
routeChargeWallet: RouteDefinition;
routePublicAccounts: RouteDefinition;
routeWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
+ account?: string;
+ subject?: string;
+ amount?: string;
}>;
routeCreateWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
+ account?: string;
+ subject?: string;
+ amount?: string;
}>;
routeOperationDetails: RouteDefinition<{ wopid: string }>;
routeSolveSecondFactor: RouteDefinition;
diff --git a/packages/bank-ui/src/pages/AccountPage/views.tsx b/packages/bank-ui/src/pages/AccountPage/views.tsx
index 7ad00cf1d..3a182ed1b 100644
--- a/packages/bank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/bank-ui/src/pages/AccountPage/views.tsx
@@ -32,7 +32,9 @@ export function InvalidIbanView({ error }: State.InvalidIban) {
const IS_PUBLIC_ACCOUNT_ENABLED = false;
-function ShowDemoInfo({ routePublicAccounts }: {
+function ShowDemoInfo({
+ routePublicAccounts,
+}: {
routePublicAccounts: RouteDefinition;
}): VNode {
const { i18n } = useTranslationContext();
@@ -50,7 +52,10 @@ function ShowDemoInfo({ routePublicAccounts }: {
This part of the demo shows how a bank that supports Taler directly
would work. In addition to using your own bank account, you can also
see the transaction history of some{" "}
- <a name="public account" href={routePublicAccounts.url({})}>Public Accounts</a>.
+ <a name="public account" href={routePublicAccounts.url({})}>
+ Public Accounts
+ </a>
+ .
</i18n.Translate>
) : (
<i18n.Translate>
@@ -62,7 +67,9 @@ function ShowDemoInfo({ routePublicAccounts }: {
);
}
-function ShowPedingOperation({ routeSolveSecondFactor }: {
+function ShowPedingOperation({
+ routeSolveSecondFactor,
+}: {
routeSolveSecondFactor: RouteDefinition;
}): VNode {
const { i18n } = useTranslationContext();
@@ -140,7 +147,10 @@ export function ReadyView({
onOperationCreated={onOperationCreated}
onAuthorizationRequired={onAuthorizationRequired}
/>
- <Transactions account={account} routeCreateWireTransfer={routeCreateWireTransfer} />
+ <Transactions
+ account={account}
+ routeCreateWireTransfer={routeCreateWireTransfer}
+ />
</Fragment>
);
}
diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx
index 427e9a156..39f042455 100644
--- a/packages/bank-ui/src/pages/BankFrame.tsx
+++ b/packages/bank-ui/src/pages/BankFrame.tsx
@@ -14,7 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ Amounts,
+ ObservabilityEventType,
+ TalerError,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
import {
Footer,
Header,
@@ -22,22 +29,23 @@ import {
ToastBanner,
notifyError,
notifyException,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { ComponentChildren, VNode, h } from "preact";
-import { useEffect, useErrorBoundary } from "preact/hooks";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
import { useBankCoreApiContext } from "../context/config.js";
import { useSettingsContext } from "../context/settings.js";
import { useAccountDetails } from "../hooks/account.js";
-import { useSessionState } from "../hooks/session.js";
import { useBankState } from "../hooks/bank-state.js";
import {
getAllBooleanPreferences,
getLabelForPreferences,
usePreferences,
} from "../hooks/preferences.js";
+import { useSessionState } from "../hooks/session.js";
import { RouteDefinition } from "../route.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
+import { privatePages } from "../Routing.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -85,13 +93,18 @@ export function BankFrame({
title="Bank"
iconLinkURL={settings.iconLinkURL ?? "#"}
profileURL={routeAccountDetails?.url({})}
+ notificationURL={
+ preferences.showDebugInfo
+ ? privatePages.notifications.url({})
+ : undefined
+ }
onLogout={
session.state.status !== "loggedIn"
? undefined
: () => {
- session.logOut();
- resetBankState();
- }
+ session.logOut();
+ resetBankState();
+ }
}
sites={
!settings.topNavSites ? [] : Object.entries(settings.topNavSites)
@@ -102,11 +115,11 @@ export function BankFrame({
<div class="text-xs font-semibold leading-6 text-gray-400">
<i18n.Translate>Preferences</i18n.Translate>
</div>
- <ul role="list" class="space-y-1">
+ <ul role="list" class="space-y-4">
{getAllBooleanPreferences().map((set) => {
const isOn: boolean = !!preferences[set];
return (
- <li key={set} class="mt-2 pl-2">
+ <li key={set} class="pl-2">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
<span
@@ -144,19 +157,23 @@ export function BankFrame({
</Header>
</div>
- <div class="fixed z-20 w-full">
+ <div class="fixed z-20 top-14 w-full">
<div class="mx-auto w-4/5">
<ToastBanner />
+ {/* <Attention type="success" title={"hola" as TranslatedString} onClose={() => { }} /> */}
</div>
</div>
<main class="-mt-32 flex-1">
{account && routeAccountDetails && (
- <header class="py-5 bg-indigo-600 ">
+ <header class="py-6 bg-indigo-600">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<h1 class=" flex flex-wrap items-center justify-between sm:flex-nowrap">
<span class="text-2xl font-bold tracking-tight text-white">
- <WelcomeAccount account={account} routeAccountDetails={routeAccountDetails} />
+ <WelcomeAccount
+ account={account}
+ routeAccountDetails={routeAccountDetails}
+ />
</span>
<span class="text-2xl font-bold tracking-tight text-white">
<AccountBalance account={account} />
@@ -166,13 +183,15 @@ export function BankFrame({
</header>
)}
- <div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
+ <div class="mx-auto max-w-7xl px-4 pb-4 sm:px-6 lg:px-8">
<div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
{children}
</div>
</div>
</main>
+ <AppActivity />
+
<Footer
testingUrlKey="corebank-api-base-url"
GIT_HASH={GIT_HASH}
@@ -182,8 +201,117 @@ export function BankFrame({
);
}
-function WelcomeAccount({ account, routeAccountDetails }: {
- account: string,
+function Wait({ class: clazz }: { class?: string }): VNode {
+ return (
+ <Fragment>
+ <style>{`
+ .animated-loader {
+ display: inline-block;
+ --b: 5px;
+ border-radius: 50%;
+ aspect-ratio: 1;
+ padding: 1px;
+ background: conic-gradient(#0000 10%,#4f46e5) content-box;
+ -webkit-mask:
+ repeating-conic-gradient(#0000 0deg,#000 1deg 20deg,#0000 21deg 36deg),
+ radial-gradient(farthest-side,#0000 calc(100% - var(--b) - 1px),#000 calc(100% - var(--b)));
+ -webkit-mask-composite: destination-in;
+ mask-composite: intersect;
+ animation:spinning-loader 1s infinite steps(10);
+ }
+ @keyframes spinning-loader {to{transform: rotate(1turn)}}
+ `}</style>
+ <div class={`animated-loader ${clazz}`} />
+ </Fragment>
+ );
+}
+
+function AppActivity(): VNode {
+ const [lastEvent, setLastEvent] = useState<{
+ url: string;
+ id: string;
+ when: AbsoluteTime;
+ }>();
+ const [status, setStatus] = useState<"ok" | "fail">();
+ const d = useBankCoreApiContext();
+ const onBackendActivity = !d ? undefined : d.onBackendActivity;
+ const cancelRequest = !d ? undefined : d.cancelRequest;
+ const [pref] = usePreferences();
+ useEffect(() => {
+ // console.log("ASDASDS", onBackendActivity)
+ if (!pref.showDebugInfo) return;
+ if (!onBackendActivity) return;
+ return onBackendActivity((ev) => {
+ switch (ev.type) {
+ case ObservabilityEventType.HttpFetchStart: {
+ setLastEvent(ev);
+ setStatus(undefined);
+ return;
+ }
+ case ObservabilityEventType.HttpFetchFinishError: {
+ setStatus("fail");
+ return;
+ }
+ case ObservabilityEventType.HttpFetchFinishSuccess: {
+ setStatus("ok");
+ return;
+ }
+ /**
+ * all of this are ignored
+ */
+ case ObservabilityEventType.DbQueryStart:
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError:
+ case ObservabilityEventType.RequestStart:
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError:
+ case ObservabilityEventType.TaskStart:
+ case ObservabilityEventType.TaskStop:
+ case ObservabilityEventType.TaskReset:
+ case ObservabilityEventType.ShepherdTaskResult:
+ case ObservabilityEventType.DeclareTaskDependency:
+ case ObservabilityEventType.CryptoStart:
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError:
+ return;
+ default: {
+ assertUnreachable(ev);
+ }
+ }
+ });
+ });
+ if (!pref.showDebugInfo || !lastEvent) return <Fragment />;
+ return (
+ <div
+ data-status={status}
+ class="fixed z-20 bottom-0 w-full ease-in-out delay-1000 transition-transform data-[status=ok]:scale-y-0"
+ >
+ <div
+ data-status={status}
+ class="mx-auto w-4/5 center flex p-1 bg-gray-300 m-1 data-[status=fail]:bg-red-200 data-[status=ok]:bg-green-200 "
+ >
+ {!status ? <Wait class="w-6 h-6" /> : <div class="w-6 h-6" />}
+
+ <p class="ml-2 my-auto text-sm text-gray-500">{lastEvent.url}</p>
+ {!status ? (
+ <button
+ onClick={() => {
+ if (cancelRequest) cancelRequest(lastEvent.id);
+ }}
+ >
+ cancel
+ </button>
+ ) : undefined}
+ </div>
+ </div>
+ );
+}
+
+function WelcomeAccount({
+ account,
+ routeAccountDetails,
+}: {
+ account: string;
routeAccountDetails: RouteDefinition;
}): VNode {
const { i18n } = useTranslationContext();
@@ -196,7 +324,8 @@ function WelcomeAccount({ account, routeAccountDetails }: {
}
if (result.type === "fail") {
return (
- <a name="account details"
+ <a
+ name="account details"
href={routeAccountDetails.url({})}
class="underline underline-offset-2"
>
@@ -205,7 +334,8 @@ function WelcomeAccount({ account, routeAccountDetails }: {
);
}
return (
- <a name="account details"
+ <a
+ name="account details"
href={routeAccountDetails.url({})}
class="underline underline-offset-2"
>
diff --git a/packages/bank-ui/src/pages/LoginForm.tsx b/packages/bank-ui/src/pages/LoginForm.tsx
index bd20e79c8..a097417c3 100644
--- a/packages/bank-ui/src/pages/LoginForm.tsx
+++ b/packages/bank-ui/src/pages/LoginForm.tsx
@@ -14,15 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- HttpStatusCode
-} from "@gnu-taler/taler-util";
+import { HttpStatusCode } from "@gnu-taler/taler-util";
import {
Button,
LocalNotificationBanner,
ShowInputErrorLabel,
useLocalNotificationHandler,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
@@ -62,38 +60,42 @@ export function LoginForm({
ref.current?.focus();
}, []);
- const errors =
- undefinedIfEmpty({
- username: !username
- ? i18n.str`Missing username`
- : // : !USERNAME_REGEX.test(username)
+ const errors = undefinedIfEmpty({
+ username: !username
+ ? i18n.str`Missing username`
+ : // : !USERNAME_REGEX.test(username)
// ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
undefined,
- password: !password ? i18n.str`Missing password` : undefined,
- });
+ password: !password ? i18n.str`Missing password` : undefined,
+ });
async function doLogout() {
session.logOut();
}
- const loginHandler = !username || !password ? undefined : withErrorHandler(
- async () => authenticator(username)
- .createAccessToken(password, {
- // scope: "readwrite" as "write", // FIX: different than merchant
- scope: "readwrite",
- duration: { d_us: "forever" },
- refreshable: true,
- }),
- (result) => {
- session.logIn({ username, token: result.body.access_token })
- },
- (fail) => {
- switch (fail.case) {
- case HttpStatusCode.Unauthorized: return i18n.str`Wrong credentials for "${username}"`;
- case HttpStatusCode.NotFound: return i18n.str`Account not found`;
- }
- }
- )
+ const loginHandler =
+ !username || !password
+ ? undefined
+ : withErrorHandler(
+ async () =>
+ authenticator(username).createAccessToken(password, {
+ // scope: "readwrite" as "write", // FIX: different than merchant
+ scope: "readwrite",
+ duration: { d_us: "forever" },
+ refreshable: true,
+ }),
+ (result) => {
+ session.logIn({ username, token: result.body.access_token });
+ },
+ (fail) => {
+ switch (fail.case) {
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`Wrong credentials for "${username}"`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`Account not found`;
+ }
+ },
+ );
return (
<div class="flex min-h-full flex-col justify-center ">
diff --git a/packages/bank-ui/src/pages/OperationState/index.ts b/packages/bank-ui/src/pages/OperationState/index.ts
index e4d9d45e3..8ab5659b1 100644
--- a/packages/bank-ui/src/pages/OperationState/index.ts
+++ b/packages/bank-ui/src/pages/OperationState/index.ts
@@ -106,15 +106,15 @@ export namespace State {
account: string;
routeHere: RouteDefinition<{ wopid: string }>;
onAbort:
- | undefined
- | (() => Promise<
- TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
- >);
+ | undefined
+ | (() => Promise<
+ TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
+ >);
onConfirm:
- | undefined
- | (() => Promise<
- TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
- >);
+ | undefined
+ | (() => Promise<
+ TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
+ >);
error: undefined;
id: string;
}
diff --git a/packages/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts
index 9c5626cce..80af1a91d 100644
--- a/packages/bank-ui/src/pages/OperationState/state.ts
+++ b/packages/bank-ui/src/pages/OperationState/state.ts
@@ -191,9 +191,9 @@ export function useComponentState({
routeClose,
onAbort: !creds
? async () => {
- onAbort();
- return undefined;
- }
+ onAbort();
+ return undefined;
+ }
: doAbort,
};
}
diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx
index 6eee6daa9..330fe1072 100644
--- a/packages/bank-ui/src/pages/OperationState/views.tsx
+++ b/packages/bank-ui/src/pages/OperationState/views.tsx
@@ -73,6 +73,7 @@ export function NeedConfirmationView({
title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.BadRequest:
return notify({
@@ -80,6 +81,7 @@ export function NeedConfirmationView({
title: i18n.str`The operation id is invalid.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -87,6 +89,7 @@ export function NeedConfirmationView({
title: i18n.str`The operation was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
default:
assertUnreachable(resp);
@@ -111,6 +114,7 @@ export function NeedConfirmationView({
title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
return notify({
@@ -118,6 +122,7 @@ export function NeedConfirmationView({
title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.BadRequest:
return notify({
@@ -125,6 +130,7 @@ export function NeedConfirmationView({
title: i18n.str`The operation id is invalid.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -132,6 +138,7 @@ export function NeedConfirmationView({
title: i18n.str`The operation was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
return notify({
@@ -139,6 +146,7 @@ export function NeedConfirmationView({
title: i18n.str`Your balance is not enough.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
@@ -147,7 +155,6 @@ export function NeedConfirmationView({
sent: AbsoluteTime.never(),
location: routeHere.url({ wopid: id }),
request: id,
-
});
return onAuthorizationRequired();
}
@@ -331,10 +338,7 @@ export function ConfirmedView({ routeClose }: State.Confirmed) {
);
}
-export function ReadyView({
- uri,
- onAbort: doAbort,
-}: State.Ready): VNode {
+export function ReadyView({ uri, onAbort: doAbort }: State.Ready): VNode {
const { i18n } = useTranslationContext();
const walletInegrationApi = useTalerWalletIntegrationAPI();
const [notification, notify, errorHandler] = useLocalNotification();
@@ -355,6 +359,7 @@ export function ReadyView({
title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
description: hasError.detail.hint as TranslatedString,
debug: hasError.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.BadRequest:
return notify({
@@ -362,6 +367,7 @@ export function ReadyView({
title: i18n.str`The operation id is invalid.`,
description: hasError.detail.hint as TranslatedString,
debug: hasError.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -369,6 +375,7 @@ export function ReadyView({
title: i18n.str`The operation was not found.`,
description: hasError.detail.hint as TranslatedString,
debug: hasError.detail,
+ when: AbsoluteTime.now(),
});
default:
assertUnreachable(hasError);
diff --git a/packages/bank-ui/src/pages/PaymentOptions.tsx b/packages/bank-ui/src/pages/PaymentOptions.tsx
index 07dd18931..a034392d2 100644
--- a/packages/bank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/bank-ui/src/pages/PaymentOptions.tsx
@@ -15,15 +15,15 @@
*/
import { AmountJson, TalerError } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
+import { useEffect } from "preact/hooks";
+import { useWithdrawalDetails } from "../hooks/account.js";
import { useBankState } from "../hooks/bank-state.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "../route.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
-import { EmptyObject, RouteDefinition } from "../route.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { useWithdrawalDetails } from "../hooks/account.js";
-import { useEffect } from "preact/hooks";
-import { useSessionState } from "../hooks/session.js";
function ShowOperationPendingTag({
woid,
@@ -35,14 +35,15 @@ function ShowOperationPendingTag({
const { i18n } = useTranslationContext();
const { state: credentials } = useSessionState();
const result = useWithdrawalDetails(woid);
- const loading = !result
+ const loading = !result;
const error =
!loading && (result instanceof TalerError || result.type === "fail");
const pending =
- !loading && !error &&
- (result.body.status === "pending" || result.body.status === "selected")
- && credentials.status === "loggedIn"
- && credentials.username === result.body.username;
+ !loading &&
+ !error &&
+ (result.body.status === "pending" || result.body.status === "selected") &&
+ credentials.status === "loggedIn" &&
+ credentials.username === result.body.username;
useEffect(() => {
if (!loading && !pending && onOperationAlreadyCompleted) {
onOperationAlreadyCompleted();
@@ -96,9 +97,9 @@ export function PaymentOptions({
routeCashout: RouteDefinition;
routeChargeWallet: RouteDefinition;
routeWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
+ account?: string;
+ subject?: string;
+ amount?: string;
}>;
}): VNode {
const { i18n } = useTranslationContext();
@@ -126,9 +127,7 @@ export function PaymentOptions({
<span class="flex">
<div class="text-4xl mr-4 my-auto">&#x1F4B5;</div>
<span class="grow self-center text-lg text-gray-900 align-middle text-center">
- <i18n.Translate>
- to a Taler wallet
- </i18n.Translate>
+ <i18n.Translate>to a Taler wallet</i18n.Translate>
</span>
<svg
class="self-center flex-none h-5 w-5 text-indigo-600"
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
index 8d9df1151..d10f62cce 100644
--- a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -29,7 +29,7 @@ import {
assertUnreachable,
buildPayto,
parsePaytoUri,
- stringifyPaytoUri
+ stringifyPaytoUri,
} from "@gnu-taler/taler-util";
import {
InternationalizationAPI,
@@ -43,9 +43,9 @@ import { ComponentChildren, Fragment, Ref, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { mutate } from "swr";
import { useBankCoreApiContext } from "../context/config.js";
-import { useSessionState } from "../hooks/session.js";
import { useBankState } from "../hooks/bank-state.js";
-import { EmptyObject, RouteDefinition } from "../route.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "../route.js";
import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js";
interface Props {
@@ -59,9 +59,9 @@ interface Props {
routeCancel?: RouteDefinition;
routeCashout?: RouteDefinition;
routeHere: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
+ account?: string;
+ subject?: string;
+ amount?: string;
}>;
limit: AmountJson;
balance: AmountJson;
@@ -79,7 +79,6 @@ export function PaytoWireTransferForm({
routeHere,
onAuthorizationRequired,
limit,
- balance,
}: Props): VNode {
const [isRawPayto, setIsRawPayto] = useState(false);
const { state: credentials } = useSessionState();
@@ -101,14 +100,19 @@ export function PaytoWireTransferForm({
const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
const [notification, notify, handleError] = useLocalNotification();
- const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as const : "iban" as const;
+ const paytoType =
+ config.wire_type === "X_TALER_BANK"
+ ? ("x-taler-bank" as const)
+ : ("iban" as const);
const errorsWire = undefinedIfEmpty({
account: !account
? i18n.str`Required`
- : paytoType === "iban" ? validateIBAN(account, i18n) :
- paytoType === "x-taler-bank" ? validateTalerBank(account, i18n) :
- 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`
@@ -119,11 +123,11 @@ export function PaytoWireTransferForm({
const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
-
const errorsPayto = undefinedIfEmpty({
rawPaytoInput: !rawPaytoInput
? i18n.str`Required`
- : !parsed ? i18n.str`Does not follow the pattern`
+ : !parsed
+ ? i18n.str`Does not follow the pattern`
: validateRawPayto(parsed, limit, url.host, i18n, paytoType),
});
@@ -140,11 +144,15 @@ export function PaytoWireTransferForm({
delete p.params.amount;
// if this payto is valid then it already have message
payto_uri = stringifyPaytoUri(p);
- acName = !p.isKnown ? undefined :
- p.targetType === "iban" ? p.iban :
- p.targetType === "bitcoin" ? p.targetPath :
- p.targetType === "x-taler-bank" ? p.account :
- assertUnreachable(p);
+ acName = !p.isKnown
+ ? undefined
+ : p.targetType === "iban"
+ ? p.iban
+ : p.targetType === "bitcoin"
+ ? p.targetPath
+ : p.targetType === "x-taler-bank"
+ ? p.account
+ : assertUnreachable(p);
} else {
if (!account || !subject) return;
let payto;
@@ -159,7 +167,8 @@ export function PaytoWireTransferForm({
payto = buildPayto("iban", account, undefined);
break;
}
- default: assertUnreachable(paytoType)
+ default:
+ assertUnreachable(paytoType);
}
payto.params.message = encodeURIComponent(subject);
@@ -184,6 +193,7 @@ export function PaytoWireTransferForm({
title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Unauthorized:
return notify({
@@ -191,13 +201,25 @@ export function PaytoWireTransferForm({
title: i18n.str`Not enough permission to complete the operation.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_ADMIN_CREDITOR:
+ return notify({
+ type: "error",
+ title: i18n.str`Bank administrator can't be the transfer creditor.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
return notify({
type: "error",
- title: i18n.str`The destination account "${acName ?? puri}" was not found.`,
+ title: i18n.str`The destination account "${
+ acName ?? puri
+ }" was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_SAME_ACCOUNT:
return notify({
@@ -205,6 +227,7 @@ export function PaytoWireTransferForm({
title: i18n.str`The origin and the destination of the transfer can't be the same.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
return notify({
@@ -212,6 +235,7 @@ export function PaytoWireTransferForm({
title: i18n.str`Your balance is not enough.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -219,12 +243,17 @@ export function PaytoWireTransferForm({
title: i18n.str`The origin account "${puri}" was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "create-transaction",
id: String(resp.body.challenge_id),
- location: routeHere.url({ account: account ?? "", amount, subject }),
+ location: routeHere.url({
+ account: account ?? "",
+ amount,
+ subject,
+ }),
sent: AbsoluteTime.never(),
request,
});
@@ -281,10 +310,12 @@ export function PaytoWireTransferForm({
break;
}
default: {
- assertUnreachable(parsed)
+ assertUnreachable(parsed);
}
}
- const amountStr = !parsed.params ? undefined : parsed.params["amount"];
+ const amountStr = !parsed.params
+ ? undefined
+ : parsed.params["amount"];
if (amountStr) {
const amount = Amounts.parse(amountStr);
if (amount) {
@@ -350,7 +381,8 @@ export function PaytoWireTransferForm({
}
break;
}
- default: assertUnreachable(paytoType)
+ default:
+ assertUnreachable(paytoType);
}
rawPaytoInputSetter(stringifyPaytoUri(payto));
}
@@ -374,9 +406,7 @@ export function PaytoWireTransferForm({
>
<i18n.Translate>Cashout</i18n.Translate>
</a>
- ) : (
- undefined
- )}
+ ) : undefined}
</div>
</div>
@@ -394,34 +424,39 @@ export function PaytoWireTransferForm({
{(() => {
switch (paytoType) {
case "x-taler-bank": {
- return <TextField
- id="x-taler-bank"
- required
- 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}
- />
+ return (
+ <TextField
+ id="x-taler-bank"
+ required
+ 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"
- required
- 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}
- />
+ return (
+ <TextField
+ id="iban"
+ required
+ 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)
+ default:
+ assertUnreachable(paytoType);
}
})()}
@@ -506,11 +541,12 @@ export function PaytoWireTransferForm({
value={rawPaytoInput ?? ""}
required
title={i18n.str`Uniform resource identifier of the target account`}
-
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]`
+ 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 => {
@@ -618,13 +654,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);
@@ -668,81 +704,94 @@ export function RenderAmount({
);
}
-
-function validateRawPayto(parsed: PaytoUri, limit: AmountJson, host: string, i18n: InternationalizationAPI, type: "iban" | "x-taler-bank"): TranslatedString | undefined {
+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}"`
+ 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`
+ return i18n.str`Only "x-taler-bank" target are supported`;
}
if (parsed.host !== host) {
- return i18n.str`Only this host is allowed. Use "${host}"`
+ return i18n.str`Only this host is allowed. Use "${host}"`;
}
if (!parsed.account) {
- return i18n.str`Missing account name`
+ return i18n.str`Missing account name`;
}
- const result = validateTalerBank(parsed.account, i18n)
- if (result) return result
+ 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`
+ return i18n.str`Only "IBAN" target are supported`;
}
- const result = validateIBAN(parsed.iban, i18n)
- if (result) return result
+ const result = validateIBAN(parsed.iban, i18n);
+ if (result) return result;
break;
}
- default: assertUnreachable(type)
+ default:
+ assertUnreachable(type);
}
if (!parsed.params.amount) {
- return i18n.str`Missing "amount" parameter to specify the amount to be transferred`
+ return i18n.str`Missing "amount" parameter to specify the amount to be transferred`;
}
- const amount = Amounts.parse(parsed.params.amount)
+ const amount = Amounts.parse(parsed.params.amount);
if (!amount) {
- return i18n.str`The "amount" parameter is not valid`
+ return i18n.str`The "amount" parameter is not valid`;
}
- result = validateAmount(amount, limit, i18n)
+ 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`
+ return i18n.str`Missing the "message" parameter to specify a reference text for the transfer`;
}
- const subject = parsed.params.message
- result = validateSubject(subject, i18n)
+ const subject = parsed.params.message;
+ result = validateSubject(subject, i18n);
if (result) return result;
- return undefined
+ return undefined;
}
-function validateAmount(amount: AmountJson, limit: AmountJson, i18n: InternationalizationAPI): TranslatedString | undefined {
+function validateAmount(
+ amount: AmountJson,
+ limit: AmountJson,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
if (amount.currency !== limit.currency) {
- return i18n.str`The only currency allowed is "${limit.currency}"`
+ return i18n.str`The only currency allowed is "${limit.currency}"`;
}
if (Amounts.isZero(amount)) {
- return i18n.str`Can't transfer zero amount`
+ return i18n.str`Can't transfer zero amount`;
}
if (Amounts.cmp(limit, amount) === -1) {
- return i18n.str`Balance is not enough`
+ return i18n.str`Balance is not enough`;
}
- return undefined
+ return undefined;
}
-function validateSubject(text: string, i18n: InternationalizationAPI): TranslatedString | undefined {
+function validateSubject(
+ text: string,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
if (text.length < 2) {
- return i18n.str`Use a longer subject`
+ return i18n.str`Use a longer subject`;
}
- return undefined
+ return undefined;
}
interface PaytoFieldProps {
- id: string,
+ id: string;
label: TranslatedString;
required?: boolean;
help?: TranslatedString;
@@ -755,13 +804,17 @@ interface PaytoFieldProps {
disabled?: boolean;
}
-function Wrapper({ withIcon, children }: { withIcon: boolean, children: ComponentChildren }): VNode {
+function Wrapper({
+ withIcon,
+ children,
+}: {
+ withIcon: boolean;
+ children: ComponentChildren;
+}): VNode {
if (withIcon) {
- return <div class="flex justify-between">
- {children}
- </div>
+ return <div class="flex justify-between">{children}</div>;
}
- return <Fragment>{children}</Fragment>
+ return <Fragment>{children}</Fragment>;
}
export function TextField({
@@ -777,43 +830,34 @@ export function TextField({
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}
- {required &&
- <b style={{ color: "red" }}> *</b>
- }
- </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}
- />
+ return (
+ <div class="sm:col-span-5">
+ <label for={id} class="block text-sm font-medium leading-6 text-gray-900">
+ {label}
+ {required && <b style={{ color: "red" }}> *</b>}
+ </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>
- {help &&
- <p class="mt-2 text-sm text-gray-500">
- {help}
- </p>
- }
- </div>
+ );
}
diff --git a/packages/bank-ui/src/pages/ProfileNavigation.tsx b/packages/bank-ui/src/pages/ProfileNavigation.tsx
index 10497f015..1775d9329 100644
--- a/packages/bank-ui/src/pages/ProfileNavigation.tsx
+++ b/packages/bank-ui/src/pages/ProfileNavigation.tsx
@@ -27,9 +27,9 @@ export function ProfileNavigation({
routeMyAccountDelete,
routeMyAccountDetails,
routeMyAccountPassword,
- routeConversionConfig
+ routeConversionConfig,
}: {
- current: "details" | "delete" | "credentials" | "cashouts" | "conversion",
+ current: "details" | "delete" | "credentials" | "cashouts" | "conversion";
routeMyAccountDetails: RouteDefinition;
routeMyAccountDelete: RouteDefinition;
routeMyAccountPassword: RouteDefinition;
@@ -40,9 +40,7 @@ export function ProfileNavigation({
const { config } = useBankCoreApiContext();
const { state: credentials } = useSessionState();
const isAdminUser =
- credentials.status !== "loggedIn"
- ? false
- : credentials.isUserAdministrator;
+ credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator;
const nonAdminUser = !isAdminUser;
const { navigateTo } = useNavigationContext();
diff --git a/packages/bank-ui/src/pages/PublicHistoriesPage.tsx b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
index 84d703cbe..554da0c3f 100644
--- a/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
@@ -31,8 +31,8 @@ export function PublicHistoriesPage(): VNode {
const result = usePublicAccounts(undefined);
const firstAccount =
result &&
- !(result instanceof TalerError) &&
- result.data.public_accounts.length > 0
+ !(result instanceof TalerError) &&
+ result.data.public_accounts.length > 0
? result.data.public_accounts[0].username
: undefined;
@@ -71,7 +71,12 @@ export function PublicHistoriesPage(): VNode {
</a>
</li>,
);
- txs[account.username] = <Transactions account={account.username} routeCreateWireTransfer={undefined} />;
+ txs[account.username] = (
+ <Transactions
+ account={account.username}
+ routeCreateWireTransfer={undefined}
+ />
+ );
}
return (
diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx
index da11e631d..f442857a8 100644
--- a/packages/bank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/bank-ui/src/pages/QrCodeSection.tsx
@@ -17,13 +17,13 @@
import {
HttpStatusCode,
stringifyWithdrawUri,
- WithdrawUriResult
+ WithdrawUriResult,
} from "@gnu-taler/taler-util";
import {
Button,
LocalNotificationBanner,
useLocalNotificationHandler,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useEffect } from "preact/hooks";
@@ -56,20 +56,20 @@ export function QrCodeSection({
const onAbortHandler = handleError(
async () => {
if (!creds) return undefined;
- return api.abortWithdrawalById(
- creds,
- withdrawUri.withdrawalOperationId,
- )
+ return api.abortWithdrawalById(creds, withdrawUri.withdrawalOperationId);
},
onAborted,
(fail) => {
switch (fail.case) {
- case HttpStatusCode.BadRequest: return i18n.str`The operation id is invalid.`;
- case HttpStatusCode.NotFound: return i18n.str`The operation was not found.`;
- case HttpStatusCode.Conflict: return i18n.str`The reserve operation has been confirmed previously and can't be aborted`;
+ case HttpStatusCode.BadRequest:
+ return i18n.str`The operation id is invalid.`;
+ case HttpStatusCode.NotFound:
+ return i18n.str`The operation was not found.`;
+ case HttpStatusCode.Conflict:
+ return i18n.str`The reserve operation has been confirmed previously and can't be aborted`;
}
- }
- )
+ },
+ );
return (
<Fragment>
diff --git a/packages/bank-ui/src/pages/RegistrationPage.tsx b/packages/bank-ui/src/pages/RegistrationPage.tsx
index e9f7e602f..2ade465c2 100644
--- a/packages/bank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/bank-ui/src/pages/RegistrationPage.tsx
@@ -16,10 +16,7 @@
import {
AccessToken,
HttpStatusCode,
- OperationFail,
TalerErrorCode,
- TranslatedString,
- assertUnreachable,
} from "@gnu-taler/taler-util";
import {
LocalNotificationBanner,
@@ -77,7 +74,7 @@ function RegistrationForm({
// const [phone, setPhone] = useState<string | undefined>();
// const [email, setEmail] = useState<string | undefined>();
const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
- const [notification, _, handleError] = useLocalNotification();
+ const [notification, , handleError] = useLocalNotification();
const settings = useSettingsContext();
const { bank: api } = useBankCoreApiContext();
@@ -125,19 +122,29 @@ function RegistrationForm({
onComplete();
} else {
onError(resp, (_case) => {
- switch(_case) {
- case HttpStatusCode.BadRequest: return i18n.str`Server replied with invalid phone or email.`;
- case HttpStatusCode.Unauthorized: return i18n.str`No enough permission to create that account.`;
- case TalerErrorCode.BANK_UNALLOWED_DEBIT: return i18n.str`Registration is disabled because the bank ran out of bonus credit.`;
- case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: return i18n.str`That username can't be used because is reserved.`;
- case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: return i18n.str`That username is already taken.`;
- case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: return i18n.str`That account id is already taken.`;
- case TalerErrorCode.BANK_MISSING_TAN_INFO: return i18n.str`No information for the selected authentication channel.`;
- case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: return i18n.str`Authentication channel is not supported.`;
- case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return i18n.str`Only admin is allow to set debt limit.`;
- case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: return i18n.str`Only admin can create accounts with second factor authentication.`;
+ switch (_case) {
+ case HttpStatusCode.BadRequest:
+ return i18n.str`Server replied with invalid phone or email.`;
+ case HttpStatusCode.Unauthorized:
+ return i18n.str`No enough permission to create that account.`;
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return i18n.str`Registration is disabled because the bank ran out of bonus credit.`;
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return i18n.str`That username can't be used because is reserved.`;
+ case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
+ return i18n.str`That username is already taken.`;
+ case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
+ return i18n.str`That account id is already taken.`;
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return i18n.str`No information for the selected authentication channel.`;
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return i18n.str`Authentication channel is not supported.`;
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return i18n.str`Only admin is allow to set debt limit.`;
+ case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
+ return i18n.str`Only admin can create accounts with second factor authentication.`;
}
- })
+ });
}
});
}
diff --git a/packages/bank-ui/src/pages/ShowNotifications.tsx b/packages/bank-ui/src/pages/ShowNotifications.tsx
new file mode 100644
index 000000000..fe041fb19
--- /dev/null
+++ b/packages/bank-ui/src/pages/ShowNotifications.tsx
@@ -0,0 +1,55 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useNotifications } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { Time } from "../components/Time.js";
+
+export function ShowNotifications(): VNode {
+ const ns = useNotifications();
+ if (!ns.length) {
+ return <div>no notifications</div>;
+ }
+ return (
+ <div>
+ <p>Notifications</p>
+ <table>
+ <thead></thead>
+ <tbody>
+ {ns.map((n, idx) => {
+ return (
+ <tr key={idx}>
+ <td>
+ <Time
+ timestamp={n.message.when}
+ format="dd/MM/yyyy HH:mm:ss"
+ />
+ </td>
+ <td>{n.message.title}</td>
+ <td>
+ {n.message.type === "error"
+ ? n.message.description
+ : undefined}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {/* <ToastBanner all /> */}
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/SolveChallengePage.tsx b/packages/bank-ui/src/pages/SolveChallengePage.tsx
index b2e053b3c..528cc12df 100644
--- a/packages/bank-ui/src/pages/SolveChallengePage.tsx
+++ b/packages/bank-ui/src/pages/SolveChallengePage.tsx
@@ -34,21 +34,20 @@ import {
useLocalNotification,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
+import { Time } from "../components/Time.js";
import { useBankCoreApiContext } from "../context/config.js";
+import { useNavigationContext } from "../context/navigation.js";
import { useWithdrawalDetails } from "../hooks/account.js";
-import { useSessionState } from "../hooks/session.js";
import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js";
import { useConversionInfo } from "../hooks/regional.js";
+import { useSessionState } from "../hooks/session.js";
import { RouteDefinition } from "../route.js";
import { undefinedIfEmpty } from "../utils.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
import { OperationNotFound } from "./WithdrawalQRCode.js";
-import { useNavigationContext } from "../context/navigation.js";
-import { Time } from "../components/Time.js";
export function SolveChallengePage({
onChallengeCompleted,
@@ -107,6 +106,7 @@ export function SolveChallengePage({
title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Unauthorized:
return notify({
@@ -114,6 +114,7 @@ export function SolveChallengePage({
title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
return notify({
@@ -121,6 +122,7 @@ export function SolveChallengePage({
title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
default:
assertUnreachable(resp);
@@ -145,6 +147,7 @@ export function SolveChallengePage({
title: i18n.str`Challenge not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Unauthorized:
return notify({
@@ -152,6 +155,7 @@ export function SolveChallengePage({
title: i18n.str`This user is not authorized to complete this challenge.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.TooManyRequests:
return notify({
@@ -159,6 +163,7 @@ export function SolveChallengePage({
title: i18n.str`Too many attempts, try another code.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED:
return notify({
@@ -166,6 +171,7 @@ export function SolveChallengePage({
title: i18n.str`The confirmation code is wrong, try again.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED:
return notify({
@@ -173,6 +179,7 @@ export function SolveChallengePage({
title: i18n.str`The operation expired.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
default:
assertUnreachable(resp);
@@ -206,6 +213,7 @@ export function SolveChallengePage({
title: i18n.str`The operation failed.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
}
// another challenge required, save the request and the ID
@@ -220,6 +228,7 @@ export function SolveChallengePage({
return notify({
type: "info",
title: i18n.str`The operation needs another confirmation to complete.`,
+ when: AbsoluteTime.now(),
});
}
updateBankState("currentChallenge", undefined);
@@ -267,7 +276,7 @@ export function SolveChallengePage({
onStart={startChallenge}
onCancel={() => {
updateBankState("currentChallenge", undefined);
- navigateTo(ch.location)
+ navigateTo(ch.location);
}}
/>
{ch.info && (
@@ -341,15 +350,15 @@ function ChallengeDetails({
onStart: () => void;
onCancel: () => void;
}): VNode {
- const { i18n, dateLocale } = useTranslationContext();
+ const { i18n } = useTranslationContext();
const { config } = useBankCoreApiContext();
- const firstTime = AbsoluteTime.isNever(challenge.sent)
+ const firstTime = AbsoluteTime.isNever(challenge.sent);
useEffect(() => {
if (firstTime) {
- onStart()
+ onStart();
}
- }, [])
+ }, []);
return (
<div class="px-4 mt-4 ">
<div class="w-full">
@@ -535,9 +544,11 @@ function ChallengeDetails({
<i18n.Translate>Sent at</i18n.Translate>
</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <Time format="dd/MM/yyyy HH:mm:ss"
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
timestamp={challenge.sent}
- relative={Duration.fromSpec({ days: 1 })} />
+ relative={Duration.fromSpec({ days: 1 })}
+ />
</dd>
</div>
)}
@@ -668,11 +679,11 @@ function ShowCashoutDetails({
switch (info.case) {
case HttpStatusCode.NotImplemented: {
return (
- <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 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>
);
}
diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
index 8c831199a..f16488b25 100644
--- a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
@@ -15,12 +15,13 @@
*/
import {
+ AbsoluteTime,
AmountJson,
Amounts,
HttpStatusCode,
TranslatedString,
assertUnreachable,
- parseWithdrawUri
+ parseWithdrawUri,
} from "@gnu-taler/taler-util";
import {
Attention,
@@ -39,7 +40,11 @@ import { usePreferences } from "../hooks/preferences.js";
import { RouteDefinition } from "../route.js";
import { undefinedIfEmpty } from "../utils.js";
import { OperationState } from "./OperationState/index.js";
-import { InputAmount, RenderAmount, doAutoFocus } from "./PaytoWireTransferForm.js";
+import {
+ InputAmount,
+ RenderAmount,
+ doAutoFocus,
+} from "./PaytoWireTransferForm.js";
const RefAmount = forwardRef(InputAmount);
@@ -54,7 +59,7 @@ function OldWithdrawalForm({
limit: AmountJson;
balance: AmountJson;
focus?: boolean;
- routeOperationDetails: RouteDefinition<{ wopid: string }>,
+ routeOperationDetails: RouteDefinition<{ wopid: string }>;
onOperationCreated: (wopid: string) => void;
routeCancel: RouteDefinition;
}): VNode {
@@ -87,23 +92,25 @@ function OldWithdrawalForm({
wopid: bankState.currentWithdrawalOperationId,
});
return (
- <Attention type="warning" title={i18n.str`There is an operation already`} onClose={() => {
- updateBankState("currentWithdrawalOperationId", undefined);
- }}>
+ <Attention
+ type="warning"
+ title={i18n.str`There is an operation already`}
+ onClose={() => {
+ updateBankState("currentWithdrawalOperationId", undefined);
+ }}
+ >
<span ref={focus ? doAutoFocus : undefined} />
- <i18n.Translate>
- Complete the operation in
- </i18n.Translate>{" "}
+ <i18n.Translate>Complete the operation in</i18n.Translate>{" "}
<a
class="font-semibold text-yellow-700 hover:text-yellow-600"
name="complete operation"
href={url}
- // onClick={(e) => {
- // e.preventDefault()
- // walletInegrationApi.publishTalerAction(uri, () => {
- // navigateTo(url)
- // })
- // }}
+ // onClick={(e) => {
+ // e.preventDefault()
+ // walletInegrationApi.publishTalerAction(uri, () => {
+ // navigateTo(url)
+ // })
+ // }}
>
<i18n.Translate>this page</i18n.Translate>
</a>
@@ -156,6 +163,7 @@ function OldWithdrawalForm({
title: i18n.str`The operation was rejected due to insufficient funds`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
break;
}
@@ -165,6 +173,7 @@ function OldWithdrawalForm({
title: i18n.str`The operation was rejected due to insufficient funds`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
break;
}
@@ -174,6 +183,7 @@ function OldWithdrawalForm({
title: i18n.str`Account not found`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
break;
}
@@ -213,16 +223,24 @@ function OldWithdrawalForm({
</div>
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
- Current balance is <RenderAmount value={balance} spec={config.currency_specification} />
+ Current balance is{" "}
+ <RenderAmount
+ value={balance}
+ spec={config.currency_specification}
+ />
</i18n.Translate>
</p>
- {Amounts.cmp(limit, balance) > 0 ?
+ {Amounts.cmp(limit, balance) > 0 ? (
<p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
- Your account allows you to withdraw <RenderAmount value={limit} spec={config.currency_specification} />
+ Your account allows you to withdraw{" "}
+ <RenderAmount
+ value={limit}
+ spec={config.currency_specification}
+ />
</i18n.Translate>
- </p> : undefined
- }
+ </p>
+ ) : undefined}
<div class="mt-4">
<div class="sm:inline">
<button
@@ -312,7 +330,7 @@ export function WalletWithdrawForm({
limit: AmountJson;
balance: AmountJson;
focus?: boolean;
- routeOperationDetails: RouteDefinition<{ wopid: string }>,
+ routeOperationDetails: RouteDefinition<{ wopid: string }>;
onAuthorizationRequired: () => void;
onOperationCreated: (wopid: string) => void;
onOperationAborted: () => void;
@@ -374,7 +392,7 @@ export function WalletWithdrawForm({
routeClose={routeCancel}
routeHere={routeOperationDetails}
onAbort={onOperationAborted}
- // route={routeCancel}
+ // route={routeCancel}
/>
)}
</div>
diff --git a/packages/bank-ui/src/pages/WireTransfer.tsx b/packages/bank-ui/src/pages/WireTransfer.tsx
index a3f7d6bc0..a459677f1 100644
--- a/packages/bank-ui/src/pages/WireTransfer.tsx
+++ b/packages/bank-ui/src/pages/WireTransfer.tsx
@@ -43,13 +43,13 @@ export function WireTransfer({
}: {
onSuccess?: () => void;
routeHere: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
+ account?: string;
+ subject?: string;
+ amount?: string;
}>;
toAccount?: string;
- withSubject?: string,
- withAmount?: string,
+ withSubject?: string;
+ withAmount?: string;
routeCancel?: RouteDefinition;
onAuthorizationRequired: () => void;
}): VNode {
diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 5925719c3..965650eb0 100644
--- a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -96,6 +96,7 @@ export function WithdrawalConfirmationQuestion({
title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
return notify({
@@ -103,6 +104,7 @@ export function WithdrawalConfirmationQuestion({
title: i18n.str`The withdrawal operation can't be confirmed before a wallet accepted the transaction.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.BadRequest:
return notify({
@@ -110,6 +112,7 @@ export function WithdrawalConfirmationQuestion({
title: i18n.str`The operation id is invalid.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -117,6 +120,7 @@ export function WithdrawalConfirmationQuestion({
title: i18n.str`The operation was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
return notify({
@@ -124,12 +128,15 @@ export function WithdrawalConfirmationQuestion({
title: i18n.str`Your balance is not enough for the operation.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "confirm-withdrawal",
id: String(resp.body.challenge_id),
- location: routeHere.url({ wopid: withdrawUri.withdrawalOperationId }),
+ location: routeHere.url({
+ wopid: withdrawUri.withdrawalOperationId,
+ }),
sent: AbsoluteTime.never(),
request: withdrawUri.withdrawalOperationId,
});
@@ -157,6 +164,9 @@ export function WithdrawalConfirmationQuestion({
return notify({
type: "error",
title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.BadRequest:
return notify({
@@ -164,6 +174,7 @@ export function WithdrawalConfirmationQuestion({
title: i18n.str`The operation id is invalid.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -171,6 +182,7 @@ export function WithdrawalConfirmationQuestion({
title: i18n.str`The operation was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
default: {
assertUnreachable(resp);
@@ -218,7 +230,9 @@ export function WithdrawalConfirmationQuestion({
<Fragment>
<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">
- <i18n.Translate>Payment provider's account number</i18n.Translate>
+ <i18n.Translate>
+ Payment provider's account number
+ </i18n.Translate>
</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
{p.iban}
@@ -227,7 +241,9 @@ export function WithdrawalConfirmationQuestion({
{name && (
<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">
- <i18n.Translate>Payment provider's name</i18n.Translate>
+ <i18n.Translate>
+ Payment provider's name
+ </i18n.Translate>
</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
{name}
@@ -244,7 +260,9 @@ export function WithdrawalConfirmationQuestion({
<Fragment>
<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">
- <i18n.Translate>Payment provider's account id</i18n.Translate>
+ <i18n.Translate>
+ Payment provider's account id
+ </i18n.Translate>
</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
{p.account}
@@ -253,7 +271,9 @@ export function WithdrawalConfirmationQuestion({
{name && (
<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">
- <i18n.Translate>Payment provider's name</i18n.Translate>
+ <i18n.Translate>
+ Payment provider's name
+ </i18n.Translate>
</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
{name}
@@ -267,7 +287,9 @@ export function WithdrawalConfirmationQuestion({
return (
<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">
- <i18n.Translate>Payment provider's account</i18n.Translate>
+ <i18n.Translate>
+ Payment provider's account
+ </i18n.Translate>
</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
{details.account.targetPath}
diff --git a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
index b91fecd9d..fb280cf9c 100644
--- a/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
+++ b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
@@ -31,7 +31,7 @@ export function WithdrawalOperationPage({
}: {
onAuthorizationRequired: () => void;
operationId: string;
- purpose: "after-creation" | "after-confirmation",
+ purpose: "after-creation" | "after-confirmation";
onOperationAborted: () => void;
routeClose: RouteDefinition;
routeWithdrawalDetails: RouteDefinition<{ wopid: string }>;
diff --git a/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
index 2216b96fc..bd9352b21 100644
--- a/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
+++ b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -31,7 +31,7 @@ interface Props {
routeMyAccountPassword: RouteDefinition;
routeMyAccountCashout: RouteDefinition;
routeCreateCashout: RouteDefinition;
- routeConversionConfig:RouteDefinition;
+ routeConversionConfig: RouteDefinition;
}
export function CashoutListForAccount({
@@ -58,7 +58,8 @@ export function CashoutListForAccount({
return (
<Fragment>
{accountIsTheCurrentUser ? (
- <ProfileNavigation current="cashouts"
+ <ProfileNavigation
+ current="cashouts"
routeMyAccountCashout={routeMyAccountCashout}
routeMyAccountDelete={routeMyAccountDelete}
routeMyAccountDetails={routeMyAccountDetails}
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
index 62c8df7f8..39b2303c0 100644
--- a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -118,6 +118,7 @@ export function ShowAccountDetails({
title: i18n.str`The rights to change the account are not sufficient`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -125,6 +126,7 @@ export function ShowAccountDetails({
title: i18n.str`The username was not found`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME:
return notify({
@@ -132,6 +134,7 @@ export function ShowAccountDetails({
title: i18n.str`You can't change the legal name, please contact the your account administrator.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
return notify({
@@ -139,6 +142,7 @@ export function ShowAccountDetails({
title: i18n.str`You can't change the debt limit, please contact the your account administrator.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT:
return notify({
@@ -146,6 +150,7 @@ export function ShowAccountDetails({
title: i18n.str`You can't change the cashout address, please contact the your account administrator.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_MISSING_TAN_INFO:
return notify({
@@ -153,6 +158,7 @@ export function ShowAccountDetails({
title: i18n.str`No information for the selected authentication channel.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
@@ -170,6 +176,7 @@ export function ShowAccountDetails({
title: i18n.str`Authentication channel is not supported.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
}
default:
@@ -183,7 +190,8 @@ export function ShowAccountDetails({
<Fragment>
<LocalNotificationBanner notification={notification} showDebug={true} />
{accountIsTheCurrentUser ? (
- <ProfileNavigation current="details"
+ <ProfileNavigation
+ current="details"
routeMyAccountCashout={routeMyAccountCashout}
routeMyAccountDelete={routeMyAccountDelete}
routeConversionConfig={routeConversionConfig}
diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
index c33aeb09e..8c0581312 100644
--- a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
+++ b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -17,6 +17,7 @@ import {
AbsoluteTime,
HttpStatusCode,
TalerErrorCode,
+ TranslatedString,
assertUnreachable,
} from "@gnu-taler/taler-util";
import {
@@ -112,21 +113,33 @@ export function UpdateAccountPassword({
return notify({
type: "error",
title: i18n.str`Not authorized to change the password, maybe the session is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
type: "error",
title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD:
return notify({
type: "error",
title: i18n.str`You need to provide the old password. If you don't have it contact your account administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD:
return notify({
type: "error",
title: i18n.str`Your current password doesn't match, can't change to a new password.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
@@ -149,7 +162,8 @@ export function UpdateAccountPassword({
<Fragment>
<LocalNotificationBanner notification={notification} />
{accountIsTheCurrentUser ? (
- <ProfileNavigation current="credentials"
+ <ProfileNavigation
+ current="credentials"
routeMyAccountCashout={routeMyAccountCashout}
routeMyAccountDelete={routeMyAccountDelete}
routeMyAccountDetails={routeMyAccountDetails}
@@ -273,7 +287,6 @@ export function UpdateAccountPassword({
<i18n.Translate>Repeat the same password</i18n.Translate>
</p>
</div>
-
</div>
</div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx
index bce7afe11..10b6afdf9 100644
--- a/packages/bank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/bank-ui/src/pages/admin/AccountForm.tsx
@@ -18,14 +18,12 @@ import {
Amounts,
PaytoString,
TalerCorebankApi,
- TranslatedString,
assertUnreachable,
buildPayto,
parsePaytoUri,
stringifyPaytoUri,
} from "@gnu-taler/taler-util";
import {
- Attention,
CopyButton,
ShowInputErrorLabel,
useTranslationContext,
@@ -41,7 +39,11 @@ import {
validateIBAN,
validateTalerBank,
} from "../../utils.js";
-import { InputAmount, TextField, doAutoFocus } from "../PaytoWireTransferForm.js";
+import {
+ InputAmount,
+ TextField,
+ doAutoFocus,
+} from "../PaytoWireTransferForm.js";
import { getRandomPassword } from "../rnd.js";
const EMAIL_REGEX =
@@ -99,7 +101,10 @@ 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 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 = {
@@ -110,8 +115,10 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
isPublic: template?.is_public,
name: template?.name ?? "",
cashout_payto_uri:
- getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ?? ("" as PaytoString),
- payto_uri: getAccountId(paytoType, 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 ?? "",
@@ -130,9 +137,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
const isCashoutEnabled = config.allow_conversion;
const editableCashout =
- (purpose === "create" ||
- (purpose === "update" &&
- (config.allow_edit_cashout_payto_uri || userIsAdmin)));
+ purpose === "create" ||
+ (purpose === "update" &&
+ (config.allow_edit_cashout_payto_uri || userIsAdmin));
const editableThreshold =
userIsAdmin && (purpose === "create" || purpose === "update");
const editableAccount = purpose === "create" && userIsAdmin;
@@ -141,7 +148,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
const hasEmail = !!defaultValue.email || !!form.email;
function updateForm(newForm: typeof defaultValue): void {
-
const trimmedAmountStr = newForm.debit_threshold?.trim();
const parsedAmount = Amounts.parse(
`${config.currency}:${trimmedAmountStr}`,
@@ -154,19 +160,25 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
? undefined
: !editableCashout
? undefined
- : !newForm.cashout_payto_uri ? undefined
- : cashoutPaytoType === "iban" ? validateIBAN(newForm.cashout_payto_uri, i18n) :
- cashoutPaytoType === "x-taler-bank" ? validateTalerBank(newForm.cashout_payto_uri, i18n) :
- undefined,
+ : !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
- : !newForm.payto_uri ? undefined
- : paytoType === "iban" ? validateIBAN(newForm.payto_uri, i18n) :
- paytoType === "x-taler-bank" ? validateTalerBank(newForm.payto_uri, i18n) :
- undefined,
+ : !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
@@ -207,30 +219,38 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
onChange(undefined);
} else {
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;
+ 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);
}
- default: assertUnreachable(cashoutPaytoType)
- }
const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout);
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;
+ 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);
}
- default: assertUnreachable(paytoType)
- }
const internalURI = !internal ? undefined : stringifyPaytoUri(internal);
const threshold = !parsedAmount
@@ -247,7 +267,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
username: newForm.username!,
contact_data: undefinedIfEmpty({
email: !newForm.email ? undefined : newForm.email,
- phone: !newForm.phone ? undefined :newForm.phone,
+ phone: !newForm.phone ? undefined : newForm.phone,
}),
debit_threshold: threshold ?? config.default_debit_threshold,
cashout_payto_uri: cashoutURI,
@@ -270,7 +290,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
cashout_payto_uri: cashoutURI,
contact_data: undefinedIfEmpty({
email: !newForm.email ? undefined : newForm.email,
- phone: !newForm.phone ? undefined :newForm.phone,
+ phone: !newForm.phone ? undefined : newForm.phone,
}),
debit_threshold: threshold,
is_public: newForm.isPublic,
@@ -370,7 +390,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
</p>
</div>
- {purpose === "create" ? undefined :
+ {purpose === "create" ? undefined : (
<TextField
id="internal-account"
label={i18n.str`Internal account`}
@@ -379,20 +399,23 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
? i18n.str`If empty a random account id will be assigned`
: i18n.str`Share this id to receive bank transfers`
}
-
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 ?? ""}
- />}
+ 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">
<label
@@ -422,7 +445,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
/>
</div>
<p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>To be used when second factor authentication is enabled</i18n.Translate>
+ <i18n.Translate>
+ To be used when second factor authentication is enabled
+ </i18n.Translate>
</p>
</div>
@@ -454,7 +479,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
/>
</div>
<p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>To be used when second factor authentication is enabled</i18n.Translate>
+ <i18n.Translate>
+ To be used when second factor authentication is enabled
+ </i18n.Translate>
</p>
</div>
@@ -468,14 +495,17 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
form.cashout_payto_uri = e as PaytoString;
updateForm(structuredClone(form));
}}
- value={(form.cashout_payto_uri ?? defaultValue.cashout_payto_uri) as PaytoString}
+ value={
+ (form.cashout_payto_uri ??
+ defaultValue.cashout_payto_uri) as PaytoString
+ }
disabled={!editableCashout}
/>
)}
{/* channel, not shown if old cashout api */}
{OLD_CASHOUT_API ||
- config.supported_tan_channels.length === 0 ? undefined : (
+ config.supported_tan_channels.length === 0 ? undefined : (
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
@@ -486,7 +516,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
<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">
{config.supported_tan_channels.indexOf(TanChannel.EMAIL) ===
- -1 ? undefined : (
+ -1 ? undefined : (
<label
onClick={(e) => {
if (!hasEmail) return;
@@ -544,7 +574,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
)}
{config.supported_tan_channels.indexOf(TanChannel.SMS) ===
- -1 ? undefined : (
+ -1 ? undefined : (
<label
onClick={(e) => {
if (!hasPhone) return;
@@ -619,9 +649,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
!editableThreshold
? undefined
: (e) => {
- form.debit_threshold = e as AmountString;
- updateForm(structuredClone(form));
- }
+ form.debit_threshold = e as AmountString;
+ updateForm(structuredClone(form));
+ }
}
/>
<ShowInputErrorLabel
@@ -633,7 +663,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
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>
+ <i18n.Translate>
+ How much the balance can go below zero.
+ </i18n.Translate>
</p>
</div>
@@ -673,7 +705,9 @@ 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>
@@ -685,7 +719,9 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
class="text-sm text-black font-medium leading-6 "
id="availability-label"
>
- <i18n.Translate>Is this account a payment provider?</i18n.Translate>
+ <i18n.Translate>
+ Is this account a payment provider?
+ </i18n.Translate>
</span>
</span>
<button
@@ -726,13 +762,17 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
);
}
-function getAccountId(type: "iban" | "x-taler-bank", 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 "<unknown>";
if (type === "iban" && p.targetType === "iban") return p.iban;
- if (type === "x-taler-bank" && p.targetType === "x-taler-bank") return p.account;
+ if (type === "x-taler-bank" && p.targetType === "x-taler-bank")
+ return p.account;
return "<unsupported>";
}
diff --git a/packages/bank-ui/src/pages/admin/AccountList.tsx b/packages/bank-ui/src/pages/admin/AccountList.tsx
index 8a692aaed..3ab491960 100644
--- a/packages/bank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/bank-ui/src/pages/admin/AccountList.tsx
@@ -24,8 +24,8 @@ import { Fragment, VNode, h } from "preact";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { useBankCoreApiContext } from "../../context/config.js";
import { useBusinessAccounts } from "../../hooks/regional.js";
-import { RenderAmount } from "../PaytoWireTransferForm.js";
import { RouteDefinition } from "../../route.js";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
interface Props {
routeCreate: RouteDefinition;
@@ -33,14 +33,12 @@ interface Props {
routeShowAccount: RouteDefinition<{ account: string }>;
routeRemoveAccount: RouteDefinition<{ account: string }>;
routeUpdatePasswordAccount: RouteDefinition<{ account: string }>;
- routeShowCashoutsAccount: RouteDefinition<{ account: string }>;
}
export function AccountList({
routeCreate,
routeRemoveAccount,
routeShowAccount,
- routeShowCashoutsAccount,
routeUpdatePasswordAccount,
}: Props): VNode {
const result = useBusinessAccounts();
@@ -62,8 +60,8 @@ export function AccountList({
}
}
- const onGoStart = result.isFirstPage ? undefined : result.loadFirst
- const onGoNext = result.isLastPage ? undefined : result.loadNext
+ const onGoStart = result.isFirstPage ? undefined : result.loadFirst;
+ const onGoNext = result.isLastPage ? undefined : result.loadNext;
const accounts = result.result;
return (
@@ -90,9 +88,7 @@ export function AccountList({
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
{!accounts.length ? (
- <div>
- {/* FIXME: ADD empty list */}
- </div>
+ <div>{/* FIXME: ADD empty list */}</div>
) : (
<table class="min-w-full divide-y divide-gray-300">
<thead>
@@ -230,7 +226,6 @@ export function AccountList({
</button>
</div>
</nav>
-
</div>
</div>
</div>
diff --git a/packages/bank-ui/src/pages/admin/AdminHome.tsx b/packages/bank-ui/src/pages/admin/AdminHome.tsx
index 752d86aa6..b8b28f8a0 100644
--- a/packages/bank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/bank-ui/src/pages/admin/AdminHome.tsx
@@ -53,9 +53,9 @@ interface Props {
routeCreate: RouteDefinition;
routeDownloadStats: RouteDefinition;
routeCreateWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
+ account?: string;
+ subject?: string;
+ amount?: string;
}>;
routeShowAccount: RouteDefinition<{ account: string }>;
@@ -68,7 +68,6 @@ export function AdminHome({
routeCreate,
routeRemoveAccount,
routeShowAccount,
- routeShowCashoutsAccount,
routeUpdatePasswordAccount,
routeDownloadStats,
routeCreateWireTransfer,
@@ -77,7 +76,10 @@ export function AdminHome({
return (
<Fragment>
<Metrics routeDownloadStats={routeDownloadStats} />
- <WireTransfer routeHere={routeCreateWireTransfer} onAuthorizationRequired={onAuthorizationRequired} />
+ <WireTransfer
+ routeHere={routeCreateWireTransfer}
+ onAuthorizationRequired={onAuthorizationRequired}
+ />
<Transactions
account="admin"
@@ -87,7 +89,6 @@ export function AdminHome({
routeCreate={routeCreate}
routeRemoveAccount={routeRemoveAccount}
routeShowAccount={routeShowAccount}
- routeShowCashoutsAccount={routeShowCashoutsAccount}
routeUpdatePasswordAccount={routeUpdatePasswordAccount}
/>
</Fragment>
@@ -355,13 +356,16 @@ function Metrics({
</div>
<dl class="mt-5 grid grid-cols-1 md:grid-cols-2 divide-y divide-gray-200 overflow-hidden rounded-lg bg-white shadow-lg md:divide-x md:divide-y-0">
{resp.current.body.type !== "with-conversions" ||
- resp.previous.body.type !== "with-conversions" ? undefined : (
+ resp.previous.body.type !== "with-conversions" ? undefined : (
<Fragment>
<div class="px-4 py-5 sm:p-6">
<dt class="text-base font-normal text-gray-900">
<i18n.Translate>Cashin</i18n.Translate>
<div class="text-xs text-gray-500">
- <i18n.Translate>Transferred from an external account to an account in this bank.</i18n.Translate>
+ <i18n.Translate>
+ Transferred from an external account to an account in this
+ bank.
+ </i18n.Translate>
</div>
</dt>
<MetricValue
@@ -375,8 +379,11 @@ function Metrics({
<i18n.Translate>Cashout</i18n.Translate>
</dt>
<div class="text-xs text-gray-500">
- <i18n.Translate>Transferred from an account in this bank to an external account.</i18n.Translate>
- </div>
+ <i18n.Translate>
+ Transferred from an account in this bank to an external
+ account.
+ </i18n.Translate>
+ </div>
<MetricValue
current={resp.current.body.cashoutFiatVolume}
previous={resp.previous.body.cashoutFiatVolume}
@@ -389,7 +396,9 @@ function Metrics({
<dt class="text-base font-normal text-gray-900">
<i18n.Translate>Payin</i18n.Translate>
<div class="text-xs text-gray-500">
- <i18n.Translate>Transferred from an account to a Taler exchange.</i18n.Translate>
+ <i18n.Translate>
+ Transferred from an account to a Taler exchange.
+ </i18n.Translate>
</div>
</dt>
<MetricValue
@@ -402,7 +411,9 @@ function Metrics({
<dt class="text-base font-normal text-gray-900">
<i18n.Translate>Payout</i18n.Translate>
<div class="text-xs text-gray-500">
- <i18n.Translate>Transferred from a Taler exchange to another account.</i18n.Translate>
+ <i18n.Translate>
+ Transferred from a Taler exchange to another account.
+ </i18n.Translate>
</div>
</dt>
<MetricValue
@@ -444,9 +455,9 @@ function MetricValue({
const rate =
!currAmount ||
- Number.isNaN(currAmount) ||
- !prevAmount ||
- Number.isNaN(prevAmount)
+ Number.isNaN(currAmount) ||
+ !prevAmount ||
+ Number.isNaN(prevAmount)
? 0
: cmp === -1
? 1 - Math.round(currAmount) / Math.round(prevAmount)
diff --git a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
index 38119735e..f5755e2cd 100644
--- a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {
+ AbsoluteTime,
HttpStatusCode,
TalerCorebankApi,
TalerErrorCode,
@@ -69,6 +70,7 @@ export function CreateNewAccount({
title: i18n.str`Server replied that phone or email is invalid`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Unauthorized:
return notify({
@@ -76,6 +78,7 @@ export function CreateNewAccount({
title: i18n.str`The rights to perform the operation are not sufficient`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
return notify({
@@ -83,6 +86,7 @@ export function CreateNewAccount({
title: i18n.str`Account username is already taken`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
return notify({
@@ -90,6 +94,7 @@ export function CreateNewAccount({
title: i18n.str`Account id is already taken`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
return notify({
@@ -97,6 +102,7 @@ export function CreateNewAccount({
title: i18n.str`Bank ran out of bonus credit.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
return notify({
@@ -104,6 +110,7 @@ export function CreateNewAccount({
title: i18n.str`Account username can't be used because is reserved`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
return notify({
@@ -111,6 +118,7 @@ export function CreateNewAccount({
title: i18n.str`Only admin is allow to set debt limit.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_MISSING_TAN_INFO:
return notify({
@@ -118,6 +126,7 @@ export function CreateNewAccount({
title: i18n.str`No information for the selected authentication channel.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
return notify({
@@ -125,6 +134,7 @@ export function CreateNewAccount({
title: i18n.str`Authentication channel is not supported.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
return notify({
@@ -132,6 +142,7 @@ export function CreateNewAccount({
title: i18n.str`Only admin can create accounts with second factor authentication.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
default:
assertUnreachable(resp);
diff --git a/packages/bank-ui/src/pages/admin/DownloadStats.tsx b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
index fba366676..40035db51 100644
--- a/packages/bank-ui/src/pages/admin/DownloadStats.tsx
+++ b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
@@ -31,7 +31,7 @@ import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { useBankCoreApiContext } from "../../context/config.js";
import { useSessionState } from "../../hooks/session.js";
-import { EmptyObject, RouteDefinition } from "../../route.js";
+import { RouteDefinition } from "../../route.js";
import { getTimeframesForDate } from "./AdminHome.js";
interface Props {
@@ -341,7 +341,8 @@ export function DownloadStats({ routeCancel }: Props): VNode {
</div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- <a name="cancel"
+ <a
+ name="cancel"
href={routeCancel.url({})}
class="text-sm font-semibold leading-6 text-gray-900"
>
@@ -459,9 +460,9 @@ async function fetchAllStatus(
// await delay()
const previous = options.compareWithPrevious
? await api.getMonitor(token, {
- timeframe: frame.timeframe,
- which: frame.moment.previous,
- })
+ timeframe: frame.timeframe,
+ which: frame.moment.previous,
+ })
: undefined;
if (previous && previous.type === "fail" && options.endOnFirstFail) {
diff --git a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
index 61def9a95..74172d058 100644
--- a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
@@ -127,6 +127,7 @@ export function RemoveAccount({
title: i18n.str`No enough permission to delete the account.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotFound:
return notify({
@@ -134,6 +135,7 @@ export function RemoveAccount({
title: i18n.str`The username was not found.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
return notify({
@@ -141,6 +143,7 @@ export function RemoveAccount({
title: i18n.str`Can't delete a reserved username.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO:
return notify({
@@ -148,6 +151,7 @@ export function RemoveAccount({
title: i18n.str`Can't delete an account with balance different than zero.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
index 8845ec9a0..818a131e0 100644
--- a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
+++ b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
@@ -15,13 +15,14 @@
*/
import {
+ AbsoluteTime,
AmountJson,
Amounts,
HttpStatusCode,
TalerBankConversionApi,
TalerError,
TranslatedString,
- assertUnreachable
+ assertUnreachable,
} from "@gnu-taler/taler-util";
import {
Attention,
@@ -30,18 +31,30 @@ import {
ShowInputErrorLabel,
useLocalNotification,
useTranslationContext,
- utils
+ utils,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import { useBankCoreApiContext } from "../../context/config.js";
import { useSessionState } from "../../hooks/session.js";
-import { TransferCalculation, useCashinEstimator, useCashoutEstimator, useConversionInfo } from "../../hooks/regional.js";
+import {
+ TransferCalculation,
+ useCashinEstimator,
+ useCashoutEstimator,
+ useConversionInfo,
+} from "../../hooks/regional.js";
import { RouteDefinition } from "../../route.js";
import { undefinedIfEmpty } from "../../utils.js";
import { InputAmount, RenderAmount } from "../PaytoWireTransferForm.js";
import { ProfileNavigation } from "../ProfileNavigation.js";
-import { FormErrors, FormStatus, FormValues, RecursivePartial, UIField, useFormState } from "../../hooks/form.js";
+import {
+ FormErrors,
+ FormStatus,
+ FormValues,
+ RecursivePartial,
+ UIField,
+ useFormState,
+} from "../../hooks/form.js";
interface Props {
routeMyAccountDetails: RouteDefinition;
@@ -53,11 +66,12 @@ interface Props {
onUpdateSuccess: () => void;
}
-type FormType = { amount: AmountJson, conv: TalerBankConversionApi.ConversionRate }
-
+type FormType = {
+ amount: AmountJson;
+ conv: TalerBankConversionApi.ConversionRate;
+};
function useComponentState({
- onUpdateSuccess,
routeCancel,
routeConversionConfig,
routeMyAccountCashout,
@@ -67,9 +81,11 @@ function useComponentState({
}: Props): utils.RecursiveState<VNode> {
const { i18n } = useTranslationContext();
- const result = useConversionInfo()
- const info = result && !(result instanceof TalerError) && result.type === "ok" ?
- result.body : undefined;
+ const result = useConversionInfo();
+ const info =
+ result && !(result instanceof TalerError) && result.type === "ok"
+ ? result.body
+ : undefined;
const { state: credentials } = useSessionState();
const creds =
@@ -78,17 +94,17 @@ function useComponentState({
: credentials;
if (!info) {
- return <i18n.Translate>loading...</i18n.Translate>
+ return <i18n.Translate>loading...</i18n.Translate>;
}
if (!creds) {
- return <i18n.Translate>only admin can setup conversion</i18n.Translate>
+ return <i18n.Translate>only admin can setup conversion</i18n.Translate>;
}
- return () => {
+ return function afterComponentLoads() {
const { i18n } = useTranslationContext();
- const { bank, conversion, config } = useBankCoreApiContext();
+ const { conversion } = useBankCoreApiContext();
const [notification, notify, handleError] = useLocalNotification();
@@ -96,66 +112,91 @@ function useComponentState({
amount: "100",
conv: {
cashin_min_amount: info.conversion_rate.cashin_min_amount.split(":")[1],
- cashin_tiny_amount: info.conversion_rate.cashin_tiny_amount.split(":")[1],
+ cashin_tiny_amount:
+ info.conversion_rate.cashin_tiny_amount.split(":")[1],
cashin_fee: info.conversion_rate.cashin_fee.split(":")[1],
cashin_ratio: info.conversion_rate.cashin_ratio,
cashin_rounding_mode: info.conversion_rate.cashin_rounding_mode,
- cashout_min_amount: info.conversion_rate.cashout_min_amount.split(":")[1],
- cashout_tiny_amount: info.conversion_rate.cashout_tiny_amount.split(":")[1],
+ cashout_min_amount:
+ info.conversion_rate.cashout_min_amount.split(":")[1],
+ cashout_tiny_amount:
+ info.conversion_rate.cashout_tiny_amount.split(":")[1],
cashout_fee: info.conversion_rate.cashout_fee.split(":")[1],
cashout_ratio: info.conversion_rate.cashout_ratio,
cashout_rounding_mode: info.conversion_rate.cashout_rounding_mode,
- }
- }
+ },
+ };
const [form, status] = useFormState<FormType>(
initalState,
- createFormValidator(i18n, info.regional_currency, info.fiat_currency)
- )
+ createFormValidator(i18n, info.regional_currency, info.fiat_currency),
+ );
- const {
- estimateByDebit: calculateCashoutFromDebit,
- } = useCashoutEstimator();
+ const { estimateByDebit: calculateCashoutFromDebit } =
+ useCashoutEstimator();
- const {
- estimateByDebit: calculateCashinFromDebit,
- } = useCashinEstimator();
+ const { estimateByDebit: calculateCashinFromDebit } = useCashinEstimator();
- const [calculationResult, setCalc] = useState<{ cashin: TransferCalculation, cashout: TransferCalculation }>()
+ const [calculationResult, setCalc] = useState<{
+ cashin: TransferCalculation;
+ cashout: TransferCalculation;
+ }>();
useEffect(() => {
async function doAsync() {
await handleError(async () => {
if (!info) return;
if (!form.amount?.value || form.amount.error) return;
- const in_amount = Amounts.parseOrThrow(`${info.fiat_currency}:${form.amount.value}`)
- const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee)
+ const in_amount = Amounts.parseOrThrow(
+ `${info.fiat_currency}:${form.amount.value}`,
+ );
+ const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee);
const cashin = await calculateCashinFromDebit(in_amount, in_fee);
if (cashin === "amount-is-too-small") {
- setCalc(undefined)
+ setCalc(undefined);
return;
}
// const out_amount = Amounts.parseOrThrow(`${info.regional_currency}:${form.amount.value}`)
- const out_fee = Amounts.parseOrThrow(info.conversion_rate.cashout_fee)
- const cashout = await calculateCashoutFromDebit(cashin.credit, out_fee);
+ const out_fee = Amounts.parseOrThrow(
+ info.conversion_rate.cashout_fee,
+ );
+ const cashout = await calculateCashoutFromDebit(
+ cashin.credit,
+ out_fee,
+ );
setCalc({ cashin, cashout });
});
}
doAsync();
- }, [form.amount?.value, form.conv?.cashin_fee?.value, form.conv?.cashout_fee?.value]);
-
- const [section, setSection] = useState<"detail" | "cashout" | "cashin">("detail")
- const cashinCalc = calculationResult?.cashin === "amount-is-too-small" ? undefined : calculationResult?.cashin
- const cashoutCalc = calculationResult?.cashout === "amount-is-too-small" ? undefined : calculationResult?.cashout
+ }, [
+ form.amount?.value,
+ form.conv?.cashin_fee?.value,
+ form.conv?.cashout_fee?.value,
+ ]);
+
+ const [section, setSection] = useState<"detail" | "cashout" | "cashin">(
+ "detail",
+ );
+ const cashinCalc =
+ calculationResult?.cashin === "amount-is-too-small"
+ ? undefined
+ : calculationResult?.cashin;
+ const cashoutCalc =
+ calculationResult?.cashout === "amount-is-too-small"
+ ? undefined
+ : calculationResult?.cashout;
async function doUpdate() {
- if (!creds) return
+ if (!creds) return;
await handleError(async () => {
if (status.status === "fail") return;
- const resp = await conversion.updateConversionRate(creds.token, status.result.conv)
+ const resp = await conversion.updateConversionRate(
+ creds.token,
+ status.result.conv,
+ );
if (resp.type === "ok") {
- setSection("detail")
+ setSection("detail");
} else {
switch (resp.case) {
case HttpStatusCode.Unauthorized: {
@@ -164,6 +205,7 @@ function useComponentState({
title: i18n.str`Wrong credentials`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
}
case HttpStatusCode.NotImplemented: {
@@ -172,6 +214,7 @@ function useComponentState({
title: i18n.str`Conversion is disabled`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
}
default:
@@ -181,16 +224,16 @@ function useComponentState({
});
}
- const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio)
- const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio)
+ const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio);
+ const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio);
const both_high = in_ratio > 1 && out_ratio > 1;
const both_low = in_ratio < 1 && out_ratio < 1;
-
return (
<div>
- <ProfileNavigation current="conversion"
+ <ProfileNavigation
+ current="conversion"
routeMyAccountCashout={routeMyAccountCashout}
routeMyAccountDelete={routeMyAccountDelete}
routeMyAccountDetails={routeMyAccountDetails}
@@ -200,7 +243,6 @@ function useComponentState({
<LocalNotificationBanner notification={notification} />
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
-
<div class="px-4 sm:px-0">
<h2 class="text-base font-semibold leading-7 text-gray-900">
<i18n.Translate>Conversion</i18n.Translate>
@@ -218,7 +260,7 @@ function useComponentState({
aria-labelledby="project-type-0-label"
aria-describedby="project-type-0-description-0 project-type-0-description-1"
onChange={() => {
- setSection("detail")
+ setSection("detail");
}}
/>
<span class="flex flex-1">
@@ -242,7 +284,7 @@ function useComponentState({
aria-labelledby="project-type-1-label"
aria-describedby="project-type-1-description-0 project-type-1-description-1"
onChange={() => {
- setSection("cashout")
+ setSection("cashout");
}}
/>
<span class="flex flex-1">
@@ -265,7 +307,7 @@ function useComponentState({
aria-labelledby="project-type-1-label"
aria-describedby="project-type-1-description-0 project-type-1-description-1"
onChange={() => {
- setSection("cashin")
+ setSection("cashin");
}}
/>
<span class="flex flex-1">
@@ -277,7 +319,6 @@ function useComponentState({
</span>
</label>
</div>
-
</div>
<form
@@ -288,8 +329,9 @@ function useComponentState({
e.preventDefault();
}}
>
- {section == "cashin" &&
- <ConversionForm id="cashin"
+ {section == "cashin" && (
+ <ConversionForm
+ id="cashin"
inputCurrency={info.fiat_currency}
outputCurrency={info.regional_currency}
fee={form?.conv?.cashin_fee}
@@ -297,682 +339,830 @@ function useComponentState({
ratio={form?.conv?.cashin_ratio}
rounding={form?.conv?.cashin_rounding_mode}
tiny={form?.conv?.cashin_tiny_amount}
- />}
-
- {section == "cashout" && <Fragment>
- <ConversionForm id="cashout"
- inputCurrency={info.regional_currency}
- outputCurrency={info.fiat_currency}
- fee={form?.conv?.cashout_fee}
- minimum={form?.conv?.cashout_min_amount}
- ratio={form?.conv?.cashout_ratio}
- rounding={form?.conv?.cashout_rounding_mode}
- tiny={form?.conv?.cashout_tiny_amount}
/>
- </Fragment>}
-
- {section == "detail" && <Fragment>
- <div class="px-6 pt-6">
- <div class="justify-between items-center flex ">
- <dt class="text-sm text-gray-600">
- <i18n.Translate>Cashin ratio</i18n.Translate>
- </dt>
- <dd class="text-sm text-gray-900">
- {info.conversion_rate.cashin_ratio}
- </dd>
- </div>
- </div>
+ )}
+
+ {section == "cashout" && (
+ <Fragment>
+ <ConversionForm
+ id="cashout"
+ inputCurrency={info.regional_currency}
+ outputCurrency={info.fiat_currency}
+ fee={form?.conv?.cashout_fee}
+ minimum={form?.conv?.cashout_min_amount}
+ ratio={form?.conv?.cashout_ratio}
+ rounding={form?.conv?.cashout_rounding_mode}
+ tiny={form?.conv?.cashout_tiny_amount}
+ />
+ </Fragment>
+ )}
- <div class="px-6 pt-6">
- <div class="justify-between items-center flex ">
- <dt class="text-sm text-gray-600">
- <i18n.Translate>Cashout ratio</i18n.Translate>
- </dt>
- <dd class="text-sm text-gray-900">
- {info.conversion_rate.cashout_ratio}
- </dd>
+ {section == "detail" && (
+ <Fragment>
+ <div class="px-6 pt-6">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Cashin ratio</i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ {info.conversion_rate.cashin_ratio}
+ </dd>
+ </div>
</div>
- </div>
- {both_low || both_high ? <div class="p-4">
- <Attention title={i18n.str`Bad ratios`} type="warning">
- <i18n.Translate>
- One of the ratios should be higher or equal than 1 an the other should be lower or equal than 1.
- </i18n.Translate>
- </Attention>
- </div> : undefined}
-
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- for="amount"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Initial amount`}</label>
- <InputAmount
- name="amount"
- left
- currency={info.fiat_currency}
- value={form.amount?.value ?? ""}
- onChange={form.amount?.onUpdate}
- />
- <ShowInputErrorLabel
- message={form.amount?.error}
- isDirty={form.amount?.value !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Use it to test how the conversion will affect the amount.</i18n.Translate>
- </p>
+ <div class="px-6 pt-6">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Cashout ratio</i18n.Translate>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ {info.conversion_rate.cashout_ratio}
+ </dd>
</div>
</div>
- </div>
- {!cashoutCalc || !cashinCalc ? undefined : (
+ {both_low || both_high ? (
+ <div class="p-4">
+ <Attention title={i18n.str`Bad ratios`} type="warning">
+ <i18n.Translate>
+ One of the ratios should be higher or equal than 1 an
+ the other should be lower or equal than 1.
+ </i18n.Translate>
+ </Attention>
+ </div>
+ ) : undefined}
+
<div class="px-6 pt-6">
- <div class="sm:col-span-5">
- <dl class="mt-4 space-y-4">
- <div class="justify-between items-center flex ">
- <dt class="text-sm text-gray-600">
- <i18n.Translate>Sending to this bank</i18n.Translate>
- </dt>
- <dd class="text-sm text-gray-900">
- <RenderAmount
- value={cashinCalc.debit}
- negative
- withColor
- spec={info.regional_currency_specification}
- />
- </dd>
- </div>
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for="amount"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Initial amount`}</label>
+ <InputAmount
+ name="amount"
+ left
+ currency={info.fiat_currency}
+ value={form.amount?.value ?? ""}
+ onChange={form.amount?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={form.amount?.error}
+ isDirty={form.amount?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Use it to test how the conversion will affect the
+ amount.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
- {Amounts.isZero(cashinCalc.beforeFee) ? undefined : (
- <div class="flex items-center justify-between afu ">
- <dt class="flex items-center text-sm text-gray-600">
- <span>
- <i18n.Translate>Converted</i18n.Translate>
- </span>
+ {!cashoutCalc || !cashinCalc ? undefined : (
+ <div class="px-6 pt-6">
+ <div class="sm:col-span-5">
+ <dl class="mt-4 space-y-4">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>
+ Sending to this bank
+ </i18n.Translate>
</dt>
<dd class="text-sm text-gray-900">
<RenderAmount
- value={cashinCalc.beforeFee}
- spec={info.fiat_currency_specification}
+ value={cashinCalc.debit}
+ negative
+ withColor
+ spec={info.regional_currency_specification}
/>
</dd>
</div>
- )}
- <div class="flex justify-between items-center border-t-2 afu pt-4">
- <dt class="text-lg text-gray-900 font-medium">
- <i18n.Translate>Cashin after fee</i18n.Translate>
- </dt>
- <dd class="text-lg text-gray-900 font-medium">
- <RenderAmount
- value={cashinCalc.credit}
- withColor
- spec={info.fiat_currency_specification}
- />
- </dd>
- </div>
- </dl>
- </div>
-
- <div class="sm:col-span-5">
- <dl class="mt-4 space-y-4">
- <div class="justify-between items-center flex ">
- <dt class="text-sm text-gray-600">
- <i18n.Translate>Sending from this bank</i18n.Translate>
- </dt>
- <dd class="text-sm text-gray-900">
- <RenderAmount
- value={cashoutCalc.debit}
- negative
- withColor
- spec={info.fiat_currency_specification}
- />
- </dd>
- </div>
- {Amounts.isZero(cashoutCalc.beforeFee) ? undefined : (
- <div class="flex items-center justify-between afu">
- <dt class="flex items-center text-sm text-gray-600">
- <span>
- <i18n.Translate>Converted</i18n.Translate>
- </span>
+ {Amounts.isZero(cashinCalc.beforeFee) ? undefined : (
+ <div class="flex items-center justify-between afu ">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Converted</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashinCalc.beforeFee}
+ spec={info.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-lg text-gray-900 font-medium">
+ <i18n.Translate>Cashin after fee</i18n.Translate>
+ </dt>
+ <dd class="text-lg text-gray-900 font-medium">
+ <RenderAmount
+ value={cashinCalc.credit}
+ withColor
+ spec={info.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ </dl>
+ </div>
+
+ <div class="sm:col-span-5">
+ <dl class="mt-4 space-y-4">
+ <div class="justify-between items-center flex ">
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>
+ Sending from this bank
+ </i18n.Translate>
</dt>
<dd class="text-sm text-gray-900">
<RenderAmount
- value={cashoutCalc.beforeFee}
+ value={cashoutCalc.debit}
+ negative
+ withColor
+ spec={info.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+
+ {Amounts.isZero(cashoutCalc.beforeFee) ? undefined : (
+ <div class="flex items-center justify-between afu">
+ <dt class="flex items-center text-sm text-gray-600">
+ <span>
+ <i18n.Translate>Converted</i18n.Translate>
+ </span>
+ </dt>
+ <dd class="text-sm text-gray-900">
+ <RenderAmount
+ value={cashoutCalc.beforeFee}
+ spec={info.regional_currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ <div class="flex justify-between items-center border-t-2 afu pt-4">
+ <dt class="text-lg text-gray-900 font-medium">
+ <i18n.Translate>Cashout after fee</i18n.Translate>
+ </dt>
+ <dd class="text-lg text-gray-900 font-medium">
+ <RenderAmount
+ value={cashoutCalc.credit}
+ withColor
spec={info.regional_currency_specification}
/>
</dd>
</div>
- )}
- <div class="flex justify-between items-center border-t-2 afu pt-4">
- <dt class="text-lg text-gray-900 font-medium">
- <i18n.Translate>Cashout after fee</i18n.Translate>
- </dt>
- <dd class="text-lg text-gray-900 font-medium">
- <RenderAmount
- value={cashoutCalc.credit}
- withColor
- spec={info.regional_currency_specification}
- />
- </dd>
+ </dl>
+ </div>
+
+ {cashoutCalc &&
+ status.status === "ok" &&
+ Amounts.cmp(status.result.amount, cashoutCalc.credit) <
+ 0 ? (
+ <div class="p-4">
+ <Attention
+ title={i18n.str`Bad configuration`}
+ type="warning"
+ >
+ <i18n.Translate>
+ This configuration allows users to cash out more of
+ what has been cashed in.
+ </i18n.Translate>
+ </Attention>
</div>
- </dl>
+ ) : undefined}
</div>
-
- {cashoutCalc && status.status === "ok" && Amounts.cmp(status.result.amount, cashoutCalc.credit) < 0 ? <div class="p-4">
- <Attention title={i18n.str`Bad configuration`} type="warning">
- <i18n.Translate>
- This configuration allows users to cash out more of what has been cashed in.
- </i18n.Translate>
- </Attention>
- </div> : undefined}
- </div>
- )}
- </Fragment>}
-
+ )}
+ </Fragment>
+ )}
<div class="flex items-center justify-between mt-4 gap-x-6 border-t border-gray-900/10 px-4 py-4">
- <a name="cancel"
+ <a
+ name="cancel"
href={routeCancel.url({})}
class="text-sm font-semibold leading-6 text-gray-900"
>
<i18n.Translate>Cancel</i18n.Translate>
</a>
- {section == "cashin" || section == "cashout" ? <Fragment>
- <button
- type="submit"
- name="update conversion"
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={async () => {
- doUpdate()
- }}
- >
- <i18n.Translate>Update</i18n.Translate>
- </button>
- </Fragment> : <div />}
+ {section == "cashin" || section == "cashout" ? (
+ <Fragment>
+ <button
+ type="submit"
+ name="update conversion"
+ class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
+ onClick={async () => {
+ doUpdate();
+ }}
+ >
+ <i18n.Translate>Update</i18n.Translate>
+ </button>
+ </Fragment>
+ ) : (
+ <div />
+ )}
</div>
-
-
</form>
</div>
</div>
);
-
- }
+ };
}
export const ConversionConfig = utils.recursive(useComponentState);
/**
- *
- * @param i18n
- * @param regional
- * @param fiat
+ *
+ * @param i18n
+ * @param regional
+ * @param fiat
* @returns form validator
*/
-function createFormValidator(i18n: InternationalizationAPI, regional: string, fiat: string) {
+function createFormValidator(
+ i18n: InternationalizationAPI,
+ regional: string,
+ fiat: string,
+) {
return function check(state: FormValues<FormType>): FormStatus<FormType> {
+ const cashin_min_amount = Amounts.parse(
+ `${fiat}:${state.conv.cashin_min_amount}`,
+ );
+ const cashin_tiny_amount = Amounts.parse(
+ `${regional}:${state.conv.cashin_tiny_amount}`,
+ );
+ const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`);
- const cashin_min_amount = Amounts.parse(`${fiat}:${state.conv.cashin_min_amount}`)
- const cashin_tiny_amount = Amounts.parse(`${regional}:${state.conv.cashin_tiny_amount}`)
- const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`)
-
- const cashout_min_amount = Amounts.parse(`${regional}:${state.conv.cashout_min_amount}`)
- const cashout_tiny_amount = Amounts.parse(`${fiat}:${state.conv.cashout_tiny_amount}`)
- const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`)
+ const cashout_min_amount = Amounts.parse(
+ `${regional}:${state.conv.cashout_min_amount}`,
+ );
+ const cashout_tiny_amount = Amounts.parse(
+ `${fiat}:${state.conv.cashout_tiny_amount}`,
+ );
+ const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`);
- const am = Amounts.parse(`${fiat}:${state.amount}`)
+ const am = Amounts.parse(`${fiat}:${state.amount}`);
- const cashin_ratio = Number.parseFloat(state.conv.cashin_ratio ?? "")
- const cashout_ratio = Number.parseFloat(state.conv.cashout_ratio ?? "")
+ const cashin_ratio = Number.parseFloat(state.conv.cashin_ratio ?? "");
+ const cashout_ratio = Number.parseFloat(state.conv.cashout_ratio ?? "");
const errors = undefinedIfEmpty<FormErrors<FormType>>({
conv: undefinedIfEmpty<FormErrors<FormType["conv"]>>({
- cashin_min_amount: !state.conv.cashin_min_amount ? i18n.str`required` :
- !cashin_min_amount ? i18n.str`invalid` :
- undefined,
- cashin_tiny_amount: !state.conv.cashin_tiny_amount ? i18n.str`required` :
- !cashin_tiny_amount ? i18n.str`invalid` :
- undefined,
- cashin_fee: !state.conv.cashin_fee ? i18n.str`required` :
- !cashin_fee ? i18n.str`invalid` :
- undefined,
-
- cashout_min_amount: !state.conv.cashout_min_amount ? i18n.str`required` :
- !cashout_min_amount ? i18n.str`invalid` :
- undefined,
- cashout_tiny_amount: !state.conv.cashin_tiny_amount ? i18n.str`required` :
- !cashout_tiny_amount ? i18n.str`invalid` :
- undefined,
- cashout_fee: !state.conv.cashin_fee ? i18n.str`required` :
- !cashout_fee ? i18n.str`invalid` :
- undefined,
-
- cashin_rounding_mode: !state.conv.cashin_rounding_mode ? i18n.str`required` : undefined,
- cashout_rounding_mode: !state.conv.cashout_rounding_mode ? i18n.str`required` : undefined,
-
- cashin_ratio: !state.conv.cashin_ratio ? i18n.str`required` : Number.isNaN(cashin_ratio) ? i18n.str`invalid` : undefined,
- cashout_ratio: !state.conv.cashout_ratio ? i18n.str`required` : Number.isNaN(cashout_ratio) ? i18n.str`invalid` : undefined,
+ cashin_min_amount: !state.conv.cashin_min_amount
+ ? i18n.str`required`
+ : !cashin_min_amount
+ ? i18n.str`invalid`
+ : undefined,
+ cashin_tiny_amount: !state.conv.cashin_tiny_amount
+ ? i18n.str`required`
+ : !cashin_tiny_amount
+ ? i18n.str`invalid`
+ : undefined,
+ cashin_fee: !state.conv.cashin_fee
+ ? i18n.str`required`
+ : !cashin_fee
+ ? i18n.str`invalid`
+ : undefined,
+
+ cashout_min_amount: !state.conv.cashout_min_amount
+ ? i18n.str`required`
+ : !cashout_min_amount
+ ? i18n.str`invalid`
+ : undefined,
+ cashout_tiny_amount: !state.conv.cashin_tiny_amount
+ ? i18n.str`required`
+ : !cashout_tiny_amount
+ ? i18n.str`invalid`
+ : undefined,
+ cashout_fee: !state.conv.cashin_fee
+ ? i18n.str`required`
+ : !cashout_fee
+ ? i18n.str`invalid`
+ : undefined,
+
+ cashin_rounding_mode: !state.conv.cashin_rounding_mode
+ ? i18n.str`required`
+ : undefined,
+ cashout_rounding_mode: !state.conv.cashout_rounding_mode
+ ? i18n.str`required`
+ : undefined,
+
+ cashin_ratio: !state.conv.cashin_ratio
+ ? i18n.str`required`
+ : Number.isNaN(cashin_ratio)
+ ? i18n.str`invalid`
+ : undefined,
+ cashout_ratio: !state.conv.cashout_ratio
+ ? i18n.str`required`
+ : Number.isNaN(cashout_ratio)
+ ? i18n.str`invalid`
+ : undefined,
}),
- amount: !state.amount ? i18n.str`required` :
- !am ? i18n.str`invalid` :
- undefined,
- })
+ amount: !state.amount
+ ? i18n.str`required`
+ : !am
+ ? i18n.str`invalid`
+ : undefined,
+ });
const result: RecursivePartial<FormType> = {
amount: am,
conv: {
- cashin_fee: !errors?.conv?.cashin_fee ? Amounts.stringify(cashin_fee!) : undefined,
- cashin_min_amount: !errors?.conv?.cashin_min_amount ? Amounts.stringify(cashin_min_amount!) : undefined,
- cashin_ratio: !errors?.conv?.cashin_ratio ? String(cashin_ratio!) : undefined,
- cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode ? (state.conv.cashin_rounding_mode!) : undefined,
- cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount ? Amounts.stringify(cashin_tiny_amount!) : undefined,
- cashout_fee: !errors?.conv?.cashout_fee ? Amounts.stringify(cashout_fee!) : undefined,
- cashout_min_amount: !errors?.conv?.cashout_min_amount ? Amounts.stringify(cashout_min_amount!) : undefined,
- cashout_ratio: !errors?.conv?.cashout_ratio ? String(cashout_ratio!) : undefined,
- cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode ? (state.conv.cashout_rounding_mode!) : undefined,
- cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount ? Amounts.stringify(cashout_tiny_amount!) : undefined,
- }
-
- }
- return errors === undefined ?
- { status: "ok", result: result as FormType, errors } :
- { status: "fail", result, errors }
- }
+ cashin_fee: !errors?.conv?.cashin_fee
+ ? Amounts.stringify(cashin_fee!)
+ : undefined,
+ cashin_min_amount: !errors?.conv?.cashin_min_amount
+ ? Amounts.stringify(cashin_min_amount!)
+ : undefined,
+ cashin_ratio: !errors?.conv?.cashin_ratio
+ ? String(cashin_ratio!)
+ : undefined,
+ cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode
+ ? state.conv.cashin_rounding_mode!
+ : undefined,
+ cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount
+ ? Amounts.stringify(cashin_tiny_amount!)
+ : undefined,
+ cashout_fee: !errors?.conv?.cashout_fee
+ ? Amounts.stringify(cashout_fee!)
+ : undefined,
+ cashout_min_amount: !errors?.conv?.cashout_min_amount
+ ? Amounts.stringify(cashout_min_amount!)
+ : undefined,
+ cashout_ratio: !errors?.conv?.cashout_ratio
+ ? String(cashout_ratio!)
+ : undefined,
+ cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode
+ ? state.conv.cashout_rounding_mode!
+ : undefined,
+ cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount
+ ? Amounts.stringify(cashout_tiny_amount!)
+ : undefined,
+ },
+ };
+ return errors === undefined
+ ? { status: "ok", result: result as FormType, errors }
+ : { status: "fail", result, errors };
+ };
}
-
-function ConversionForm({ id, inputCurrency, outputCurrency, fee, minimum, ratio, rounding, tiny }: {
- inputCurrency: string,
- outputCurrency: string,
- minimum: UIField | undefined,
- tiny: UIField | undefined,
- fee: UIField | undefined,
- rounding: UIField | undefined,
- ratio: UIField | undefined,
- id: string,
+function ConversionForm({
+ id,
+ inputCurrency,
+ outputCurrency,
+ fee,
+ minimum,
+ ratio,
+ rounding,
+ tiny,
+}: {
+ inputCurrency: string;
+ outputCurrency: string;
+ minimum: UIField | undefined;
+ tiny: UIField | undefined;
+ fee: UIField | undefined;
+ rounding: UIField | undefined;
+ ratio: UIField | undefined;
+ id: string;
}): VNode {
const { i18n } = useTranslationContext();
- return <Fragment>
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- for="cashin_min_amount"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Minimum amount`}</label>
- <InputAmount
- name="cashin_min_amount"
- left
- currency={inputCurrency}
- value={minimum?.value ?? ""}
- onChange={minimum?.onUpdate}
+ return (
+ <Fragment>
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for={`${id}_min_amount`}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Minimum amount`}</label>
+ <InputAmount
+ name={`${id}_min_amount`}
+ left
+ currency={inputCurrency}
+ value={minimum?.value ?? ""}
+ onChange={minimum?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={minimum?.error}
+ isDirty={minimum?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Only cashout operation above this threshold will be allowed
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="px-6 pt-6">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for={`${id}_ratio`}
+ >
+ {i18n.str`Ratio`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="number"
+ class="block rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="current"
+ id={`${id}_ratio`}
+ data-error={!!ratio?.error && ratio?.value !== undefined}
+ value={ratio?.value ?? ""}
+ onChange={(e) => {
+ ratio?.onUpdate(e.currentTarget.value);
+ }}
+ autocomplete="off"
/>
<ShowInputErrorLabel
- message={minimum?.error}
- isDirty={minimum?.value !== undefined}
+ message={ratio?.error}
+ isDirty={ratio?.value !== undefined}
/>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Only cashout operation above this threshold will be allowed</i18n.Translate>
- </p>
</div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>Conversion ratio between currencies</i18n.Translate>
+ </p>
</div>
- </div>
-
- <div class="px-6 pt-6">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="password"
- >
- {i18n.str`Ratio`}
- </label>
- <div class="mt-2">
- <input
- type="number"
- class="block rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="current"
- id="cashin_ratio"
- data-error={!!ratio?.error && ratio?.value !== undefined}
- value={ratio?.value ?? ""}
- onChange={(e) => {
- ratio?.onUpdate(e.currentTarget.value);
- }}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={ratio?.error}
- isDirty={ratio?.value !== undefined}
- />
+
+ <div class="px-6 pt-4">
+ <Attention title={i18n.str`Example conversion`}>
+ <i18n.Translate>
+ 1 {inputCurrency} will be converted into {ratio?.value}{" "}
+ {outputCurrency}
+ </i18n.Translate>
+ </Attention>
</div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>
- Conversion ratio between currencies
- </i18n.Translate>
- </p>
- </div>
-
- <div class="px-6 pt-4">
- <Attention title={i18n.str`Example conversion`}>
- <i18n.Translate>1 {inputCurrency} will be converted into {ratio?.value} {outputCurrency}</i18n.Translate>
- </Attention>
- </div>
-
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- for="cashin_tiny_amount"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Rounding value`}</label>
- <InputAmount
- name="cashin_tiny_amount"
- left
- currency={outputCurrency}
- value={tiny?.value ?? ""}
- onChange={tiny?.onUpdate}
- />
- <ShowInputErrorLabel
- message={tiny?.error}
- isDirty={tiny?.value !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Smallest difference between two amounts after the ratio is applied.</i18n.Translate>
- </p>
+
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for={`${id}_tiny_amount`}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Rounding value`}</label>
+ <InputAmount
+ name={`${id}_tiny_amount`}
+ left
+ currency={outputCurrency}
+ value={tiny?.value ?? ""}
+ onChange={tiny?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={tiny?.error}
+ isDirty={tiny?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Smallest difference between two amounts after the ratio is
+ applied.
+ </i18n.Translate>
+ </p>
+ </div>
</div>
</div>
- </div>
-
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="channel"
- >
- {i18n.str`Rounding mode`}
- </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={(e) => {
- e.preventDefault();
- rounding?.onUpdate("zero")
- }}
- data-selected={rounding?.value === "zero"}
- class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 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>Zero</i18n.Translate>
+
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for={`${id}_channel`}
+ >
+ {i18n.str`Rounding mode`}
+ </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={(e) => {
+ e.preventDefault();
+ rounding?.onUpdate("zero");
+ }}
+ data-selected={rounding?.value === "zero"}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 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 class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Zero</i18n.Translate>
+ </span>
+ <i18n.Translate>
+ Amount will be round below to the largest possible value
+ smaller than the input.
+ </i18n.Translate>
</span>
- <i18n.Translate>Amount will be round below to the largest possible value smaller than the input.</i18n.Translate>
</span>
- </span>
+ <svg
+ data-selected={rounding?.value === "zero"}
+ 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={(e) => {
+ e.preventDefault();
+ rounding?.onUpdate("up");
+ }}
+ data-selected={rounding?.value === "up"}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 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 class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Up</i18n.Translate>
+ </span>
+ <i18n.Translate>
+ Amount will be round up to the smallest possible value
+ larger than the input.
+ </i18n.Translate>
+ </span>
+ </span>
+ <svg
+ data-selected={rounding?.value === "up"}
+ 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={(e) => {
+ e.preventDefault();
+ rounding?.onUpdate("nearest");
+ }}
+ data-selected={rounding?.value === "nearest"}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 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 class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Nearest</i18n.Translate>
+ </span>
+ <i18n.Translate>
+ Amount will be round to the closest possible value.
+ </i18n.Translate>
+ </span>
+ </span>
+ <svg
+ data-selected={rounding?.value === "nearest"}
+ 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>
+
+ <div class="px-6 pt-4">
+ <Attention title={i18n.str`Examples`}>
+ <section class="grid grid-cols-1 gap-y-3 text-gray-600">
+ <details class="group text-sm">
+ <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <i18n.Translate>
+ Rounding an amount of 1.24 with rounding value 0.1
+ </i18n.Translate>
<svg
- data-selected={rounding?.value === "zero"}
- class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
- viewBox="0 0 20 20"
- fill="currentColor"
+ class="h-6 w-6 rotate-0 transform group-open:rotate-180"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="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"
- />
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19 9l-7 7-7-7"
+ ></path>
</svg>
- </label>
-
- <label
- onClick={(e) => {
- e.preventDefault();
- rounding?.onUpdate("up")
- }}
- data-selected={rounding?.value === "up"}
- class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 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-0-label"
- class="block text-sm font-medium text-gray-900 "
- >
- <i18n.Translate>Up</i18n.Translate>
- </span>
- <i18n.Translate>Amount will be round up to the smallest possible value larger than the input.</i18n.Translate>
- </span>
- </span>
+ </summary>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ Given the rounding value of 0.1 the possible values closest to
+ 1.24 are: 1.1, 1.2, 1.3, 1.4.
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "zero" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "nearest" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 mt-4">
+ <i18n.Translate>
+ With the "up" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ </details>
+ <details class="group ">
+ <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <i18n.Translate>
+ Rounding an amount of 1.26 with rounding value 0.1
+ </i18n.Translate>
<svg
- data-selected={rounding?.value === "up"}
- class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
- viewBox="0 0 20 20"
- fill="currentColor"
+ class="h-6 w-6 rotate-0 transform group-open:rotate-180"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="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"
- />
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19 9l-7 7-7-7"
+ ></path>
</svg>
- </label>
- <label
- onClick={(e) => {
- e.preventDefault();
- rounding?.onUpdate("nearest")
- }}
- data-selected={rounding?.value === "nearest"}
- class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 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-0-label"
- class="block text-sm font-medium text-gray-900 "
- >
- <i18n.Translate>Nearest</i18n.Translate>
- </span>
- <i18n.Translate>Amount will be round to the closest possible value.</i18n.Translate>
- </span>
- </span>
+ </summary>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ Given the rounding value of 0.1 the possible values closest to
+ 1.24 are: 1.1, 1.2, 1.3, 1.4.
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "zero" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "nearest" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "up" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ </details>
+ <details class="group ">
+ <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <i18n.Translate>
+ Rounding an amount of 1.24 with rounding value 0.3
+ </i18n.Translate>
<svg
- data-selected={rounding?.value === "nearest"}
- class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
- viewBox="0 0 20 20"
- fill="currentColor"
+ class="h-6 w-6 rotate-0 transform group-open:rotate-180"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="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"
- />
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19 9l-7 7-7-7"
+ ></path>
</svg>
- </label>
- </div>
- </div>
- </div>
+ </summary>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ Given the rounding value of 0.3 the possible values closest to
+ 1.24 are: 0.9, 1.2, 1.5, 1.8.
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "zero" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "nearest" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "up" mode the value will be rounded to 1.5
+ </i18n.Translate>
+ </p>
+ </details>
+ <details class="group ">
+ <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <i18n.Translate>
+ Rounding an amount of 1.26 with rounding value 0.3
+ </i18n.Translate>
+ <svg
+ class="h-6 w-6 rotate-0 transform group-open:rotate-180"
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="2"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M19 9l-7 7-7-7"
+ ></path>
+ </svg>
+ </summary>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ Given the rounding value of 0.3 the possible values closest to
+ 1.24 are: 0.9, 1.2, 1.5, 1.8.
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "zero" mode the value will be rounded to 1.2
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "nearest" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ <p class="text-gray-900 my-4">
+ <i18n.Translate>
+ With the "up" mode the value will be rounded to 1.3
+ </i18n.Translate>
+ </p>
+ </details>
+ </section>
+ </Attention>
</div>
- </div>
- <div class="px-6 pt-4">
- <Attention title={i18n.str`Examples`}>
- <section class="grid grid-cols-1 gap-y-3 text-gray-600">
- <details class="group text-sm">
- <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for={`${id}_fee`}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Fee`}</label>
+ <InputAmount
+ name={`${id}_fee`}
+ left
+ currency={outputCurrency}
+ value={fee?.value ?? ""}
+ onChange={fee?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={fee?.error}
+ isDirty={fee?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
<i18n.Translate>
- Rounding an amount of 1.24 with rounding value 0.1
- </i18n.Translate>
- <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
- </svg>
- </summary>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- Given the rounding value of 0.1 the possible values closest to 1.24 are: 1.1, 1.2, 1.3, 1.4.
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "zero" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "nearest" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 mt-4">
- <i18n.Translate>
- With the "up" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- </details>
- <details class="group ">
- <summary class="flex cursor-pointer flex-row items-center justify-between ">
- <i18n.Translate>
- Rounding an amount of 1.26 with rounding value 0.1
- </i18n.Translate>
- <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
- </svg>
- </summary>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- Given the rounding value of 0.1 the possible values closest to 1.24 are: 1.1, 1.2, 1.3, 1.4.
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "zero" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "nearest" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "up" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- </details>
- <details class="group ">
- <summary class="flex cursor-pointer flex-row items-center justify-between ">
- <i18n.Translate>
- Rounding an amount of 1.24 with rounding value 0.3
- </i18n.Translate>
- <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
- </svg>
- </summary>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- Given the rounding value of 0.3 the possible values closest to 1.24 are: 0.9, 1.2, 1.5, 1.8.
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "zero" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "nearest" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "up" mode the value will be rounded to 1.5
- </i18n.Translate>
- </p>
- </details>
- <details class="group ">
- <summary class="flex cursor-pointer flex-row items-center justify-between ">
- <i18n.Translate>
- Rounding an amount of 1.26 with rounding value 0.3
- </i18n.Translate>
- <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
- </svg>
- </summary>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- Given the rounding value of 0.3 the possible values closest to 1.24 are: 0.9, 1.2, 1.5, 1.8.
+ Amount to be deducted before amount is credited.
</i18n.Translate>
</p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "zero" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "nearest" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "up" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- </details>
- </section>
- </Attention>
- </div>
-
-
-
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- for="cashin_fee"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Fee`}</label>
- <InputAmount
- name="cashin_fee"
- left
- currency={outputCurrency}
- value={fee?.value ?? ""}
- onChange={fee?.onUpdate}
- />
- <ShowInputErrorLabel
- message={fee?.error}
- isDirty={fee?.value !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Amount to be deducted before amount is credited.</i18n.Translate>
- </p>
+ </div>
</div>
</div>
- </div>
-
- </Fragment>
+ </Fragment>
+ );
}
diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
index 2f15d16b4..a76179b4d 100644
--- a/packages/bank-ui/src/pages/regional/CreateCashout.tsx
+++ b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
@@ -39,9 +39,13 @@ import { useEffect, useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { VersionHint, useBankCoreApiContext } from "../../context/config.js";
import { useAccountDetails } from "../../hooks/account.js";
-import { useSessionState } from "../../hooks/session.js";
import { useBankState } from "../../hooks/bank-state.js";
-import { TransferCalculation, useCashoutEstimator, useConversionInfo, useEstimator } from "../../hooks/regional.js";
+import {
+ TransferCalculation,
+ useCashoutEstimator,
+ useConversionInfo,
+} from "../../hooks/regional.js";
+import { useSessionState } from "../../hooks/session.js";
import { RouteDefinition } from "../../route.js";
import { TanChannel, undefinedIfEmpty } from "../../utils.js";
import { LoginForm } from "../LoginForm.js";
@@ -141,11 +145,11 @@ export function CreateCashout({
switch (info.case) {
case HttpStatusCode.NotImplemented: {
return (
- <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 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>
);
}
@@ -185,7 +189,8 @@ export function CreateCashout({
credit: fiatZero,
beforeFee: fiatZero,
};
- const [calculationResult, setCalculation] = useState<TransferCalculation>(zeroCalc);
+ const [calculationResult, setCalculation] =
+ useState<TransferCalculation>(zeroCalc);
const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee);
const sellRate = conversionInfo.cashout_ratio;
/**
@@ -193,30 +198,33 @@ 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
}`,
);
useEffect(() => {
async function doAsync() {
await handleError(async () => {
- const higerThanMin = form.isDebit ?
- Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) === 1 : true;
- const notZero = Amounts.isNonZero(inputAmount)
+ const higerThanMin = form.isDebit
+ ? Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) === 1
+ : true;
+ const notZero = Amounts.isNonZero(inputAmount);
if (notZero && higerThanMin) {
const resp = await (form.isDebit
? calculateFromDebit(inputAmount, sellFee)
: calculateFromCredit(inputAmount, sellFee));
setCalculation(resp);
} else {
- setCalculation(zeroCalc)
+ setCalculation(zeroCalc);
}
});
}
doAsync();
}, [form.amount, form.isDebit]);
- const calc = calculationResult === "amount-is-too-small" ? zeroCalc : calculationResult
+ const calc =
+ calculationResult === "amount-is-too-small" ? zeroCalc : calculationResult;
const balanceAfter = Amounts.sub(account.balance, calc.debit).amount;
@@ -231,8 +239,14 @@ export function CreateCashout({
? i18n.str`Invalid`
: Amounts.cmp(limit, calc.debit) === -1
? i18n.str`Balance is not enough`
- : form.isDebit && Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) < 1
- ? i18n.str`Needs to be higher than ${Amounts.stringifyValueWithSpec(Amounts.parseOrThrow(conversionInfo.cashout_min_amount), regional_currency_specification).normal}`
+ : form.isDebit &&
+ Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) < 1
+ ? i18n.str`Needs to be higher than ${
+ Amounts.stringifyValueWithSpec(
+ Amounts.parseOrThrow(conversionInfo.cashout_min_amount),
+ regional_currency_specification,
+ ).normal
+ }`
: calculationResult === "amount-is-too-small"
? i18n.str`Amount needs to be higher`
: Amounts.isZero(calc.credit)
@@ -280,6 +294,7 @@ export function CreateCashout({
title: i18n.str`Account not found`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
return notify({
@@ -287,6 +302,7 @@ export function CreateCashout({
title: i18n.str`Duplicated request detected, check if the operation succeeded or try again.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_BAD_CONVERSION:
return notify({
@@ -294,6 +310,7 @@ export function CreateCashout({
title: i18n.str`The conversion rate was incorrectly applied`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_UNALLOWED_DEBIT:
return notify({
@@ -301,6 +318,7 @@ export function CreateCashout({
title: i18n.str`The account does not have sufficient funds`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case HttpStatusCode.NotImplemented:
return notify({
@@ -308,6 +326,7 @@ export function CreateCashout({
title: i18n.str`Cashout are disabled`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
return notify({
@@ -315,6 +334,7 @@ export function CreateCashout({
title: i18n.str`Missing cashout URI in the profile`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
return notify({
@@ -322,6 +342,7 @@ export function CreateCashout({
title: i18n.str`Sending the confirmation message failed, retry later or contact the administrator.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
+ when: AbsoluteTime.now(),
});
}
assertUnreachable(resp);
@@ -406,7 +427,10 @@ export function CreateCashout({
<dd class="text-sm text-gray-900">{cashoutLegalName}</dd>
</div>
<p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>If this name doesn't match the account holder's name your transaction may fail.</i18n.Translate>
+ <i18n.Translate>
+ If this name doesn't match the account holder's name your
+ transaction may fail.
+ </i18n.Translate>
</p>
</Fragment>
) : (
@@ -482,7 +506,7 @@ export function CreateCashout({
updateForm(structuredClone(form));
}}
>
- {form.isDebit ?
+ {form.isDebit ? (
<svg
class="self-center flex-none h-5 w-5 text-indigo-600"
viewBox="0 0 20 20"
@@ -495,12 +519,17 @@ export function CreateCashout({
clip-rule="evenodd"
/>
</svg>
-
- :
- <svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
+ ) : (
+ <svg
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-5 h-5"
+ >
<path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
- }
+ )}
<i18n.Translate>Send {regional_currency}</i18n.Translate>
</button>
@@ -514,7 +543,7 @@ export function CreateCashout({
updateForm(structuredClone(form));
}}
>
- {!form.isDebit ?
+ {!form.isDebit ? (
<svg
class="self-center flex-none h-5 w-5 text-indigo-600"
viewBox="0 0 20 20"
@@ -527,12 +556,17 @@ export function CreateCashout({
clip-rule="evenodd"
/>
</svg>
-
- :
- <svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
+ ) : (
+ <svg
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-5 h-5"
+ >
<path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
- }
+ )}
<i18n.Translate>Receive {fiat_currency}</i18n.Translate>
</button>
@@ -579,9 +613,9 @@ export function CreateCashout({
cashoutDisabled
? undefined
: (value) => {
- form.amount = value;
- updateForm(structuredClone(form));
- }
+ form.amount = value;
+ updateForm(structuredClone(form));
+ }
}
/>
<ShowInputErrorLabel
@@ -622,7 +656,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>
@@ -655,7 +689,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"
@@ -727,7 +761,7 @@ export function CreateCashout({
)}
{config.supported_tan_channels.indexOf(TanChannel.SMS) ===
- -1 ? undefined : (
+ -1 ? undefined : (
<label
onClick={() => {
if (!resultAccount.body.contact_data?.phone) return;
@@ -803,7 +837,7 @@ export function CreateCashout({
</button>
</div>
</form>
- </div >
- </div >
+ </div>
+ </div>
);
}
diff --git a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
index 415f88868..3f635db7e 100644
--- a/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
+++ b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
@@ -16,7 +16,6 @@
import {
AbsoluteTime,
Amounts,
- Duration,
HttpStatusCode,
TalerError,
assertUnreachable,
@@ -26,20 +25,19 @@ import {
Loading,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
import { VNode, h } from "preact";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { Time } from "../../components/Time.js";
import { useCashoutDetails, useConversionInfo } from "../../hooks/regional.js";
import { RouteDefinition } from "../../route.js";
import { RenderAmount } from "../PaytoWireTransferForm.js";
-import { Time } from "../../components/Time.js";
interface Props {
id: string;
routeClose: RouteDefinition;
}
export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
- const { i18n, dateLocale } = useTranslationContext();
+ const { i18n } = useTranslationContext();
const cid = Number.parseInt(id, 10);
const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid);
@@ -70,11 +68,11 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
);
case HttpStatusCode.NotImplemented:
return (
- <Attention
- type="warning"
- 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 type="warning" 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:
@@ -92,10 +90,11 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
switch (info.case) {
case HttpStatusCode.NotImplemented: {
return (
- <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 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>
);
}
@@ -134,9 +133,12 @@ export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
<i18n.Translate>Created</i18n.Translate>
</dt>
<dd class="text-sm ">
- <Time format="dd/MM/yyyy HH:mm:ss"
- timestamp={AbsoluteTime.fromProtocolTimestamp(result.body.creation_time)}
- // relative={Duration.fromSpec({ days: 1 })}
+ <Time
+ format="dd/MM/yyyy HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ result.body.creation_time,
+ )}
+ // relative={Duration.fromSpec({ days: 1 })}
/>
</dd>
</div>
diff --git a/packages/bank-ui/src/route.ts b/packages/bank-ui/src/route.ts
index 1f85ce54e..11f13d140 100644
--- a/packages/bank-ui/src/route.ts
+++ b/packages/bank-ui/src/route.ts
@@ -18,7 +18,7 @@ import { useNavigationContext } from "./context/navigation.js";
declare const __location: unique symbol;
/**
* special string that defined a location in the application
- *
+ *
* this help to prevent wrong path
*/
export type AppLocation = string & {
@@ -29,7 +29,7 @@ export type EmptyObject = Record<string, never>;
export function urlPattern<
T extends Record<string, string | undefined> = EmptyObject,
>(pattern: RegExp, reverse: (p: T) => string): RouteDefinition<T> {
- const url = reverse as ((p: T) => AppLocation)
+ const url = reverse as (p: T) => AppLocation;
return {
pattern: new RegExp(pattern),
url,
@@ -38,14 +38,16 @@ export function urlPattern<
/**
* defines a location in the app
- *
+ *
* pattern: how a string will trigger this location
* url(): how a state serialize to a location
*/
export type ObjectOf<T> = Record<string, T> | EmptyObject;
-export type RouteDefinition<T extends ObjectOf<string | undefined> = EmptyObject> = {
+export type RouteDefinition<
+ T extends ObjectOf<string | undefined> = EmptyObject,
+> = {
pattern: RegExp;
url: (p: T) => AppLocation;
};
@@ -54,7 +56,9 @@ const nullRountDef = {
pattern: new RegExp(/.*/),
url: () => "" as AppLocation,
};
-export function buildNullRoutDefinition<T extends ObjectOf<string>>(): RouteDefinition<T> {
+export function buildNullRoutDefinition<
+ T extends ObjectOf<string>,
+>(): RouteDefinition<T> {
return nullRountDef;
}
@@ -76,7 +80,7 @@ function findMatch<T extends ObjectOf<RouteDefinition>>(
const name = pageList[idx];
const found = pagesMap[name].pattern.exec(path);
if (found !== null) {
- const values = {} as Record<string, any>
+ const values = {} as Record<string, unknown>;
Object.entries(params).forEach(([key, value]) => {
values[key] = value;
@@ -97,7 +101,7 @@ function findMatch<T extends ObjectOf<RouteDefinition>>(
/**
* get the type of the params of a location
- *
+ *
*/
type RouteParamsType<
RouteType,
@@ -105,24 +109,29 @@ type RouteParamsType<
> = RouteType[Key] extends RouteDefinition<infer ParamType> ? ParamType : never;
/**
- * Helps to create a map of a type with the key
+ * Helps to create a map of a type with the key
*/
type MapKeyValue<Type> = {
- [Key in keyof Type]: Key extends string ? {
- parent: Type,
- name: Key,
- values: RouteParamsType<Type, Key>;
- } : never;
-}
+ [Key in keyof Type]: Key extends string
+ ? {
+ parent: Type;
+ name: Key;
+ values: RouteParamsType<Type, Key>;
+ }
+ : never;
+};
/**
* create a enumeration of value of a mapped type
*/
-type EnumerationOf<T> = T[keyof T]
+type EnumerationOf<T> = T[keyof T];
-type Location<T> = EnumerationOf<MapKeyValue<T>>
+type Location<T> = EnumerationOf<MapKeyValue<T>>;
-export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(pagesMap: T): Location<T> | undefined {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function useCurrentLocation<T extends ObjectOf<RouteDefinition<any>>>(
+ pagesMap: T,
+): Location<T> | undefined {
const pageList = Object.keys(pagesMap as object) as Array<keyof T>;
const { path, params } = useNavigationContext();
diff --git a/packages/bank-ui/src/stories.test.ts b/packages/bank-ui/src/stories.test.ts
index 207945865..8ed00a1e6 100644
--- a/packages/bank-ui/src/stories.test.ts
+++ b/packages/bank-ui/src/stories.test.ts
@@ -19,7 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import {
- AccessToken,
AmountString,
TalerCorebankApi,
setupI18n,
@@ -51,11 +50,7 @@ describe("All the examples:", () => {
});
});
-function DefaultTestingContext({
- children,
-}: {
- children: ComponentChildren;
-}): VNode {
+function DefaultTestingContext(_props: { children: ComponentChildren }): VNode {
const cfg: TalerCorebankApi.Config = {
name: "libeufin-bank",
allow_deletions: true,
diff --git a/packages/bank-ui/src/utils.ts b/packages/bank-ui/src/utils.ts
index 8b0febe42..305f13803 100644
--- a/packages/bank-ui/src/utils.ts
+++ b/packages/bank-ui/src/utils.ts
@@ -15,6 +15,7 @@
*/
import {
+ AbsoluteTime,
AmountString,
PaytoString,
TalerError,
@@ -73,36 +74,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 {
@@ -155,6 +156,7 @@ export function buildRequestErrorMessage(
title: i18n.str`Request timeout`,
description: cause.message as TranslatedString,
debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
};
break;
}
@@ -164,6 +166,7 @@ export function buildRequestErrorMessage(
title: i18n.str`Request throttled`,
description: cause.message as TranslatedString,
debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
};
break;
}
@@ -173,6 +176,7 @@ export function buildRequestErrorMessage(
title: i18n.str`Malformed response`,
description: cause.message as TranslatedString,
debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
};
break;
}
@@ -182,6 +186,7 @@ export function buildRequestErrorMessage(
title: i18n.str`Network error`,
description: cause.message as TranslatedString,
debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
};
break;
}
@@ -191,6 +196,7 @@ export function buildRequestErrorMessage(
title: i18n.str`Unexpected request error`,
description: cause.message as TranslatedString,
debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
};
break;
}
@@ -200,6 +206,7 @@ export function buildRequestErrorMessage(
title: i18n.str`Unexpected error`,
description: cause.message as TranslatedString,
debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
};
break;
}
@@ -373,11 +380,10 @@ export function validateIBAN(
i18n: InternationalizationAPI,
): TranslatedString | undefined {
if (!IBAN_REGEX.test(account)) {
- return i18n.str`IBAN only have uppercased letters and numbers`
+ return i18n.str`IBAN only have uppercased letters and numbers`;
}
// Check total length
- if (account.length < 4)
- return i18n.str`IBAN numbers have more that 4 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`;
@@ -423,25 +429,7 @@ export function validateTalerBank(
i18n: InternationalizationAPI,
): TranslatedString | undefined {
if (!USERNAME_REGEX.test(account)) {
- return i18n.str`Account only have letters and numbers`
+ 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
+ return undefined;
}
-