summaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-01-23 18:00:42 -0300
committerSebastian <sebasjm@gmail.com>2024-01-24 17:14:02 -0300
commit236d4347f5884bb1d9ca1d3bb4ad0ba776577fd2 (patch)
treea38823a73006c38bd54cb438da81f13bb513dce5 /packages/demobank-ui/src/pages
parent579128ce40c7e56f390cadaf2fc2fd4cc6290d68 (diff)
downloadwallet-core-236d4347f5884bb1d9ca1d3bb4ad0ba776577fd2.tar.gz
wallet-core-236d4347f5884bb1d9ca1d3bb4ad0ba776577fd2.tar.bz2
wallet-core-236d4347f5884bb1d9ca1d3bb4ad0ba776577fd2.zip
many changes
activate eslint update file headers removed history and preact-router remove eslint errors and more applied prettier
Diffstat (limited to 'packages/demobank-ui/src/pages')
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/index.ts42
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/state.ts58
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/test.ts15
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx127
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx244
-rw-r--r--packages/demobank-ui/src/pages/DownloadStats.tsx494
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx213
-rw-r--r--packages/demobank-ui/src/pages/OperationState/index.ts99
-rw-r--r--packages/demobank-ui/src/pages/OperationState/state.ts143
-rw-r--r--packages/demobank-ui/src/pages/OperationState/test.ts15
-rw-r--r--packages/demobank-ui/src/pages/OperationState/views.tsx497
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx184
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx635
-rw-r--r--packages/demobank-ui/src/pages/ProfileNavigation.tsx214
-rw-r--r--packages/demobank-ui/src/pages/PublicHistoriesPage.tsx32
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx121
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx257
-rw-r--r--packages/demobank-ui/src/pages/SolveChallengePage.tsx806
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx367
-rw-r--r--packages/demobank-ui/src/pages/WireTransfer.tsx60
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx325
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx47
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx298
-rw-r--r--packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx76
-rw-r--r--packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx220
-rw-r--r--packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx173
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx807
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx288
-rw-r--r--packages/demobank-ui/src/pages/admin/AdminHome.tsx650
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx230
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx194
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx544
-rw-r--r--packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx136
-rw-r--r--packages/demobank-ui/src/pages/rnd.ts36
34 files changed, 5306 insertions, 3341 deletions
diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts
index 115da807d..31a8a9e34 100644
--- a/packages/demobank-ui/src/pages/AccountPage/index.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/index.ts
@@ -14,21 +14,36 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, AmountJson, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerCorebankApi,
+ TalerError,
+} from "@gnu-taler/taler-util";
import { Loading, utils } from "@gnu-taler/web-util/browser";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { LoginForm } from "../LoginForm.js";
import { useComponentState } from "./state.js";
import { InvalidIbanView, ReadyView } from "./views.js";
+import { RouteDefinition } from "../../route.js";
export interface Props {
account: string;
onAuthorizationRequired: () => void;
- goToConfirmOperation: (id: string) => void;
+ onOperationCreated: (wopid: string) => void;
+ onClose: () => void;
+ tab: "charge-wallet" | "wire-transfer" | undefined;
+ routeClose: RouteDefinition<Record<string, never>>;
+ routeChargeWallet: RouteDefinition<Record<string, never>>;
+ routeWireTransfer: RouteDefinition<Record<string, never>>;
}
-export type State = State.Loading |
- State.LoadingError | State.Ready | State.InvalidIban | State.UserNotFound;
+export type State =
+ | State.Loading
+ | State.LoadingError
+ | State.Ready
+ | State.InvalidIban
+ | State.UserNotFound;
export namespace State {
export interface Loading {
@@ -48,21 +63,26 @@ export namespace State {
export interface Ready extends BaseInfo {
status: "ready";
error: undefined;
- account: string,
- limit: AmountJson,
+ account: string;
+ tab: "charge-wallet" | "wire-transfer" | undefined;
+ limit: AmountJson;
onAuthorizationRequired: () => void;
- goToConfirmOperation: (id: string) => void;
+ onOperationCreated: (wopid: string) => void;
+ onClose: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
+ routeChargeWallet: RouteDefinition<Record<string, never>>;
+ routeWireTransfer: RouteDefinition<Record<string, never>>;
}
export interface InvalidIban {
- status: "invalid-iban",
+ status: "invalid-iban";
error: TalerCorebankApi.AccountData;
}
export interface UserNotFound {
- status: "login",
+ status: "login";
reason: "not-found" | "forbidden";
- onRegister?: () => void;
+ routeRegister?: RouteDefinition<Record<string, never>>;
}
}
@@ -76,7 +96,7 @@ export interface Transaction {
const viewMapping: utils.StateViewMap<State> = {
loading: Loading,
- "login": LoginForm,
+ login: LoginForm,
"invalid-iban": InvalidIbanView,
"loading-error": ErrorLoadingWithDebug,
ready: ReadyView,
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts
index 56c041a4a..a07ea37d3 100644
--- a/packages/demobank-ui/src/pages/AccountPage/state.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/state.ts
@@ -14,15 +14,27 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, HttpStatusCode, TalerError, parsePaytoUri } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
import { useAccountDetails } from "../../hooks/access.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { Props, State } from "./index.js";
-export function useComponentState({ account, goToConfirmOperation, onAuthorizationRequired }: Props): State {
+export function useComponentState({
+ account,
+ tab,
+ routeChargeWallet,
+ routeWireTransfer,
+ onOperationCreated,
+ onClose,
+ routeClose,
+ onAuthorizationRequired,
+}: Props): State {
const result = useAccountDetails(account);
- const { i18n } = useTranslationContext();
if (!result) {
return {
@@ -40,16 +52,18 @@ export function useComponentState({ account, goToConfirmOperation, onAuthorizati
if (result.type === "fail") {
switch (result.case) {
- case HttpStatusCode.Unauthorized: return {
- status: "login",
- reason: "forbidden"
- }
- case HttpStatusCode.NotFound: return {
- status: "login",
- reason: "not-found",
- }
+ case HttpStatusCode.Unauthorized:
+ return {
+ status: "login",
+ reason: "forbidden",
+ };
+ case HttpStatusCode.NotFound:
+ return {
+ status: "login",
+ reason: "not-found",
+ };
default: {
- assertUnreachable(result)
+ assertUnreachable(result);
}
}
}
@@ -61,10 +75,14 @@ export function useComponentState({ account, goToConfirmOperation, onAuthorizati
const debitThreshold = Amounts.parseOrThrow(data.debit_threshold);
const payto = parsePaytoUri(data.payto_uri);
- if (!payto || !payto.isKnown || (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")) {
+ if (
+ !payto ||
+ !payto.isKnown ||
+ (payto.targetType !== "iban" && payto.targetType !== "x-taler-bank")
+ ) {
return {
status: "invalid-iban",
- error: data
+ error: data,
};
}
@@ -73,12 +91,16 @@ export function useComponentState({ account, goToConfirmOperation, onAuthorizati
? Amounts.sub(debitThreshold, balance).amount
: Amounts.add(balance, debitThreshold).amount;
-
return {
status: "ready",
- goToConfirmOperation,
+ onOperationCreated,
error: undefined,
+ tab,
onAuthorizationRequired,
+ onClose,
+ routeClose,
+ routeChargeWallet,
+ routeWireTransfer,
account,
limit,
};
diff --git a/packages/demobank-ui/src/pages/AccountPage/test.ts b/packages/demobank-ui/src/pages/AccountPage/test.ts
index 538decb29..14c8be948 100644
--- a/packages/demobank-ui/src/pages/AccountPage/test.ts
+++ b/packages/demobank-ui/src/pages/AccountPage/test.ts
@@ -19,14 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import * as tests from "@gnu-taler/web-util/testing";
-import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
-import { expect } from "chai";
-import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
-import { Props } from "./index.js";
-import { useComponentState } from "./state.js";
+// import * as tests from "@gnu-taler/web-util/testing";
+// import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+// import { expect } from "chai";
+// import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
+// import { Props } from "./index.js";
+// import { useComponentState } from "./state.js";
describe("Account states", () => {
- it("should do some tests", async () => {
- });
+ it("should do some tests", async () => {});
});
diff --git a/packages/demobank-ui/src/pages/AccountPage/views.tsx b/packages/demobank-ui/src/pages/AccountPage/views.tsx
index c9a7a6c13..9baefe96c 100644
--- a/packages/demobank-ui/src/pages/AccountPage/views.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage/views.tsx
@@ -22,6 +22,7 @@ import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
import { PaymentOptions } from "../PaymentOptions.js";
import { State } from "./index.js";
+import { privatePages } from "../../Routing.js";
export function InvalidIbanView({ error }: State.InvalidIban) {
return (
@@ -29,29 +30,34 @@ export function InvalidIbanView({ error }: State.InvalidIban) {
);
}
-const IS_PUBLIC_ACCOUNT_ENABLED = false
+const IS_PUBLIC_ACCOUNT_ENABLED = false;
function ShowDemoInfo(): VNode {
const { i18n } = useTranslationContext();
const [settings, updateSettings] = usePreferences();
- if (!settings.showDemoDescription) return <Fragment />
- return <Attention title={i18n.str`This is a demo bank`} onClose={() => {
- updateSettings("showDemoDescription", false);
- }}>
- {IS_PUBLIC_ACCOUNT_ENABLED ? (
- <i18n.Translate>
- 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 href="/public-accounts">Public Accounts</a>.
- </i18n.Translate>
- ) : (
- <i18n.Translate>
- This part of the demo shows how a bank that supports Taler
- directly would work.
- </i18n.Translate>
- )}
- </Attention>
+ if (!settings.showDemoDescription) return <Fragment />;
+ return (
+ <Attention
+ title={i18n.str`This is a demo bank`}
+ onClose={() => {
+ updateSettings("showDemoDescription", false);
+ }}
+ >
+ {IS_PUBLIC_ACCOUNT_ENABLED ? (
+ <i18n.Translate>
+ 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 href={privatePages.publicAccountList.url({})}>Public Accounts</a>.
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate>
+ This part of the demo shows how a bank that supports Taler directly
+ would work.
+ </i18n.Translate>
+ )}
+ </Attention>
+ );
}
function ShowPedingOperation(): VNode {
@@ -60,30 +66,67 @@ function ShowPedingOperation(): VNode {
if (!bankState.currentChallenge) return <Fragment />;
const title = ((op): TranslatedString => {
switch (op) {
- case "delete-account": return i18n.str`Pending account delete operation`
- case "update-account": return i18n.str`Pending account update operation`
- case "update-password": return i18n.str`Pending password update operation`
- case "create-transaction": return i18n.str`Pending transaction operation`
- case "confirm-withdrawal": return i18n.str`Pending withdrawal operation`
- case "create-cashout": return i18n.str`Pending cashout operation`
+ case "delete-account":
+ return i18n.str`Pending account delete operation`;
+ case "update-account":
+ return i18n.str`Pending account update operation`;
+ case "update-password":
+ return i18n.str`Pending password update operation`;
+ case "create-transaction":
+ return i18n.str`Pending transaction operation`;
+ case "confirm-withdrawal":
+ return i18n.str`Pending withdrawal operation`;
+ case "create-cashout":
+ return i18n.str`Pending cashout operation`;
}
- })(bankState.currentChallenge.operation)
- return <Attention title={title} type="warning" onClose={() => { updateBankState("currentChallenge", undefined); }}>
- <i18n.Translate>
- You can complete or cancel the operation in</i18n.Translate> <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/2fa`}>
- <i18n.Translate>this page</i18n.Translate>
- </a>
- </Attention>
+ })(bankState.currentChallenge.operation);
+ return (
+ <Attention
+ title={title}
+ type="warning"
+ onClose={() => {
+ updateBankState("currentChallenge", undefined);
+ }}
+ >
+ <i18n.Translate>
+ You can complete or cancel the operation in
+ </i18n.Translate>{" "}
+ <a
+ class="font-semibold text-yellow-700 hover:text-yellow-600"
+ href={`#/2fa`}
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </Attention>
+ );
}
-export function ReadyView({ account, limit, goToConfirmOperation, onAuthorizationRequired }: State.Ready): VNode<{}> {
-
- return <Fragment>
- <ShowPedingOperation />
- <ShowDemoInfo />
- <PaymentOptions limit={limit} goToConfirmOperation={goToConfirmOperation} onAuthorizationRequired={onAuthorizationRequired} />
- <Transactions account={account} />
- </Fragment>;
+export function ReadyView({
+ tab,
+ account,
+ routeChargeWallet,
+ routeWireTransfer,
+ limit,
+ onClose,
+ routeClose,
+ onOperationCreated,
+ onAuthorizationRequired,
+}: State.Ready): VNode {
+ return (
+ <Fragment>
+ <ShowPedingOperation />
+ <ShowDemoInfo />
+ <PaymentOptions
+ tab={tab}
+ routeChargeWallet={routeChargeWallet}
+ routeWireTransfer={routeWireTransfer}
+ limit={limit}
+ routeClose={routeClose}
+ onClose={onClose}
+ onOperationCreated={onOperationCreated}
+ onAuthorizationRequired={onAuthorizationRequired}
+ />
+ <Transactions account={account} />
+ </Fragment>
+ );
}
-
-
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index 73e87d9d2..a106f370d 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -15,143 +15,195 @@
*/
import { Amounts, TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import { Footer, GlobalNotificationsBanner, Header, Loading, notifyError, notifyException, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import {
+ Footer,
+ GlobalNotificationsBanner,
+ Header,
+ Loading,
+ notifyError,
+ notifyException,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, VNode, h } from "preact";
+import { useEffect, useErrorBoundary } from "preact/hooks";
+import { privatePages } from "../Routing.js";
+import { useBankCoreApiContext } from "../context/config.js";
+import { useSettingsContext } from "../context/settings.js";
import { useAccountDetails } from "../hooks/access.js";
import { useBackendState } from "../hooks/backend.js";
-import { getAllBooleanPreferences, getLabelForPreferences, usePreferences } from "../hooks/preferences.js";
-import { RenderAmount } from "./PaytoWireTransferForm.js";
-import { useSettingsContext } from "../context/settings.js";
-import { useBankCoreApiContext } from "../context/config.js";
import { useBankState } from "../hooks/bank-state.js";
+import {
+ getAllBooleanPreferences,
+ getLabelForPreferences,
+ usePreferences,
+} from "../hooks/preferences.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
-
export function BankFrame({
children,
account,
}: {
- account?: string,
+ account?: string;
children: ComponentChildren;
}): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendState();
const settings = useSettingsContext();
const [preferences, updatePreferences] = usePreferences();
- const [, , resetBankState] = useBankState()
+ const [, , resetBankState] = useBankState();
const [error, resetError] = useErrorBoundary();
useEffect(() => {
if (error) {
- const desc = (error instanceof Error ? error.stack : String(error)) as TranslatedString
if (error instanceof Error) {
- console.log("Internal error, please report", error)
- notifyException(i18n.str`Internal error, please report.`, error)
+ console.log("Internal error, please report", error);
+ notifyException(i18n.str`Internal error, please report.`, error);
} else {
- console.log("Internal error, please report", error)
- notifyError(i18n.str`Internal error, please report.`, String(error) as TranslatedString)
+ console.log("Internal error, please report", error);
+ notifyError(
+ i18n.str`Internal error, please report.`,
+ String(error) as TranslatedString,
+ );
}
- resetError()
+ resetError();
}
- }, [error])
-
- return (<div class="min-h-full flex flex-col m-0 bg-slate-200" style="min-height: 100vh;">
-
- <div class="bg-indigo-600 pb-32">
- <Header
- title="Bank"
- iconLinkURL={settings.iconLinkURL ?? "#"}
- onLogout={backend.state.status !== "loggedIn" ? undefined : () => {
- backend.logOut()
- resetBankState();
- }}
- sites={!settings.topNavSites ? [] : Object.entries(settings.topNavSites)}
- supportedLangs={["en", "es", "de"]}
- >
- <li>
- <div class="text-xs font-semibold leading-6 text-gray-400">
- <i18n.Translate>Preferences</i18n.Translate>
- </div>
- <ul role="list" class="space-y-1">
- {getAllBooleanPreferences().map(set => {
- const isOn: boolean = !!preferences[set]
- return <li class="mt-2 pl-2">
- <div class="flex items-center justify-between">
- <span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
- {getLabelForPreferences(set, i18n)}
- </span>
- </span>
- <button type="button" data-enabled={isOn} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
- onClick={() => { updatePreferences(set, !isOn); }}>
- <span aria-hidden="true" data-enabled={isOn} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
- </button>
- </div>
- </li>
- })}
- </ul>
- </li>
- </Header>
- </div >
-
- <GlobalNotificationsBanner />
-
- <main class="-mt-32 flex-1">
- {account &&
- <header class="py-5 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} /></span>
- <span class="text-2xl font-bold tracking-tight text-white"><AccountBalance account={account} /></span>
- </h1>
- </div>
- </header>
- }
-
- <div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
- <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
- {children}
- </div>
+ }, [error]);
+
+ return (
+ <div
+ class="min-h-full flex flex-col m-0 bg-slate-200"
+ style="min-height: 100vh;"
+ >
+ <div class="bg-indigo-600 pb-32">
+ <Header
+ title="Bank"
+ iconLinkURL={settings.iconLinkURL ?? "#"}
+ onLogout={
+ backend.state.status !== "loggedIn"
+ ? undefined
+ : () => {
+ backend.logOut();
+ resetBankState();
+ }
+ }
+ sites={
+ !settings.topNavSites ? [] : Object.entries(settings.topNavSites)
+ }
+ supportedLangs={["en", "es", "de"]}
+ >
+ <li>
+ <div class="text-xs font-semibold leading-6 text-gray-400">
+ <i18n.Translate>Preferences</i18n.Translate>
+ </div>
+ <ul role="list" class="space-y-1">
+ {getAllBooleanPreferences().map((set) => {
+ const isOn: boolean = !!preferences[set];
+ return (
+ <li key={set} class="mt-2 pl-2">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ {getLabelForPreferences(set, i18n)}
+ </span>
+ </span>
+ <button
+ type="button"
+ data-enabled={isOn}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ updatePreferences(set, !isOn);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={isOn}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ </li>
+ </Header>
</div>
- </main>
-
- <Footer
- testingUrlKey="corebank-api-base-url"
- GIT_HASH={GIT_HASH}
- VERSION={VERSION}
- />
-
- </div >
+ <GlobalNotificationsBanner />
+
+ <main class="-mt-32 flex-1">
+ {account && (
+ <header class="py-5 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} />
+ </span>
+ <span class="text-2xl font-bold tracking-tight text-white">
+ <AccountBalance account={account} />
+ </span>
+ </h1>
+ </div>
+ </header>
+ )}
+
+ <div class="mx-auto max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
+ <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
+ {children}
+ </div>
+ </div>
+ </main>
+
+ <Footer
+ testingUrlKey="corebank-api-base-url"
+ GIT_HASH={GIT_HASH}
+ VERSION={VERSION}
+ />
+ </div>
);
}
function WelcomeAccount({ account: accountName }: { account: string }): VNode {
const { i18n } = useTranslationContext();
- return <a href="#/my-profile" class="underline underline-offset-2">
- <i18n.Translate>Welcome, <span class="whitespace-nowrap">{accountName}</span></i18n.Translate>
- </a>
+ return (
+ <a
+ href={privatePages.myAccountDetails.url({})}
+ class="underline underline-offset-2"
+ >
+ <i18n.Translate>
+ Welcome, <span class="whitespace-nowrap">{accountName}</span>
+ </i18n.Translate>
+ </a>
+ );
}
function AccountBalance({ account }: { account: string }): VNode {
const result = useAccountDetails(account);
const { config } = useBankCoreApiContext();
if (!result) {
- return <Loading />
+ return <Loading />;
}
if (result instanceof TalerError) {
- return <div />
+ return <div />;
}
- if (result.type === "fail") return <div />
+ if (result.type === "fail") return <div />;
- return <RenderAmount
- value={Amounts.parseOrThrow(result.body.balance.amount)}
- negative={result.body.balance.credit_debit_indicator === "debit"}
- spec={config.currency_specification}
- />
+ return (
+ <RenderAmount
+ value={Amounts.parseOrThrow(result.body.balance.amount)}
+ negative={result.body.balance.credit_debit_indicator === "debit"}
+ spec={config.currency_specification}
+ />
+ );
}
diff --git a/packages/demobank-ui/src/pages/DownloadStats.tsx b/packages/demobank-ui/src/pages/DownloadStats.tsx
index 48daacaea..a98c573ae 100644
--- a/packages/demobank-ui/src/pages/DownloadStats.tsx
+++ b/packages/demobank-ui/src/pages/DownloadStats.tsx
@@ -14,18 +14,28 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AccessToken, AmountString, Logger, TalerCoreBankHttpClient, TalerCorebankApi, TalerError } from "@gnu-taler/taler-util";
-import { Attention, LocalNotificationBanner, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import {
+ AccessToken,
+ AmountString,
+ TalerCoreBankHttpClient,
+ TalerCorebankApi,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ LocalNotificationBanner,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { useBankCoreApiContext } from "../context/config.js";
import { useBackendState } from "../hooks/backend.js";
+import { RouteDefinition } from "../route.js";
import { getTimeframesForDate } from "./admin/AdminHome.js";
-const logger = new Logger("PublicHistoriesPage");
-
interface Props {
- onCancel: () => void;
+ routeCancel: RouteDefinition<Record<string, never>>;
}
type Options = {
@@ -36,16 +46,19 @@ type Options = {
compareWithPrevious: boolean;
endOnFirstFail: boolean;
includeHeader: boolean;
-}
+};
-/**
+/**
* Show histories of public accounts.
*/
-export function DownloadStats({ onCancel }: Props): VNode {
+export function DownloadStats({ routeCancel }: Props): VNode {
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
- const creds = credentials.status !== "loggedIn" || !credentials.isUserAdministrator ? undefined : credentials
+ const creds =
+ credentials.status !== "loggedIn" || !credentials.isUserAdministrator
+ ? undefined
+ : credentials;
const { api } = useBankCoreApiContext();
const [options, setOptions] = useState<Options>({
@@ -56,19 +69,18 @@ export function DownloadStats({ onCancel }: Props): VNode {
includeHeader: true,
monthMetric: true,
yearMetric: true,
- })
- const [lastStep, setLastStep] = useState<{ step: number, total: number }>()
- const [downloaded, setDownloaded] = useState<string>()
- const referenceDates = [new Date()]
- const [notification, notify, handleError] = useLocalNotification()
+ });
+ const [lastStep, setLastStep] = useState<{ step: number; total: number }>();
+ const [downloaded, setDownloaded] = useState<string>();
+ const referenceDates = [new Date()];
+ const [notification, , handleError] = useLocalNotification();
if (!creds) {
- return <div>only admin can download stats</div>
+ return <div>only admin can download stats</div>;
}
return (
<div>
-
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<LocalNotificationBanner notification={notification} />
@@ -78,13 +90,12 @@ export function DownloadStats({ onCancel }: Props): VNode {
</h2>
</div>
-
<form
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
+ onSubmit={(e) => {
+ e.preventDefault();
}}
>
<div class="px-4 py-6 sm:p-8">
@@ -92,223 +103,393 @@ export function DownloadStats({ onCancel }: Props): VNode {
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Include hour metric</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={options.hourMetric} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
- onClick={() => { setOptions({ ...options, hourMetric: !options.hourMetric }) }}>
- <span aria-hidden="true" data-enabled={options.hourMetric} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ <button
+ type="button"
+ data-enabled={options.hourMetric}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ setOptions({
+ ...options,
+ hourMetric: !options.hourMetric,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.hourMetric}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</div>
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Include day metric</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={!!options.dayMetric} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
- onClick={() => { setOptions({ ...options, dayMetric: !options.dayMetric }) }}>
- <span aria-hidden="true" data-enabled={options.dayMetric} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ <button
+ type="button"
+ data-enabled={!!options.dayMetric}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ setOptions({ ...options, dayMetric: !options.dayMetric });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.dayMetric}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</div>
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Include month metric</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={!!options.monthMetric} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
- onClick={() => { setOptions({ ...options, monthMetric: !options.monthMetric }) }}>
- <span aria-hidden="true" data-enabled={options.monthMetric} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ <button
+ type="button"
+ data-enabled={!!options.monthMetric}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ setOptions({
+ ...options,
+ monthMetric: !options.monthMetric,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.monthMetric}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</div>
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Include year metric</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={!!options.yearMetric} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
- onClick={() => { setOptions({ ...options, yearMetric: !options.yearMetric }) }}>
- <span aria-hidden="true" data-enabled={options.yearMetric} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ <button
+ type="button"
+ data-enabled={!!options.yearMetric}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ setOptions({
+ ...options,
+ yearMetric: !options.yearMetric,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.yearMetric}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</div>
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Include table header</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={!!options.includeHeader} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
- onClick={() => { setOptions({ ...options, includeHeader: !options.includeHeader }) }}>
- <span aria-hidden="true" data-enabled={options.includeHeader} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ <button
+ type="button"
+ data-enabled={!!options.includeHeader}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ setOptions({
+ ...options,
+ includeHeader: !options.includeHeader,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.includeHeader}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</div>
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
- <i18n.Translate>Add previous metric for compare</i18n.Translate>
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>
+ Add previous metric for compare
+ </i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={!!options.compareWithPrevious} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
- onClick={() => { setOptions({ ...options, compareWithPrevious: !options.compareWithPrevious }) }}>
- <span aria-hidden="true" data-enabled={options.compareWithPrevious} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ <button
+ type="button"
+ data-enabled={!!options.compareWithPrevious}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ setOptions({
+ ...options,
+ compareWithPrevious: !options.compareWithPrevious,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.compareWithPrevious}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</div>
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Fail on first error</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={!!options.endOnFirstFail} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
- onClick={() => { setOptions({ ...options, endOnFirstFail: !options.endOnFirstFail }) }}>
- <span aria-hidden="true" data-enabled={options.endOnFirstFail} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ <button
+ type="button"
+ data-enabled={!!options.endOnFirstFail}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ setOptions({
+ ...options,
+ endOnFirstFail: !options.endOnFirstFail,
+ });
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={options.endOnFirstFail}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</div>
</div>
</div>
-
-
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
+ <a
+ href={routeCancel.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
>
<i18n.Translate>Cancel</i18n.Translate>
- </button>
- <button type="submit"
+ </a>
+ <button
+ type="submit"
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"
disabled={lastStep !== undefined}
onClick={async () => {
- setDownloaded(undefined)
+ setDownloaded(undefined);
await handleError(async () => {
- const csv = await fetchAllStatus(api, creds.token, options, referenceDates, (step, total) => {
- setLastStep({ step, total })
- })
- setDownloaded(csv)
- })
- setLastStep(undefined)
+ const csv = await fetchAllStatus(
+ api,
+ creds.token,
+ options,
+ referenceDates,
+ (step, total) => {
+ setLastStep({ step, total });
+ },
+ );
+ setDownloaded(csv);
+ });
+ setLastStep(undefined);
}}
>
<i18n.Translate>Download</i18n.Translate>
</button>
</div>
</form>
-
</div>
- {!lastStep || lastStep.step === lastStep.total ? <div class="h-5 mb-5" /> : <div>
- <div class="relative mb-5 h-5 rounded-full bg-gray-200">
- <div class="h-full animate-pulse rounded-full bg-blue-500" style={{
- width: `${Math.round((((lastStep.step / lastStep.total)) * 100))}%`
- }}>
- <span class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-white">
- <i18n.Translate>downloading... {Math.round((((lastStep.step / lastStep.total)) * 100))}</i18n.Translate>
- </span>
+ {!lastStep || lastStep.step === lastStep.total ? (
+ <div class="h-5 mb-5" />
+ ) : (
+ <div>
+ <div class="relative mb-5 h-5 rounded-full bg-gray-200">
+ <div
+ class="h-full animate-pulse rounded-full bg-blue-500"
+ style={{
+ width: `${Math.round((lastStep.step / lastStep.total) * 100)}%`,
+ }}
+ >
+ <span class="absolute inset-0 flex items-center justify-center text-xs font-semibold text-white">
+ <i18n.Translate>
+ downloading...{" "}
+ {Math.round((lastStep.step / lastStep.total) * 100)}
+ </i18n.Translate>
+ </span>
+ </div>
</div>
</div>
- </div>}
- {!downloaded ? <div class="h-5 mb-5" /> :
- <a href={"data:text/plain;charset=utf-8," + encodeURIComponent(downloaded)} download={"bank-stats.csv"}>
+ )}
+ {!downloaded ? (
+ <div class="h-5 mb-5" />
+ ) : (
+ <a
+ href={
+ "data:text/plain;charset=utf-8," + encodeURIComponent(downloaded)
+ }
+ download={"bank-stats.csv"}
+ >
<Attention title={i18n.str`Download completed`}>
- <i18n.Translate>click here to save the file in your computer</i18n.Translate>
+ <i18n.Translate>
+ click here to save the file in your computer
+ </i18n.Translate>
</Attention>
</a>
- }
+ )}
</div>
);
}
-
-async function fetchAllStatus(api: TalerCoreBankHttpClient, token: AccessToken, options: Options, references: Date[], progres: (current: number, total: number) => void): Promise<string> {
+async function fetchAllStatus(
+ api: TalerCoreBankHttpClient,
+ token: AccessToken,
+ options: Options,
+ references: Date[],
+ progres: (current: number, total: number) => void,
+): Promise<string> {
const allMetrics: TalerCorebankApi.MonitorTimeframeParam[] = [];
if (options.hourMetric) {
- allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.hour)
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.hour);
}
if (options.dayMetric) {
- allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.day)
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.day);
}
if (options.monthMetric) {
- allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.month)
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.month);
}
if (options.yearMetric) {
- allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.year)
+ allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.year);
}
/**
* conver request into frames
*/
- const allFrames = allMetrics.flatMap(timeframe => references.map(reference => ({
- reference,
- timeframe,
- moment: getTimeframesForDate(reference, timeframe)
- }))
- )
- const total = allFrames.length
+ const allFrames = allMetrics.flatMap((timeframe) =>
+ references.map((reference) => ({
+ reference,
+ timeframe,
+ moment: getTimeframesForDate(reference, timeframe),
+ })),
+ );
+ const total = allFrames.length;
/**
* call API for info
*/
- const allInfo = await allFrames.reduce(async (prev, frame, index) => {
- const accumulatedMap = await prev
- progres(index, total)
- // await delay()
- const previous = options.compareWithPrevious ? (await api.getMonitor(token, {
- timeframe: frame.timeframe,
- which: frame.moment.previous
- })) : undefined
-
- if (previous && previous.type === "fail" && options.endOnFirstFail) {
- throw TalerError.fromUncheckedDetail(previous.detail)
- }
+ const allInfo = await allFrames.reduce(
+ async (prev, frame, index) => {
+ const accumulatedMap = await prev;
+ progres(index, total);
+ // await delay()
+ const previous = options.compareWithPrevious
+ ? await api.getMonitor(token, {
+ timeframe: frame.timeframe,
+ which: frame.moment.previous,
+ })
+ : undefined;
+
+ if (previous && previous.type === "fail" && options.endOnFirstFail) {
+ throw TalerError.fromUncheckedDetail(previous.detail);
+ }
- const current = await api.getMonitor(token, {
- timeframe: frame.timeframe,
- which: frame.moment.current
- })
+ const current = await api.getMonitor(token, {
+ timeframe: frame.timeframe,
+ which: frame.moment.current,
+ });
- if (current.type === "fail" && options.endOnFirstFail) {
- throw TalerError.fromUncheckedDetail(current.detail)
- }
+ if (current.type === "fail" && options.endOnFirstFail) {
+ throw TalerError.fromUncheckedDetail(current.detail);
+ }
- const metricName = TalerCorebankApi.MonitorTimeframeParam[allMetrics[index]]
- accumulatedMap[metricName] = {
- reference: frame.reference,
- current: current.type !== "ok" ? undefined : current.body,
- previous: !previous || previous.type !== "ok" ? undefined : previous.body,
- }
- return accumulatedMap
- }, Promise.resolve({} as Record<string, Data>))
- progres(total, total)
+ const metricName =
+ TalerCorebankApi.MonitorTimeframeParam[allMetrics[index]];
+ accumulatedMap[metricName] = {
+ reference: frame.reference,
+ current: current.type !== "ok" ? undefined : current.body,
+ previous:
+ !previous || previous.type !== "ok" ? undefined : previous.body,
+ };
+ return accumulatedMap;
+ },
+ Promise.resolve({} as Record<string, Data>),
+ );
+ progres(total, total);
/**
* conver into table format
- *
+ *
*/
const table: Array<string[]> = [];
if (options.includeHeader) {
- table.push(["date",
+ table.push([
+ "date",
"metric",
"reference",
"talerInCount",
@@ -320,7 +501,8 @@ async function fetchAllStatus(api: TalerCoreBankHttpClient, token: AccessToken,
"cashinRegionalVolume",
"cashoutCount",
"cashoutFiatVolume",
- "cashoutRegionalVolume",])
+ "cashoutRegionalVolume",
+ ]);
}
Object.entries(allInfo).forEach(([name, data]) => {
if (data.current) {
@@ -328,9 +510,9 @@ async function fetchAllStatus(api: TalerCoreBankHttpClient, token: AccessToken,
date: data.reference.getTime(),
metric: name,
reference: "current",
- ...dataToRow(data.current)
- }
- table.push((Object.values(row) as string[]))
+ ...dataToRow(data.current),
+ };
+ table.push(Object.values(row) as string[]);
}
if (data.previous) {
@@ -338,20 +520,20 @@ async function fetchAllStatus(api: TalerCoreBankHttpClient, token: AccessToken,
date: data.reference.getTime(),
metric: name,
reference: "previous",
- ...dataToRow(data.previous)
- }
- table.push((Object.values(row) as string[]))
+ ...dataToRow(data.previous),
+ };
+ table.push(Object.values(row) as string[]);
}
- })
+ });
const csv = table.reduce((acc, row) => {
- return acc + row.join(",") + "\n"
- }, "")
+ return acc + row.join(",") + "\n";
+ }, "");
- return csv
+ return csv;
}
-type JustData = Omit<Omit<Omit<TableRow, "metric">, "date">, "reference">
+type JustData = Omit<Omit<Omit<TableRow, "metric">, "date">, "reference">;
function dataToRow(info: TalerCorebankApi.MonitorResponse): JustData {
return {
talerInCount: info.talerInCount,
@@ -359,23 +541,28 @@ function dataToRow(info: TalerCorebankApi.MonitorResponse): JustData {
talerOutCount: info.talerOutCount,
talerOutVolume: info.talerOutVolume,
cashinCount: info.type === "no-conversions" ? undefined : info.cashinCount,
- cashinFiatVolume: info.type === "no-conversions" ? undefined : info.cashinFiatVolume,
- cashinRegionalVolume: info.type === "no-conversions" ? undefined : info.cashinRegionalVolume,
- cashoutCount: info.type === "no-conversions" ? undefined : info.cashoutCount,
- cashoutFiatVolume: info.type === "no-conversions" ? undefined : info.cashoutFiatVolume,
- cashoutRegionalVolume: info.type === "no-conversions" ? undefined : info.cashoutRegionalVolume,
- }
+ cashinFiatVolume:
+ info.type === "no-conversions" ? undefined : info.cashinFiatVolume,
+ cashinRegionalVolume:
+ info.type === "no-conversions" ? undefined : info.cashinRegionalVolume,
+ cashoutCount:
+ info.type === "no-conversions" ? undefined : info.cashoutCount,
+ cashoutFiatVolume:
+ info.type === "no-conversions" ? undefined : info.cashoutFiatVolume,
+ cashoutRegionalVolume:
+ info.type === "no-conversions" ? undefined : info.cashoutRegionalVolume,
+ };
}
type Data = {
- reference: Date,
+ reference: Date;
previous: TalerCorebankApi.MonitorResponse | undefined;
current: TalerCorebankApi.MonitorResponse | undefined;
-}
+};
type TableRow = {
- date: number,
- metric: string,
- reference: "current" | "previous",
+ date: number;
+ metric: string;
+ reference: "current" | "previous";
cashinCount?: number;
cashinRegionalVolume?: AmountString;
cashinFiatVolume?: AmountString;
@@ -386,11 +573,4 @@ type TableRow = {
talerInVolume: AmountString;
talerOutCount: number;
talerOutVolume: AmountString;
-}
-async function delay() {
- return new Promise(res => {
- setTimeout(() => {
- res(null)
- }, 500)
- })
-} \ No newline at end of file
+};
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index 04bf0b7fa..7e5631cfb 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,29 +14,48 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util";
-import { LocalNotificationBanner, ShowInputErrorLabel, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import {
+ HttpStatusCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { useBankCoreApiContext } from "../context/config.js";
import { useBackendState } from "../hooks/backend.js";
import { undefinedIfEmpty } from "../utils.js";
import { doAutoFocus } from "./PaytoWireTransferForm.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
-
+import { RouteDefinition } from "../route.js";
/**
* Collect and submit login data.
*/
-export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?: boolean, currentUser?: string, onRegister?: () => void }): VNode {
+export function LoginForm({
+ currentUser,
+ fixedUser,
+ routeRegister,
+}: {
+ fixedUser?: boolean;
+ currentUser?: string;
+ routeRegister?: RouteDefinition<Record<string, never>>;
+}): VNode {
const backend = useBackendState();
- const sessionUser = backend.state.status !== "loggedOut" ? backend.state.username : undefined
- const [username, setUsername] = useState<string | undefined>(currentUser ?? sessionUser);
+ const sessionUser =
+ backend.state.status !== "loggedOut" ? backend.state.username : undefined;
+ const [username, setUsername] = useState<string | undefined>(
+ currentUser ?? sessionUser,
+ );
const [password, setPassword] = useState<string | undefined>();
const { i18n } = useTranslationContext();
const { api } = useBankCoreApiContext();
- const [notification, notify, handleError] = useLocalNotification()
+ const [notification, notify, handleError] = useLocalNotification();
const { config } = useBankCoreApiContext();
const ref = useRef<HTMLInputElement>(null);
@@ -44,63 +63,71 @@ export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?:
ref.current?.focus();
}, []);
- const [busy, setBusy] = useState<Record<string, undefined>>()
+ const [busy, setBusy] = useState<Record<string, undefined>>();
- 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,
- }) ?? busy;
+ 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,
+ }) ?? busy;
async function doLogout() {
- backend.logOut()
+ backend.logOut();
}
async function doLogin() {
if (!username || !password) return;
- setBusy({})
+ setBusy({});
await handleError(async () => {
- const resp = await api.getAuthenticationAPI(username).createAccessToken(password, {
- // scope: "readwrite" as "write", //FIX: different than merchant
- scope: "readwrite",
- duration: {
- d_us: "forever" //FIX: should return shortest
- // d_us: 60 * 60 * 24 * 7 * 1000 * 1000
- },
- refreshable: true,
- })
+ const resp = await api
+ .getAuthenticationAPI(username)
+ .createAccessToken(password, {
+ // scope: "readwrite" as "write", // FIX: different than merchant
+ scope: "readwrite",
+ duration: {
+ d_us: "forever", // FIX: should return shortest
+ // d_us: 60 * 60 * 24 * 7 * 1000 * 1000
+ },
+ refreshable: true,
+ });
if (resp.type === "ok") {
backend.logIn({ username, token: resp.body.access_token });
} else {
switch (resp.case) {
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- title: i18n.str`Wrong credentials for "${username}"`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`Account not found`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: assertUnreachable(resp)
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`Wrong credentials for "${username}"`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ default:
+ assertUnreachable(resp);
}
}
- })
+ });
setPassword(undefined);
- setBusy(undefined)
+ setBusy(undefined);
}
return (
<div class="flex min-h-full flex-col justify-center ">
<LocalNotificationBanner notification={notification} />
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
- <form class="space-y-6" noValidate
+ <form
+ class="space-y-6"
+ noValidate
onSubmit={(e) => {
e.preventDefault();
}}
@@ -108,7 +135,10 @@ export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?:
autoCorrect="off"
>
<div>
- <label for="username" class="block text-sm font-medium leading-6 text-gray-900">
+ <label
+ for="username"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
<i18n.Translate>Username</i18n.Translate>
</label>
<div class="mt-2">
@@ -138,7 +168,10 @@ export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?:
<div>
<div class="flex items-center justify-between">
- <label for="password" class="block text-sm font-medium leading-6 text-gray-900">
+ <label
+ for="password"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
<i18n.Translate>Password</i18n.Translate>
</label>
</div>
@@ -165,54 +198,58 @@ export function LoginForm({ currentUser, fixedUser, onRegister }: { fixedUser?:
</div>
</div>
- {backend.state.status !== "loggedOut" ? <div class="flex justify-between">
- <button type="submit"
- class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
- onClick={(e) => {
- e.preventDefault()
- doLogout()
- }}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
-
- <button type="submit"
- class="rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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"
- disabled={!!errors}
- onClick={async (e) => {
- e.preventDefault()
- doLogin()
- }}
- >
- <i18n.Translate>Check</i18n.Translate>
- </button>
- </div> : <div>
- <button type="submit"
- class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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"
- disabled={!!errors}
- onClick={(e) => {
- e.preventDefault()
- doLogin()
- }}
- >
- <i18n.Translate>Log in</i18n.Translate>
- </button>
- </div>}
+ {backend.state.status !== "loggedOut" ? (
+ <div class="flex justify-between">
+ <button
+ type="submit"
+ class="rounded-md bg-white-600 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600"
+ onClick={(e) => {
+ e.preventDefault();
+ doLogout();
+ }}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+
+ <button
+ type="submit"
+ class="rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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"
+ disabled={!!errors}
+ onClick={async (e) => {
+ e.preventDefault();
+ doLogin();
+ }}
+ >
+ <i18n.Translate>Check</i18n.Translate>
+ </button>
+ </div>
+ ) : (
+ <div>
+ <button
+ type="submit"
+ class="flex w-full justify-center rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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"
+ disabled={!!errors}
+ onClick={(e) => {
+ e.preventDefault();
+ doLogin();
+ }}
+ >
+ <i18n.Translate>Log in</i18n.Translate>
+ </button>
+ </div>
+ )}
</form>
- {config.allow_registrations && onRegister &&
+ {config.allow_registrations && routeRegister && (
<p class="mt-10 text-center text-sm text-gray-500 border-t">
- <button type="submit"
+ <a
+ href={routeRegister.url({})}
class="flex mt-4 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
- onClick={(e) => {
- e.preventDefault()
- onRegister()
- }}
>
<i18n.Translate>Register</i18n.Translate>
- </button>
+ </a>
</p>
- }
+ )}
</div>
</div>
);
diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts
index 18ffe0ec3..20cb1760f 100644
--- a/packages/demobank-ui/src/pages/OperationState/index.ts
+++ b/packages/demobank-ui/src/pages/OperationState/index.ts
@@ -14,28 +14,46 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, AmountJson, TalerCoreBankErrorsByMethod, TalerError, WithdrawUriResult } from "@gnu-taler/taler-util";
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerCoreBankErrorsByMethod,
+ TalerError,
+ WithdrawUriResult,
+} from "@gnu-taler/taler-util";
import { Loading, utils } from "@gnu-taler/web-util/browser";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { useComponentState } from "./state.js";
-import { AbortedView, ConfirmedView, FailedView, InvalidPaytoView, InvalidReserveView, InvalidWithdrawalView, NeedConfirmationView, ReadyView } from "./views.js";
+import {
+ AbortedView,
+ ConfirmedView,
+ FailedView,
+ InvalidPaytoView,
+ InvalidReserveView,
+ InvalidWithdrawalView,
+ NeedConfirmationView,
+ ReadyView,
+} from "./views.js";
+import { RouteDefinition } from "../../route.js";
export interface Props {
currency: string;
- onAuthorizationRequired: () => void,
- onClose: () => void;
+ onAuthorizationRequired: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
+ onAbort: () => void;
}
-export type State = State.Loading |
- State.LoadingError |
- State.Ready |
- State.Failed |
- State.Aborted |
- State.Confirmed |
- State.InvalidPayto |
- State.InvalidWithdrawal |
- State.InvalidReserve |
- State.NeedConfirmation;
+export type State =
+ | State.Loading
+ | State.LoadingError
+ | State.Ready
+ | State.Failed
+ | State.Aborted
+ | State.Confirmed
+ | State.InvalidPayto
+ | State.InvalidWithdrawal
+ | State.InvalidReserve
+ | State.NeedConfirmation;
export namespace State {
export interface Loading {
@@ -59,48 +77,55 @@ export namespace State {
export interface Ready {
status: "ready";
error: undefined;
- uri: WithdrawUriResult,
- onClose: () => Promise<TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined>;
+ uri: WithdrawUriResult;
+ onAbort: () => Promise<
+ TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
+ >;
+ routeClose: RouteDefinition<Record<string, never>>;
}
export interface InvalidPayto {
- status: "invalid-payto",
+ status: "invalid-payto";
error: undefined;
payto: string | undefined;
- onClose: () => void;
}
export interface InvalidWithdrawal {
- status: "invalid-withdrawal",
+ status: "invalid-withdrawal";
error: undefined;
- onClose: () => void;
- uri: string,
+ uri: string;
}
export interface InvalidReserve {
- status: "invalid-reserve",
+ status: "invalid-reserve";
error: undefined;
- onClose: () => void;
reserve: string | undefined;
}
export interface NeedConfirmation {
- status: "need-confirmation",
- onAuthorizationRequired: () => void,
- account: string,
- onAbort: undefined | (() => Promise<TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined>);
- onConfirm: undefined | (() => Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined>);
+ status: "need-confirmation";
+ onAuthorizationRequired: () => void;
+ account: string;
+ onAbort:
+ | undefined
+ | (() => Promise<
+ TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
+ >);
+ onConfirm:
+ | undefined
+ | (() => Promise<
+ TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
+ >);
error: undefined;
- id: string,
+ id: string;
}
export interface Aborted {
- status: "aborted",
+ status: "aborted";
error: undefined;
- onClose: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
}
export interface Confirmed {
- status: "confirmed",
+ status: "confirmed";
error: undefined;
- onClose: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
}
-
}
export interface Transaction {
@@ -113,13 +138,13 @@ export interface Transaction {
const viewMapping: utils.StateViewMap<State> = {
loading: Loading,
- "failed": FailedView,
+ failed: FailedView,
"invalid-payto": InvalidPaytoView,
"invalid-withdrawal": InvalidWithdrawalView,
"invalid-reserve": InvalidReserveView,
"need-confirmation": NeedConfirmationView,
- "aborted": AbortedView,
- "confirmed": ConfirmedView,
+ aborted: AbortedView,
+ confirmed: ConfirmedView,
"loading-error": ErrorLoadingWithDebug,
ready: ReadyView,
};
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts
index 32a13d047..20d66bbb1 100644
--- a/packages/demobank-ui/src/pages/OperationState/state.ts
+++ b/packages/demobank-ui/src/pages/OperationState/state.ts
@@ -14,7 +14,16 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, HttpStatusCode, TalerCoreBankErrorsByMethod, TalerError, parsePaytoUri, parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import {
+ Amounts,
+ HttpStatusCode,
+ TalerCoreBankErrorsByMethod,
+ TalerError,
+ assertUnreachable,
+ parsePaytoUri,
+ parseWithdrawUri,
+ stringifyWithdrawUri,
+} from "@gnu-taler/taler-util";
import { utils } from "@gnu-taler/web-util/browser";
import { useEffect, useState } from "preact/hooks";
import { mutate } from "swr";
@@ -23,73 +32,80 @@ import { useWithdrawalDetails } from "../../hooks/access.js";
import { useBackendState } from "../../hooks/backend.js";
import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { Props, State } from "./index.js";
-export function useComponentState({ currency, onClose, onAuthorizationRequired, }: Props): utils.RecursiveState<State> {
- const [settings] = usePreferences()
+export function useComponentState({
+ currency,
+ routeClose,
+ onAbort,
+ onAuthorizationRequired,
+}: Props): utils.RecursiveState<State> {
+ const [settings] = usePreferences();
const [bankState, updateBankState] = useBankState();
- const { state: credentials } = useBackendState()
- const creds = credentials.status !== "loggedIn" ? undefined : credentials
- const { api } = useBankCoreApiContext()
+ const { state: credentials } = useBackendState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const { api } = useBankCoreApiContext();
- const [failure, setFailure] = useState<TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined>()
- const amount = settings.maxWithdrawalAmount
+ const [failure, setFailure] = useState<
+ TalerCoreBankErrorsByMethod<"createWithdrawal"> | undefined
+ >();
+ const amount = settings.maxWithdrawalAmount;
async function doSilentStart() {
- //FIXME: if amount is not enough use balance
- const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`)
+ // FIXME: if amount is not enough use balance
+ const parsedAmount = Amounts.parseOrThrow(`${currency}:${amount}`);
if (!creds) return;
const resp = await api.createWithdrawal(creds, {
amount: Amounts.stringify(parsedAmount),
});
if (resp.type === "fail") {
- setFailure(resp)
+ setFailure(resp);
return;
}
- updateBankState("currentWithdrawalOperationId", resp.body.withdrawal_id)
-
+ updateBankState("currentWithdrawalOperationId", resp.body.withdrawal_id);
}
- const withdrawalOperationId = bankState.currentWithdrawalOperationId
+ const withdrawalOperationId = bankState.currentWithdrawalOperationId;
useEffect(() => {
if (withdrawalOperationId === undefined) {
- doSilentStart()
+ doSilentStart();
}
- }, [settings.fastWithdrawal, amount])
+ }, [settings.fastWithdrawal, amount]);
if (failure) {
return {
status: "failed",
- error: failure
- }
+ error: failure,
+ };
}
if (!withdrawalOperationId) {
return {
status: "loading",
- error: undefined
- }
+ error: undefined,
+ };
}
- const wid = withdrawalOperationId
+ const wid = withdrawalOperationId;
async function doAbort() {
if (!creds) return;
const resp = await api.abortWithdrawalById(creds, wid);
if (resp.type === "ok") {
- updateBankState("currentWithdrawalOperationId", undefined)
- onClose();
+ // updateBankState("currentWithdrawalOperationId", undefined)
+ onAbort();
} else {
return resp;
}
}
- async function doConfirm(): Promise<TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined> {
+ async function doConfirm(): Promise<
+ TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
+ > {
if (!creds) return;
const resp = await api.confirmWithdrawalById(creds, wid);
if (resp.type === "ok") {
- mutate(() => true)//clean withdrawal state
+ mutate(() => true); //clean withdrawal state
} else {
return resp;
}
@@ -105,30 +121,29 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired,
status: "invalid-withdrawal",
error: undefined,
uri,
- onClose,
- }
+ };
}
return (): utils.RecursiveState<State> => {
const result = useWithdrawalDetails(withdrawalOperationId);
- const shouldCreateNewOperation = result && !(result instanceof TalerError)
+ const shouldCreateNewOperation = result && !(result instanceof TalerError);
useEffect(() => {
if (shouldCreateNewOperation) {
- doSilentStart()
+ doSilentStart();
}
- }, [])
+ }, []);
if (!result) {
return {
status: "loading",
- error: undefined
- }
+ error: undefined,
+ };
}
if (result instanceof TalerError) {
return {
status: "loading-error",
- error: result
- }
+ error: result,
+ };
}
if (result.type === "fail") {
@@ -138,13 +153,11 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired,
return {
status: "aborted",
error: undefined,
- onClose: async () => {
- updateBankState("currentWithdrawalOperationId", undefined)
- onClose()
- },
- }
+ routeClose,
+ };
}
- default: assertUnreachable(result)
+ default:
+ assertUnreachable(result);
}
}
@@ -153,26 +166,20 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired,
return {
status: "aborted",
error: undefined,
- onClose: async () => {
- updateBankState("currentWithdrawalOperationId", undefined)
- onClose()
- },
- }
+ routeClose,
+ };
}
if (data.status === "confirmed") {
if (!settings.showWithdrawalSuccess) {
- updateBankState("currentWithdrawalOperationId", undefined)
- onClose()
+ updateBankState("currentWithdrawalOperationId", undefined);
+ // onClose()
}
return {
status: "confirmed",
error: undefined,
- onClose: async () => {
- updateBankState("currentWithdrawalOperationId", undefined)
- onClose()
- },
- }
+ routeClose,
+ };
}
if (data.status === "pending") {
@@ -180,11 +187,14 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired,
status: "ready",
error: undefined,
uri: parsedUri,
- onClose: !creds ? (async () => {
- onClose();
- return undefined
- }) : doAbort,
- }
+ routeClose,
+ onAbort: !creds
+ ? async () => {
+ onAbort();
+ return undefined;
+ }
+ : doAbort,
+ };
}
if (!data.selected_reserve_pub) {
@@ -192,19 +202,19 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired,
status: "invalid-reserve",
error: undefined,
reserve: data.selected_reserve_pub,
- onClose,
- }
+ };
}
- const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account)
+ const account = !data.selected_exchange_account
+ ? undefined
+ : parsePaytoUri(data.selected_exchange_account);
if (!account) {
return {
status: "invalid-payto",
error: undefined,
payto: data.selected_exchange_account,
- onClose,
- }
+ };
}
return {
@@ -214,8 +224,7 @@ export function useComponentState({ currency, onClose, onAuthorizationRequired,
account: data.username,
id: withdrawalOperationId,
onAbort: !creds ? undefined : doAbort,
- onConfirm: !creds ? undefined : doConfirm
- }
- }
-
+ onConfirm: !creds ? undefined : doConfirm,
+ };
+ };
}
diff --git a/packages/demobank-ui/src/pages/OperationState/test.ts b/packages/demobank-ui/src/pages/OperationState/test.ts
index 3ba351cd3..d47cb64a2 100644
--- a/packages/demobank-ui/src/pages/OperationState/test.ts
+++ b/packages/demobank-ui/src/pages/OperationState/test.ts
@@ -19,14 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import * as tests from "@gnu-taler/web-util/testing";
-import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
-import { expect } from "chai";
-import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
-import { Props } from "./index.js";
-import { useComponentState } from "./state.js";
+// import * as tests from "@gnu-taler/web-util/testing";
+// import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+// import { expect } from "chai";
+// import { CASHOUT_API_EXAMPLE } from "../../endpoints.js";
+// import { Props } from "./index.js";
+// import { useComponentState } from "./state.js";
describe("Withdrawal operation states", () => {
- it("should do some tests", async () => {
- });
+ it("should do some tests", async () => {});
});
diff --git a/packages/demobank-ui/src/pages/OperationState/views.tsx b/packages/demobank-ui/src/pages/OperationState/views.tsx
index c86b8bd4b..ac3724eb8 100644
--- a/packages/demobank-ui/src/pages/OperationState/views.tsx
+++ b/packages/demobank-ui/src/pages/OperationState/views.tsx
@@ -14,121 +14,143 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, HttpStatusCode, TalerErrorCode, TranslatedString, stringifyWithdrawUri } from "@gnu-taler/taler-util";
-import { Attention, LocalNotificationBanner, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ AbsoluteTime,
+ HttpStatusCode,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+ stringifyWithdrawUri,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ LocalNotificationBanner,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect } from "preact/hooks";
import { QR } from "../../components/QR.js";
import { useBankState } from "../../hooks/bank-state.js";
import { usePreferences } from "../../hooks/preferences.js";
import { ShouldBeSameUser } from "../WithdrawalConfirmationQuestion.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { State } from "./index.js";
-export function InvalidPaytoView({ payto, onClose }: State.InvalidPayto) {
- return (
- <div>Payto from server is not valid &quot;{payto}&quot;</div>
- );
+export function InvalidPaytoView({ payto }: State.InvalidPayto) {
+ return <div>Payto from server is not valid &quot;{payto}&quot;</div>;
}
-export function InvalidWithdrawalView({ uri, onClose }: State.InvalidWithdrawal) {
- return (
- <div>Withdrawal uri from server is not valid &quot;{uri}&quot;</div>
- );
+export function InvalidWithdrawalView({ uri }: State.InvalidWithdrawal) {
+ return <div>Withdrawal uri from server is not valid &quot;{uri}&quot;</div>;
}
-export function InvalidReserveView({ reserve, onClose }: State.InvalidReserve) {
- return (
- <div>Reserve from server is not valid &quot;{reserve}&quot;</div>
- );
+export function InvalidReserveView({ reserve }: State.InvalidReserve) {
+ return <div>Reserve from server is not valid &quot;{reserve}&quot;</div>;
}
-export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doConfirm, account, id, onAuthorizationRequired, }: State.NeedConfirmation) {
- const { i18n } = useTranslationContext()
- const [settings] = usePreferences()
- const [notification, notify, errorHandler] = useLocalNotification()
- const [, updateBankState] = useBankState()
+export function NeedConfirmationView({
+ onAbort: doAbort,
+ onConfirm: doConfirm,
+ account,
+ id,
+ onAuthorizationRequired,
+}: State.NeedConfirmation) {
+ const { i18n } = useTranslationContext();
+ const [settings] = usePreferences();
+ const [notification, notify, errorHandler] = useLocalNotification();
+ const [, updateBankState] = useBankState();
async function onCancel() {
errorHandler(async () => {
if (!doAbort) return;
- const resp = await doAbort()
+ const resp = await doAbort();
if (!resp) return;
switch (resp.case) {
- case HttpStatusCode.Conflict: 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,
- })
- case HttpStatusCode.BadRequest: return notify({
- type: "error",
- title: i18n.str`The operation id is invalid.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`The operation was not found.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- default: assertUnreachable(resp)
+ case HttpStatusCode.Conflict:
+ 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,
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ default:
+ assertUnreachable(resp);
}
- })
+ });
}
async function onConfirm() {
errorHandler(async () => {
if (!doConfirm) return;
- const resp = await doConfirm()
+ const resp = await doConfirm();
if (!resp) {
if (!settings.showWithdrawalSuccess) {
- notifyInfo(i18n.str`Wire transfer completed!`)
+ notifyInfo(i18n.str`Wire transfer completed!`);
}
- return
+ return;
}
switch (resp.case) {
- case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: return notify({
- type: "error",
- title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return notify({
- type: "error",
- 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,
- })
- case HttpStatusCode.BadRequest: return notify({
- type: "error",
- title: i18n.str`The operation id is invalid.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`The operation was not found.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({
- type: "error",
- title: i18n.str`Your balance is not enough.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
+ case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT:
+ return notify({
+ type: "error",
+ title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return notify({
+ type: "error",
+ 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,
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Your balance is not enough.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "confirm-withdrawal",
id: String(resp.body.challenge_id),
sent: AbsoluteTime.never(),
request: id,
- })
- return onAuthorizationRequired()
+ });
+ return onAuthorizationRequired();
}
- default: assertUnreachable(resp)
+ default:
+ assertUnreachable(resp);
}
- })
+ });
}
return (
@@ -144,23 +166,27 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
+ onSubmit={(e) => {
+ e.preventDefault();
}}
>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ <button
+ type="button"
+ class="text-sm font-semibold leading-6 text-gray-900"
onClick={(e) => {
- e.preventDefault()
- onCancel()
+ e.preventDefault();
+ onCancel();
}}
>
- <i18n.Translate>Cancel</i18n.Translate></button>
- <button type="submit"
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ type="submit"
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={(e) => {
- e.preventDefault()
- onConfirm()
+ e.preventDefault();
+ onConfirm();
}}
>
<i18n.Translate>Transfer</i18n.Translate>
@@ -171,61 +197,81 @@ export function NeedConfirmationView({ error, onAbort: doAbort, onConfirm: doCon
</div>
</div>
</div>
-
);
}
export function FailedView({ error }: State.Failed) {
const { i18n } = useTranslationContext();
switch (error.case) {
- case HttpStatusCode.Unauthorized: return <Attention type="danger"
- title={i18n.str`Unauthorized to make the operation, maybe the session has expired or the password changed.`}>
- <div class="mt-2 text-sm text-red-700">
- {error.detail.hint}
- </div>
- </Attention>
- case HttpStatusCode.Conflict: return <Attention type="danger"
- title={i18n.str`The operation was rejected due to insufficient funds.`}>
- <div class="mt-2 text-sm text-red-700">
- {error.detail.hint}
- </div>
- </Attention>
- case HttpStatusCode.NotFound: return <Attention type="danger"
- title={i18n.str`The operation was rejected due to insufficient funds.`}>
- <div class="mt-2 text-sm text-red-700">
- {error.detail.hint}
- </div>
- </Attention>
- default: assertUnreachable(error)
+ case HttpStatusCode.Unauthorized:
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`Unauthorized to make the operation, maybe the session has expired or the password changed.`}
+ >
+ <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ </Attention>
+ );
+ case HttpStatusCode.Conflict:
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation was rejected due to insufficient funds.`}
+ >
+ <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ </Attention>
+ );
+ case HttpStatusCode.NotFound:
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation was rejected due to insufficient funds.`}
+ >
+ <div class="mt-2 text-sm text-red-700">{error.detail.hint}</div>
+ </Attention>
+ );
+ default:
+ assertUnreachable(error);
}
}
-export function AbortedView({ error, onClose }: State.Aborted) {
- return (
- <div>aborted</div>
- );
+export function AbortedView() {
+ return <div>aborted</div>;
}
-export function ConfirmedView({ error, onClose }: State.Confirmed) {
+export function ConfirmedView({ routeClose }: State.Confirmed) {
const { i18n } = useTranslationContext();
- const [settings, updateSettings] = usePreferences()
+ const [settings, updateSettings] = usePreferences();
return (
<Fragment>
-
<div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white p-4 text-left shadow-xl transition-all ">
-
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
- <svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
+ <svg
+ class="h-6 w-6 text-green-600"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M4.5 12.75l6 6 9-13.5"
+ />
</svg>
</div>
<div class="mt-3 text-center sm:mt-5">
- <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
+ <h3
+ class="text-base font-semibold leading-6 text-gray-900"
+ id="modal-title"
+ >
<i18n.Translate>Withdrawal confirmed</i18n.Translate>
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
<i18n.Translate>
- The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.
+ The wire transfer to the Taler operator has been initiated. You
+ will soon receive the requested amount in your Taler wallet.
</i18n.Translate>
</p>
</div>
@@ -234,132 +280,165 @@ export function ConfirmedView({ error, onClose }: State.Confirmed) {
<div class="mt-4">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Do not show this again</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={!settings.showWithdrawalSuccess} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
+ <button
+ type="button"
+ data-enabled={!settings.showWithdrawalSuccess}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
onClick={() => {
- updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess);
- }}>
- <span aria-hidden="true" data-enabled={!settings.showWithdrawalSuccess} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ updateSettings(
+ "showWithdrawalSuccess",
+ !settings.showWithdrawalSuccess,
+ );
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={!settings.showWithdrawalSuccess}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</div>
<div class="mt-5 sm:mt-6">
- <button type="button"
+ <a
+ href={routeClose.url({})}
+ type="button"
class="inline-flex w-full justify-center 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 (e) => {
- e.preventDefault();
- onClose()
- }}>
+ >
<i18n.Translate>Close</i18n.Translate>
- </button>
+ </a>
</div>
</Fragment>
-
);
}
-export function ReadyView({ uri, onClose: doClose }: State.Ready): VNode<{}> {
+export function ReadyView({
+ uri,
+ onAbort: doAbort,
+}: State.Ready): VNode<Record<string, never>> {
const { i18n } = useTranslationContext();
- const [notification, notify, errorHandler] = useLocalNotification()
+ const [notification, notify, errorHandler] = useLocalNotification();
const talerWithdrawUri = stringifyWithdrawUri(uri);
useEffect(() => {
- //Taler Wallet WebExtension is listening to headers response and tab updates.
- //In the SPA there is no header response with the Taler URI so
- //this hack manually triggers the tab update after the QR is in the DOM.
+ // Taler Wallet WebExtension is listening to headers response and tab updates.
+ // In the SPA there is no header response with the Taler URI so
+ // this hack manually triggers the tab update after the QR is in the DOM.
// WebExtension will be using
// https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
document.title = `${document.title} ${uri.withdrawalOperationId}`;
- const meta = document.createElement("meta")
- meta.setAttribute("name", "taler-uri")
- meta.setAttribute("content", talerWithdrawUri)
- document.head.insertBefore(meta, document.head.children.length ? document.head.children[0] : null)
+ const meta = document.createElement("meta");
+ meta.setAttribute("name", "taler-uri");
+ meta.setAttribute("content", talerWithdrawUri);
+ document.head.insertBefore(
+ meta,
+ document.head.children.length ? document.head.children[0] : null,
+ );
}, []);
- async function onClose() {
+ async function onAbort() {
errorHandler(async () => {
- const hasError = await doClose()
+ const hasError = await doAbort();
if (!hasError) return;
switch (hasError.case) {
- case HttpStatusCode.Conflict: return notify({
- type: "error",
- title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
- description: hasError.detail.hint as TranslatedString,
- debug: hasError.detail,
- })
- case HttpStatusCode.BadRequest: return notify({
- type: "error",
- title: i18n.str`The operation id is invalid.`,
- description: hasError.detail.hint as TranslatedString,
- debug: hasError.detail,
- });
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`The operation was not found.`,
- description: hasError.detail.hint as TranslatedString,
- debug: hasError.detail,
- });
- default: assertUnreachable(hasError)
+ case HttpStatusCode.Conflict:
+ return notify({
+ type: "error",
+ title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
+ description: hasError.detail.hint as TranslatedString,
+ debug: hasError.detail,
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: hasError.detail.hint as TranslatedString,
+ debug: hasError.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: hasError.detail.hint as TranslatedString,
+ debug: hasError.detail,
+ });
+ default:
+ assertUnreachable(hasError);
}
- })
+ });
}
- return <Fragment>
- <LocalNotificationBanner notification={notification} />
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
- <div class="flex justify-end mt-4">
- <button type="button"
- class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
- onClick={() => {
- onClose()
- }}
- >
- Cancel
- </button>
- </div>
+ <div class="flex justify-end mt-4">
+ <button
+ type="button"
+ class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
+ onClick={onAbort}
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ </div>
- <div class="bg-white shadow sm:rounded-lg mt-4">
- <div class="p-4">
- <h3 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>On this device</i18n.Translate>
- </h3>
- <div class="mt-2 sm:flex sm:items-start sm:justify-between">
- <div class="max-w-xl text-sm text-gray-500">
- <p>
- <i18n.Translate>If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.</i18n.Translate>
- </p>
- </div>
- <div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center">
- <a href={talerWithdrawUri}
- class="inline-flex items-center 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"
- >
- <i18n.Translate>Start</i18n.Translate>
- </a>
+ <div class="bg-white shadow sm:rounded-lg mt-4">
+ <div class="p-4">
+ <h3 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>On this device</i18n.Translate>
+ </h3>
+ <div class="mt-2 sm:flex sm:items-start sm:justify-between">
+ <div class="max-w-xl text-sm text-gray-500">
+ <p>
+ <i18n.Translate>
+ If you are using a web browser on desktop you should access
+ your wallet with the GNU Taler WebExtension now or click the
+ link if your WebExtension have the "Inject Taler support"
+ option enabled.
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="mt-5 sm:ml-6 sm:mt-0 sm:flex sm:flex-shrink-0 sm:items-center">
+ <a
+ href={talerWithdrawUri}
+ class="inline-flex items-center 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"
+ >
+ <i18n.Translate>Start</i18n.Translate>
+ </a>
+ </div>
</div>
</div>
</div>
- </div>
- <div class="bg-white shadow sm:rounded-lg mt-2">
- <div class="p-4">
- <h3 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>On a mobile phone</i18n.Translate>
- </h3>
- <div class="mt-2 sm:flex sm:items-start sm:justify-between">
- <div class="max-w-xl text-sm text-gray-500">
- <p>
- <i18n.Translate>Scan the QR code with your mobile device.</i18n.Translate>
- </p>
+ <div class="bg-white shadow sm:rounded-lg mt-2">
+ <div class="p-4">
+ <h3 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>On a mobile phone</i18n.Translate>
+ </h3>
+ <div class="mt-2 sm:flex sm:items-start sm:justify-between">
+ <div class="max-w-xl text-sm text-gray-500">
+ <p>
+ <i18n.Translate>
+ Scan the QR code with your mobile device.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ <div class="mt-2 max-w-md ml-auto mr-auto">
+ <QR text={talerWithdrawUri} />
</div>
- </div>
- <div class="mt-2 max-w-md ml-auto mr-auto">
- <QR text={talerWithdrawUri} />
</div>
</div>
- </div>
-
- </Fragment>
-
+ </Fragment>
+ );
}
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index 55611c172..53086d4cc 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -15,30 +15,41 @@
*/
import { AmountJson } from "@gnu-taler/taler-util";
-import { notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { useState } from "preact/hooks";
import { useBankState } from "../hooks/bank-state.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
+import { RouteDefinition } from "../route.js";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
/**
* Let the user choose a payment option,
* then specify the details trigger the action.
*/
-export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationRequired }: {
- limit: AmountJson,
- onAuthorizationRequired: () => void,
- goToConfirmOperation: (id: string) => void,
+export function PaymentOptions({
+ routeClose,
+ routeChargeWallet,
+ routeWireTransfer,
+ tab,
+ limit,
+ onOperationCreated,
+ onClose,
+ onAuthorizationRequired,
+}: {
+ limit: AmountJson;
+ tab: "charge-wallet" | "wire-transfer" | undefined;
+ onAuthorizationRequired: () => void;
+ onOperationCreated: (wopid: string) => void;
+ onClose: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
+ routeChargeWallet: RouteDefinition<Record<string, never>>;
+ routeWireTransfer: RouteDefinition<Record<string, never>>;
}): VNode {
const { i18n } = useTranslationContext();
const [bankState] = useBankState();
- const [tab, setTab] = useState<"charge-wallet" | "wire-transfer" | undefined>();
-
return (
<div class="mt-4">
-
<fieldset>
<legend class="px-4 text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Send money</i18n.Translate>
@@ -46,65 +57,112 @@ export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationReq
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-2 sm:gap-x-4">
{/* <!-- Active: "border-indigo-600 ring-2 ring-indigo-600", Not Active: "border-gray-300" --> */}
- <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "charge-wallet" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
- <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onClick={() => {
- setTab("charge-wallet")
- }}
- />
- <div class="flex flex-col">
- <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 <b>Taler</b> wallet</i18n.Translate>
- </span>
- <svg class="self-center flex-none h-5 w-5 text-indigo-600" style={{ visibility: tab === "charge-wallet" ? "visible" : "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>
- </span>
- <div class="mt-1 flex items-center text-sm text-gray-500">
- <i18n.Translate>Withdraw digital money into your mobile wallet or browser extension</i18n.Translate>
- </div>
- {!!bankState.currentWithdrawalOperationId &&
- <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
- <svg class="h-1.5 w-1.5 fill-green-500" viewBox="0 0 6 6" aria-hidden="true">
- <circle cx="3" cy="3" r="3" />
+ <a href={routeChargeWallet.url({})}>
+ <label
+ class={
+ "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
+ (tab === "charge-wallet"
+ ? "border-indigo-600 ring-2 ring-indigo-600"
+ : "border-gray-300")
+ }
+ >
+ <div class="flex flex-col">
+ <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 <b>Taler</b> wallet
+ </i18n.Translate>
+ </span>
+ <svg
+ class="self-center flex-none h-5 w-5 text-indigo-600"
+ style={{
+ visibility:
+ tab === "charge-wallet" ? "visible" : "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>
- <i18n.Translate>operation ready</i18n.Translate>
</span>
- }
- </div>
- </label>
-
+ <div class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>
+ Withdraw digital money into your mobile wallet or browser
+ extension
+ </i18n.Translate>
+ </div>
+ {!!bankState.currentWithdrawalOperationId && (
+ <span class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 px-2 py-1 text-xs font-medium text-green-700 whitespace-pre">
+ <svg
+ class="h-1.5 w-1.5 fill-green-500"
+ viewBox="0 0 6 6"
+ aria-hidden="true"
+ >
+ <circle cx="3" cy="3" r="3" />
+ </svg>
+ <i18n.Translate>operation ready</i18n.Translate>
+ </span>
+ )}
+ </div>
+ </label>
+ </a>
- <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (tab === "wire-transfer" ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
- <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onClick={() => {
- setTab("wire-transfer")
- }} />
- <div class="flex flex-col">
- <span class="flex">
- <div class="text-4xl mr-4 my-auto">&#x2194;</div>
- <span class="grow self-center text-lg font-medium text-gray-900 align-middle text-center">
- <i18n.Translate>to another bank account</i18n.Translate>
+ <a href={routeWireTransfer.url({})}>
+ <label
+ class={
+ "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
+ (tab === "wire-transfer"
+ ? "border-indigo-600 ring-2 ring-indigo-600"
+ : "border-gray-300")
+ }
+ >
+ <div class="flex flex-col">
+ <span class="flex">
+ <div class="text-4xl mr-4 my-auto">&#x2194;</div>
+ <span class="grow self-center text-lg font-medium text-gray-900 align-middle text-center">
+ <i18n.Translate>to another bank account</i18n.Translate>
+ </span>
+ <svg
+ class="self-center flex-none h-5 w-5 text-indigo-600"
+ style={{
+ visibility:
+ tab === "wire-transfer" ? "visible" : "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>
</span>
- <svg class="self-center flex-none h-5 w-5 text-indigo-600" style={{ visibility: tab === "wire-transfer" ? "visible" : "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>
- </span>
- <div class="mt-1 flex items-center text-sm text-gray-500">
- <i18n.Translate>Make a wire transfer to an account with known bank account number.</i18n.Translate>
+ <div class="mt-1 flex items-center text-sm text-gray-500">
+ <i18n.Translate>
+ Make a wire transfer to an account with known bank account
+ number.
+ </i18n.Translate>
+ </div>
</div>
- </div>
- </label>
+ </label>
+ </a>
</div>
{tab === "charge-wallet" && (
<WalletWithdrawForm
focus
limit={limit}
onAuthorizationRequired={onAuthorizationRequired}
- goToConfirmOperation={goToConfirmOperation}
- onCancel={() => {
- setTab(undefined)
- }}
+ onOperationCreated={onOperationCreated}
+ onOperationAborted={onClose}
+ routeCancel={routeClose}
/>
)}
{tab === "wire-transfer" && (
@@ -113,17 +171,11 @@ export function PaymentOptions({ limit, goToConfirmOperation, onAuthorizationReq
title={i18n.str`Transfer details`}
limit={limit}
onAuthorizationRequired={onAuthorizationRequired}
- onSuccess={() => {
- notifyInfo(i18n.str`Wire transfer created!`);
- setTab(undefined)
- }}
- onCancel={() => {
- setTab(undefined)
- }}
+ onSuccess={onClose}
+ routeCancel={routeClose}
/>
)}
-
</fieldset>
</div>
- )
+ );
}
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 321b87253..2259929e7 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -17,68 +17,62 @@
import {
AbsoluteTime,
AmountJson,
- AmountLike,
AmountString,
Amounts,
CurrencySpecification,
FRAC_SEPARATOR,
HttpStatusCode,
- Logger,
PaytoString,
TalerErrorCode,
TranslatedString,
+ assertUnreachable,
buildPayto,
parsePaytoUri,
- stringifyPaytoUri
+ stringifyPaytoUri,
} from "@gnu-taler/taler-util";
import {
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ notifyInfo,
useLocalNotification,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { Fragment, Ref, VNode, h } from "preact";
+import { Ref, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { mutate } from "swr";
-import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser";
import { useBankCoreApiContext } from "../context/config.js";
import { useBackendState } from "../hooks/backend.js";
-import {
- undefinedIfEmpty,
- validateIBAN,
- withRuntimeErrorHandling
-} from "../utils.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
-import { LocalNotificationBanner } from "@gnu-taler/web-util/browser";
import { useBankState } from "../hooks/bank-state.js";
-
-const logger = new Logger("PaytoWireTransferForm");
+import { RouteDefinition } from "../route.js";
+import { undefinedIfEmpty, validateIBAN } from "../utils.js";
export function PaytoWireTransferForm({
focus,
title,
toAccount,
onSuccess,
- onCancel,
+ routeCancel,
onAuthorizationRequired,
limit,
}: {
- title: TranslatedString,
+ title: TranslatedString;
focus?: boolean;
- toAccount?: string,
+ toAccount?: string;
onSuccess: () => void;
onAuthorizationRequired: () => void;
- onCancel: (() => void) | undefined;
+ routeCancel?: RouteDefinition<Record<string, never>>;
limit: AmountJson;
}): VNode {
const [isRawPayto, setIsRawPayto] = useState(false);
- const { state: credentials } = useBackendState()
+ const { state: credentials } = useBackendState();
const { api } = useBankCoreApiContext();
- const sendingToFixedAccount = toAccount !== undefined
- //FIXME: support other destination that just IBAN
+ const sendingToFixedAccount = toAccount !== undefined;
+ // FIXME: support other destination that just IBAN
const [iban, setIban] = useState<string | undefined>(toAccount);
const [subject, setSubject] = useState<string | undefined>();
const [amount, setAmount] = useState<string | undefined>();
- const [, updateBankState] = useBankState()
+ const [, updateBankState] = useBankState();
const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
undefined,
@@ -89,7 +83,7 @@ export function PaytoWireTransferForm({
const trimmedAmountStr = amount?.trim();
const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
- const [notification, notify, handleError] = useLocalNotification()
+ const [notification, notify, handleError] = useLocalNotification();
const errorsWire = undefinedIfEmpty({
iban: !iban
@@ -135,18 +129,18 @@ export function PaytoWireTransferForm({
if (credentials.status !== "loggedIn") return;
if (rawPaytoInput) {
- const p = parsePaytoUri(rawPaytoInput)
+ const p = parsePaytoUri(rawPaytoInput);
if (!p) return;
- sendingAmount = p.params.amount as AmountString
- delete p.params.amount
- //if this payto is valid then it already have message
- payto_uri = stringifyPaytoUri(p)
+ sendingAmount = p.params.amount as AmountString;
+ delete p.params.amount;
+ // if this payto is valid then it already have message
+ payto_uri = stringifyPaytoUri(p);
} else {
if (!iban || !subject) return;
const ibanPayto = buildPayto("iban", iban, undefined);
ibanPayto.params.message = encodeURIComponent(subject);
payto_uri = stringifyPaytoUri(ibanPayto);
- sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString
+ sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString;
}
const puri = payto_uri;
const sAmount = sendingAmount;
@@ -155,288 +149,348 @@ export function PaytoWireTransferForm({
const request = {
payto_uri: puri,
amount: sAmount,
- }
+ };
const resp = await api.createTransaction(credentials, request);
- mutate(() => true)
+ mutate(() => true);
if (resp.type === "fail") {
switch (resp.case) {
- case HttpStatusCode.BadRequest: return notify({
- type: "error",
- title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- title: i18n.str`Not enough permission to complete the operation.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_UNKNOWN_CREDITOR: return notify({
- type: "error",
- title: i18n.str`The destination account "${puri}" was not found.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_SAME_ACCOUNT: return notify({
- type: "error",
- 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,
- })
- case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({
- type: "error",
- title: i18n.str`Your balance is not enough.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`The origin account "${puri}" was not found.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`Not enough permission to complete the operation.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
+ return notify({
+ type: "error",
+ title: i18n.str`The destination account "${puri}" was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_SAME_ACCOUNT:
+ return notify({
+ type: "error",
+ 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,
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Your balance is not enough.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The origin account "${puri}" was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "create-transaction",
id: String(resp.body.challenge_id),
sent: AbsoluteTime.never(),
request,
- })
- return onAuthorizationRequired()
+ });
+ return onAuthorizationRequired();
}
- default: assertUnreachable(resp)
+ default:
+ assertUnreachable(resp);
}
}
+ notifyInfo(i18n.str`Wire transfer created!`);
onSuccess();
setAmount(undefined);
setIban(undefined);
setSubject(undefined);
- rawPaytoInputSetter(undefined)
- })
+ rawPaytoInputSetter(undefined);
+ });
}
- return (<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">
- {/**
- * FIXME: Scan a qr code
- */}
- <div class="">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- {title}
- </h2>
- <div>
- <div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
- <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (!isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
- <input type="radio" name="project-type" value="Newsletter" class="sr-only" aria-labelledby="project-type-0-label" aria-describedby="project-type-0-description-0 project-type-0-description-1" onChange={() => {
- if (parsed && parsed.isKnown && parsed.targetType === "iban") {
- setIban(parsed.iban)
- const amountStr = parsed.params["amount"]
- if (amountStr) {
- const amount = Amounts.parse(parsed.params["amount"])
- if (amount) {
- setAmount(Amounts.stringifyValue(amount))
- }
- }
- const subject = parsed.params["message"]
- if (subject) {
- setSubject(subject)
- }
+ return (
+ <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">
+ {/**
+ * FIXME: Scan a qr code
+ */}
+ <div class="">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">{title}</h2>
+ <div>
+ <div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
+ <label
+ class={
+ "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
+ (!isRawPayto
+ ? "border-indigo-600 ring-2 ring-indigo-600"
+ : "border-gray-300")
}
- setIsRawPayto(false)
- }} />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span class="block text-sm font-medium text-gray-900">
- <i18n.Translate>Using a form</i18n.Translate>
- </span>
- </span>
- </span>
- </label>
-
- {sendingToFixedAccount ? undefined :
- <label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" + (isRawPayto ? "border-indigo-600 ring-2 ring-indigo-600" : "border-gray-300")}>
- <input type="radio" name="project-type" value="Existing Customers" class="sr-only" aria-labelledby="project-type-1-label" aria-describedby="project-type-1-description-0 project-type-1-description-1" onChange={() => {
- if (iban) {
- const payto = buildPayto("iban", iban, undefined)
- if (parsedAmount) {
- payto.params["amount"] = Amounts.stringify(parsedAmount)
+ >
+ <input
+ type="radio"
+ name="project-type"
+ value="Newsletter"
+ class="sr-only"
+ aria-labelledby="project-type-0-label"
+ aria-describedby="project-type-0-description-0 project-type-0-description-1"
+ onChange={() => {
+ if (
+ parsed &&
+ parsed.isKnown &&
+ parsed.targetType === "iban"
+ ) {
+ setIban(parsed.iban);
+ const amountStr = parsed.params["amount"];
+ if (amountStr) {
+ const amount = Amounts.parse(parsed.params["amount"]);
+ if (amount) {
+ setAmount(Amounts.stringifyValue(amount));
+ }
+ }
+ const subject = parsed.params["message"];
+ if (subject) {
+ setSubject(subject);
+ }
}
- if (subject) {
- payto.params["message"] = subject
- }
- rawPaytoInputSetter(stringifyPaytoUri(payto))
- }
- setIsRawPayto(true)
- }} />
+ setIsRawPayto(false);
+ }}
+ />
<span class="flex flex-1">
<span class="flex flex-col">
- <span class="block text-sm font-medium text-gray-900">
- <i18n.Translate>Import payto:// URI</i18n.Translate>
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Using a form</i18n.Translate>
</span>
</span>
</span>
</label>
- }
- </div>
- </div>
- </div>
- <form
- class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md sm:rounded-xl md:col-span-2 w-fit mx-auto"
- autoCapitalize="none"
- autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
- }}
- >
- <div class="p-4 sm:p-8">
- {!isRawPayto ?
- <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label for="iban" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Recipient`}</label>
- <div class="mt-2">
+ {sendingToFixedAccount ? undefined : (
+ <label
+ class={
+ "relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none" +
+ (isRawPayto
+ ? "border-indigo-600 ring-2 ring-indigo-600"
+ : "border-gray-300")
+ }
+ >
<input
- ref={focus ? doAutoFocus : undefined}
- type="text"
- class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="iban"
- id="iban"
- disabled={sendingToFixedAccount}
- value={iban ?? ""}
- placeholder="CC0123456789"
- autocomplete="off"
- required
- pattern={ibanRegex}
- onInput={(e): void => {
- setIban(e.currentTarget.value.toUpperCase());
+ type="radio"
+ name="project-type"
+ value="Existing Customers"
+ class="sr-only"
+ aria-labelledby="project-type-1-label"
+ aria-describedby="project-type-1-description-0 project-type-1-description-1"
+ onChange={() => {
+ if (iban) {
+ const payto = buildPayto("iban", iban, undefined);
+ if (parsedAmount) {
+ payto.params["amount"] =
+ Amounts.stringify(parsedAmount);
+ }
+ if (subject) {
+ payto.params["message"] = subject;
+ }
+ rawPaytoInputSetter(stringifyPaytoUri(payto));
+ }
+ setIsRawPayto(true);
}}
/>
- <ShowInputErrorLabel
- message={errorsWire?.iban}
- isDirty={iban !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>IBAN of the recipient's account</i18n.Translate>
- </p>
- </div>
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900">
+ <i18n.Translate>Import payto:// URI</i18n.Translate>
+ </span>
+ </span>
+ </span>
+ </label>
+ )}
+ </div>
+ </div>
+ </div>
- <div class="sm:col-span-5">
- <label for="subject" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Transfer subject`}</label>
- <div class="mt-2">
- <input
- type="text"
- class="block w-full 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="subject"
- id="subject"
- autocomplete="off"
- placeholder={i18n.str`subject`}
- value={subject ?? ""}
- required
- onInput={(e): void => {
- setSubject(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errorsWire?.subject}
- isDirty={subject !== undefined}
- />
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-md sm:rounded-xl md:col-span-2 w-fit mx-auto"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="p-4 sm:p-8">
+ {!isRawPayto ? (
+ <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ for="iban"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Recipient`}</label>
+ <div class="mt-2">
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="text"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="iban"
+ id="iban"
+ disabled={sendingToFixedAccount}
+ value={iban ?? ""}
+ placeholder="CC0123456789"
+ autocomplete="off"
+ required
+ pattern={ibanRegex}
+ onInput={(e): void => {
+ setIban(e.currentTarget.value.toUpperCase());
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsWire?.iban}
+ isDirty={iban !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ IBAN of the recipient's account
+ </i18n.Translate>
+ </p>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>some text to identify the transfer</i18n.Translate>
- </p>
- </div>
- <div class="sm:col-span-5">
- <label for="amount" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Amount`}</label>
- <InputAmount
- name="amount"
- left
- currency={limit.currency}
- value={trimmedAmountStr}
- onChange={(d) => {
- setAmount(d)
- }}
- />
- <ShowInputErrorLabel
- message={errorsWire?.amount}
- isDirty={trimmedAmountStr !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>amount to transfer</i18n.Translate>
- </p>
- </div>
+ <div class="sm:col-span-5">
+ <label
+ for="subject"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Transfer subject`}</label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full 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="subject"
+ id="subject"
+ autocomplete="off"
+ placeholder={i18n.str`subject`}
+ value={subject ?? ""}
+ required
+ onInput={(e): void => {
+ setSubject(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsWire?.subject}
+ isDirty={subject !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ some text to identify the transfer
+ </i18n.Translate>
+ </p>
+ </div>
- </div> :
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6 w-full">
- <div class="sm:col-span-6">
- <label for="address" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`payto URI:`}</label>
- <div class="mt-2">
- <textarea
- ref={focus ? doAutoFocus : undefined}
- name="address"
- id="address"
- type="textarea"
- rows={5}
- class="block overflow-hidden w-44 sm:w-96 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"
- value={rawPaytoInput ?? ""}
- required
- title={i18n.str`uniform resource identifier of the target account`}
- placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`}
- onInput={(e): void => {
- rawPaytoInputSetter(e.currentTarget.value);
+ <div class="sm:col-span-5">
+ <label
+ for="amount"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Amount`}</label>
+ <InputAmount
+ name="amount"
+ left
+ currency={limit.currency}
+ value={trimmedAmountStr}
+ onChange={(d) => {
+ setAmount(d);
}}
/>
<ShowInputErrorLabel
- message={errorsPayto?.rawPaytoInput}
- isDirty={rawPaytoInput !== undefined}
+ message={errorsWire?.amount}
+ isDirty={trimmedAmountStr !== undefined}
/>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>amount to transfer</i18n.Translate>
+ </p>
</div>
</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">
- {onCancel ?
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
+ ) : (
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6 w-full">
+ <div class="sm:col-span-6">
+ <label
+ for="address"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`payto URI:`}</label>
+ <div class="mt-2">
+ <textarea
+ ref={focus ? doAutoFocus : undefined}
+ name="address"
+ id="address"
+ type="textarea"
+ rows={5}
+ class="block overflow-hidden w-44 sm:w-96 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"
+ value={rawPaytoInput ?? ""}
+ required
+ title={i18n.str`uniform resource identifier of the target account`}
+ placeholder={i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`}
+ onInput={(e): void => {
+ rawPaytoInputSetter(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errorsPayto?.rawPaytoInput}
+ isDirty={rawPaytoInput !== undefined}
+ />
+ </div>
+ </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">
+ {routeCancel ? (
+ <a
+ href={routeCancel.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ ) : (
+ <div />
+ )}
+ <button
+ type="submit"
+ 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"
+ disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
+ onClick={(e) => {
+ e.preventDefault();
+ doSend();
+ }}
>
- <i18n.Translate>Cancel</i18n.Translate>
+ <i18n.Translate>Send</i18n.Translate>
</button>
- : <div />
- }
- <button type="submit"
- 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"
- disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
- onClick={(e) => {
- e.preventDefault()
- doSend()
- }}
- >
- <i18n.Translate>Send</i18n.Translate>
- </button>
- </div>
- <LocalNotificationBanner notification={notification} />
- </form>
- </div >
- )
-
+ </div>
+ <LocalNotificationBanner notification={notification} />
+ </form>
+ </div>
+ );
}
/**
* Show the element when the load ended
- * @param element
+ * @param element
*/
export function doAutoFocus(element: HTMLElement | null) {
if (element) {
setTimeout(() => {
- element.focus({ preventScroll: true })
+ element.focus({ preventScroll: true });
element.scrollIntoView({
behavior: "smooth",
block: "center",
inline: "center",
- })
- }, 100)
+ });
+ }, 100);
}
}
@@ -452,26 +506,25 @@ export function InputAmount(
error?: string;
currency: string;
name: string;
- left?: boolean | undefined,
+ left?: boolean | undefined;
value: string | undefined;
onChange?: (s: string) => void;
},
ref: Ref<HTMLInputElement>,
): VNode {
- const { config } = useBankCoreApiContext()
+ const { config } = useBankCoreApiContext();
return (
<div class="mt-2">
<div class="flex rounded-md shadow-sm border-0 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600">
- <div
- class="pointer-events-none inset-y-0 flex items-center px-3"
- >
+ <div class="pointer-events-none inset-y-0 flex items-center px-3">
<span class="text-gray-500 sm:text-sm">{currency}</span>
</div>
<input
type="number"
data-left={left}
class="disabled:bg-gray-200 text-right rounded-md rounded-l-none data-[left=true]:text-left w-full py-1.5 pl-3 text-gray-900 placeholder:text-gray-400 sm:text-sm sm:leading-6"
- placeholder="0.00" aria-describedby="price-currency"
+ placeholder="0.00"
+ aria-describedby="price-currency"
ref={ref}
name={name}
id={name}
@@ -480,10 +533,19 @@ export function InputAmount(
disabled={!onChange}
onInput={(e) => {
if (!onChange) return;
- const l = e.currentTarget.value.length
- const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR)
- if (sep_pos !== -1 && l - sep_pos - 1 > 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)
+ const l = e.currentTarget.value.length;
+ const sep_pos = e.currentTarget.value.indexOf(FRAC_SEPARATOR);
+ if (
+ sep_pos !== -1 &&
+ l - sep_pos - 1 >
+ 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,
+ );
}
onChange(e.currentTarget.value);
}}
@@ -494,13 +556,34 @@ export function InputAmount(
);
}
-export function RenderAmount({ value, spec, negative, withColor, hideSmall }: { spec: CurrencySpecification; value: AmountJson, hideSmall?: boolean, negative?: boolean, withColor?: boolean }): VNode {
- const neg = !!negative //convert to true or false
+export function RenderAmount({
+ value,
+ spec,
+ negative,
+ withColor,
+ hideSmall,
+}: {
+ spec: CurrencySpecification;
+ value: AmountJson;
+ hideSmall?: boolean;
+ negative?: boolean;
+ withColor?: boolean;
+}): VNode {
+ const neg = !!negative; // convert to true or false
- const { currency, normal, small } = Amounts.stringifyValueWithSpec(value, spec)
+ const { currency, normal, small } = Amounts.stringifyValueWithSpec(
+ value,
+ spec,
+ );
- return <span data-negative={withColor ? neg : undefined} class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
- {negative ? "- " : undefined}
- {currency} {normal} {!hideSmall && small && <sup class="-ml-1">{small}</sup>}
- </span>
-} \ No newline at end of file
+ return (
+ <span
+ data-negative={withColor ? neg : undefined}
+ class="whitespace-nowrap data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"
+ >
+ {negative ? "- " : undefined}
+ {currency} {normal}{" "}
+ {!hideSmall && small && <sup class="-ml-1">{small}</sup>}
+ </span>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/ProfileNavigation.tsx b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
index bd9883b1b..a6615d578 100644
--- a/packages/demobank-ui/src/pages/ProfileNavigation.tsx
+++ b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
@@ -1,72 +1,158 @@
+/*
+ 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import { VNode, h } from "preact";
+import { privatePages } from "../Routing.js";
import { useBankCoreApiContext } from "../context/config.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
import { useBackendState } from "../hooks/backend.js";
+import { assertUnreachable } from "@gnu-taler/taler-util";
-export function ProfileNavigation({ current }: { current: "details" | "delete" | "credentials" | "cashouts" }): VNode {
- const { i18n } = useTranslationContext()
- const { config } = useBankCoreApiContext()
+export function ProfileNavigation({
+ current,
+}: {
+ current: "details" | "delete" | "credentials" | "cashouts";
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
const { state: credentials } = useBackendState();
- const nonAdminUser = credentials.status !== "loggedIn" ? false : !credentials.isUserAdministrator
- return <div>
- <div class="sm:hidden">
- <label for="tabs" class="sr-only"><i18n.Translate>Select a section</i18n.Translate></label>
- <select id="tabs" name="tabs" class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500" onChange={(e) => {
- const op = e.currentTarget.value as typeof current
- switch (op) {
- case "details": {
- window.location.href = "#/my-profile";
- return;
- }
- case "delete": {
- window.location.href = "#/delete-my-account";
- return;
- }
- case "credentials": {
- window.location.href = "#/my-password";
- return;
- }
- case "cashouts": {
- window.location.href = "#/my-cashouts";
- return;
- }
- default: assertUnreachable(op)
- }
- }}>
- <option value="details" selected={current == "details"}><i18n.Translate>Details</i18n.Translate></option>
- {!config.allow_deletions ? undefined :
- <option value="delete" selected={current == "delete"}><i18n.Translate>Delete</i18n.Translate></option>
- }
- <option value="credentials" selected={current == "credentials"}><i18n.Translate>Credentials</i18n.Translate></option>
- {config.allow_conversion ?
- <option value="cashouts" selected={current == "cashouts"}><i18n.Translate>Cashouts</i18n.Translate></option>
- : undefined}
- </select>
- </div>
- <div class="hidden sm:block">
- <nav class="isolate flex divide-x divide-gray-200 rounded-lg shadow" aria-label="Tabs">
- <a href="#/my-profile" data-selected={current == "details"} class="rounded-l-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10" >
- <span><i18n.Translate>Details</i18n.Translate></span>
- <span aria-hidden="true" data-selected={current == "details"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
- </a>
- {!config.allow_deletions ? undefined :
- <a href="#/delete-my-account" data-selected={current == "delete"} aria-current="page" class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
- <span><i18n.Translate>Delete</i18n.Translate></span>
- <span aria-hidden="true" data-selected={current == "delete"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
+ const nonAdminUser =
+ credentials.status !== "loggedIn"
+ ? false
+ : !credentials.isUserAdministrator;
+ return (
+ <div>
+ <div class="sm:hidden">
+ <label for="tabs" class="sr-only">
+ <i18n.Translate>Select a section</i18n.Translate>
+ </label>
+ <select
+ id="tabs"
+ name="tabs"
+ class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
+ onChange={(e) => {
+ const op = e.currentTarget.value as typeof current;
+ switch (op) {
+ case "details": {
+ window.location.href = privatePages.myAccountDetails.url({});
+ return;
+ }
+ case "delete": {
+ window.location.href = privatePages.myAccountDelete.url({});
+ return;
+ }
+ case "credentials": {
+ window.location.href = privatePages.myAccountPassword.url({});
+ return;
+ }
+ case "cashouts": {
+ window.location.href = privatePages.myAccountCashouts.url({});
+ return;
+ }
+ default:
+ assertUnreachable(op);
+ }
+ }}
+ >
+ <option value="details" selected={current == "details"}>
+ <i18n.Translate>Details</i18n.Translate>
+ </option>
+ {!config.allow_deletions ? undefined : (
+ <option value="delete" selected={current == "delete"}>
+ <i18n.Translate>Delete</i18n.Translate>
+ </option>
+ )}
+ <option value="credentials" selected={current == "credentials"}>
+ <i18n.Translate>Credentials</i18n.Translate>
+ </option>
+ {config.allow_conversion ? (
+ <option value="cashouts" selected={current == "cashouts"}>
+ <i18n.Translate>Cashouts</i18n.Translate>
+ </option>
+ ) : undefined}
+ </select>
+ </div>
+ <div class="hidden sm:block">
+ <nav
+ class="isolate flex divide-x divide-gray-200 rounded-lg shadow"
+ aria-label="Tabs"
+ >
+ <a
+ href={privatePages.myAccountDetails.url({})}
+ data-selected={current == "details"}
+ class="rounded-l-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Details</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={current == "details"}
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
</a>
- }
- <a href="#/my-password" data-selected={current == "credentials"} aria-current="page" class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
- <span><i18n.Translate>Credentials</i18n.Translate></span>
- <span aria-hidden="true" data-selected={current == "credentials"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
- </a>
- {config.allow_conversion && nonAdminUser ?
- <a href="#/my-cashouts" data-selected={current == "cashouts"} class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
- <span><i18n.Translate>Cashouts</i18n.Translate></span>
- <span aria-hidden="true" data-selected={current == "cashouts"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
+ {!config.allow_deletions ? undefined : (
+ <a
+ href={privatePages.myAccountDelete.url({})}
+ data-selected={current == "delete"}
+ aria-current="page"
+ class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Delete</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={current == "delete"}
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </a>
+ )}
+ <a
+ href={privatePages.myAccountPassword.url({})}
+ data-selected={current == "credentials"}
+ aria-current="page"
+ class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Credentials</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={current == "credentials"}
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
</a>
- : undefined}
- </nav>
+ {config.allow_conversion && nonAdminUser ? (
+ <a
+ href={privatePages.myAccountCashouts.url({})}
+ data-selected={current == "cashouts"}
+ class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Cashouts</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={current == "cashouts"}
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </a>
+ ) : undefined}
+ </nav>
+ </div>
</div>
- </div>
-} \ No newline at end of file
+ );
+}
diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
index b3e18a62e..0f951b1a8 100644
--- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
@@ -14,37 +14,35 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Logger, TalerError } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { TalerError } from "@gnu-taler/taler-util";
+import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { Loading } from "@gnu-taler/web-util/browser";
import { Transactions } from "../components/Transactions/index.js";
import { usePublicAccounts } from "../hooks/access.js";
-const logger = new Logger("PublicHistoriesPage");
-
-interface Props { }
-
-/**
+/**
* Show histories of public accounts.
*/
-export function PublicHistoriesPage({ }: Props): VNode {
+export function PublicHistoriesPage(): VNode {
const { i18n } = useTranslationContext();
- //TODO: implemented filter by account name
+ // TODO: implemented filter by account name
const result = usePublicAccounts(undefined);
- const firstAccount = result && !(result instanceof TalerError) && result.data.public_accounts.length > 0
- ? result.data.public_accounts[0].username
- : undefined;
+ const firstAccount =
+ result &&
+ !(result instanceof TalerError) &&
+ result.data.public_accounts.length > 0
+ ? result.data.public_accounts[0].username
+ : undefined;
const [showAccount, setShowAccount] = useState(firstAccount);
if (!result) {
- return <Loading />
+ return <Loading />;
}
if (result instanceof TalerError) {
- return <Loading />
+ return <Loading />;
}
const { data } = result;
@@ -54,7 +52,6 @@ export function PublicHistoriesPage({ }: Props): VNode {
// Ask story of all the public accounts.
for (const account of data.public_accounts) {
- logger.trace("Asking transactions for", account.username);
const isSelected = account.username == showAccount;
accountsBar.push(
<li
@@ -89,9 +86,6 @@ export function PublicHistoriesPage({ }: Props): VNode {
<p>No public transactions found.</p>
)}
<br />
- <a href="/account" class="pure-button">
- Go back
- </a>
</div>
</article>
</section>
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index 14a1c410d..f21134aa1 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -15,22 +15,21 @@
*/
import {
+ assertUnreachable,
HttpStatusCode,
stringifyWithdrawUri,
TranslatedString,
- WithdrawUriResult
+ WithdrawUriResult,
} from "@gnu-taler/taler-util";
import {
+ LocalNotificationBanner,
useLocalNotification,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { useEffect } from "preact/hooks";
import { QR } from "../components/QR.js";
import { useBankCoreApiContext } from "../context/config.js";
-import { withRuntimeErrorHandling } from "../utils.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
-import { LocalNotificationBanner } from "@gnu-taler/web-util/browser";
import { useBackendState } from "../hooks/backend.js";
export function QrCodeSection({
@@ -43,54 +42,63 @@ export function QrCodeSection({
const { i18n } = useTranslationContext();
const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
const { state: credentials } = useBackendState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
useEffect(() => {
- //Taler Wallet WebExtension is listening to headers response and tab updates.
- //In the SPA there is no header response with the Taler URI so
- //this hack manually triggers the tab update after the QR is in the DOM.
+ // Taler Wallet WebExtension is listening to headers response and tab updates.
+ // In the SPA there is no header response with the Taler URI so
+ // this hack manually triggers the tab update after the QR is in the DOM.
// WebExtension will be using
// https://developer.chrome.com/docs/extensions/reference/tabs/#event-onUpdated
document.title = `${document.title} ${withdrawUri.withdrawalOperationId}`;
- const meta = document.createElement("meta")
- meta.setAttribute("name", "taler-uri")
- meta.setAttribute("content", talerWithdrawUri)
- document.head.insertBefore(meta, document.head.children.length ? document.head.children[0] : null)
+ const meta = document.createElement("meta");
+ meta.setAttribute("name", "taler-uri");
+ meta.setAttribute("content", talerWithdrawUri);
+ document.head.insertBefore(
+ meta,
+ document.head.children.length ? document.head.children[0] : null,
+ );
}, []);
- const [notification, notify, handleError] = useLocalNotification()
+ const [notification, notify, handleError] = useLocalNotification();
- const { api } = useBankCoreApiContext()
+ const { api } = useBankCoreApiContext();
async function doAbort() {
await handleError(async () => {
if (!creds) return;
- const resp = await api.abortWithdrawalById(creds, withdrawUri.withdrawalOperationId);
+ const resp = await api.abortWithdrawalById(
+ creds,
+ withdrawUri.withdrawalOperationId,
+ );
if (resp.type === "ok") {
onAborted();
} else {
switch (resp.case) {
- case HttpStatusCode.Conflict: return notify({
- type: "error",
- title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`
- })
- case HttpStatusCode.BadRequest: return notify({
- type: "error",
- title: i18n.str`The operation id is invalid.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`The operation was not found.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
+ case HttpStatusCode.Conflict:
+ return notify({
+ type: "error",
+ title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
default: {
- assertUnreachable(resp)
+ assertUnreachable(resp);
}
}
}
- })
+ });
}
return (
@@ -99,22 +107,37 @@ export function QrCodeSection({
<div class="bg-white shadow-xl sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>If you have a Taler wallet installed in this device</i18n.Translate>
+ <i18n.Translate>
+ If you have a Taler wallet installed in this device
+ </i18n.Translate>
</h3>
<div class="mt-4 mb-4 text-sm text-gray-500">
- <p><i18n.Translate>
- You will see the details of the operation in your wallet including the fees (if applies).
- If you still don't have one you can install it following instructions in</i18n.Translate> <a class="font-semibold text-gray-500 hover:text-gray-400" href="https://taler.net/en/wallet.html"><i18n.Translate>this page</i18n.Translate></a>.</p>
+ <p>
+ <i18n.Translate>
+ You will see the details of the operation in your wallet
+ including the fees (if applies). If you still don't have one you
+ can install it following instructions in
+ </i18n.Translate>{" "}
+ <a
+ class="font-semibold text-gray-500 hover:text-gray-400"
+ href="https://taler.net/en/wallet.html"
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ .
+ </p>
</div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 pt-2 mt-2 ">
- <button type="button"
+ <button
+ type="button"
// class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md px-3 py-2 text-sm font-semibold text-black shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
class="text-sm font-semibold leading-6 text-gray-900"
onClick={doAbort}
>
- Cancel
+ <i18n.Translate>Cancel</i18n.Translate>
</button>
- <a href={talerWithdrawUri}
+ <a
+ href={talerWithdrawUri}
class="inline-flex items-center 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"
>
<i18n.Translate>Withdraw</i18n.Translate>
@@ -126,28 +149,30 @@ export function QrCodeSection({
<div class="bg-white shadow-xl sm:rounded-lg mt-8">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>Or if you have the wallet in another device</i18n.Translate>
+ <i18n.Translate>
+ Or if you have the wallet in another device
+ </i18n.Translate>
</h3>
<div class="mt-4 max-w-xl text-sm text-gray-500">
- <i18n.Translate>Scan the QR below to start the withdrawal.</i18n.Translate>
+ <i18n.Translate>
+ Scan the QR below to start the withdrawal.
+ </i18n.Translate>
</div>
<div class="mt-2 max-w-md ml-auto mr-auto">
<QR text={talerWithdrawUri} />
</div>
</div>
<div class="flex items-center justify-center gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- <button type="button"
+ <button
+ type="button"
// class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md px-3 py-2 text-sm font-semibold text-black shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
class="text-sm font-semibold leading-6 text-gray-900"
onClick={doAbort}
>
- Cancel
+ <i18n.Translate>Cancel</i18n.Translate>
</button>
</div>
</div>
-
</Fragment>
);
}
-
-
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index b3a49a178..931a9b700 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -13,29 +13,33 @@
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 { AccessToken, HttpStatusCode, Logger, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
+import {
+ AccessToken,
+ HttpStatusCode,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
import {
LocalNotificationBanner,
ShowInputErrorLabel,
useLocalNotification,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { useBankCoreApiContext } from "../context/config.js";
-import { useBackendState } from "../hooks/backend.js";
+import { useSettingsContext } from "../context/settings.js";
+import { RouteDefinition } from "../route.js";
import { undefinedIfEmpty } from "../utils.js";
import { getRandomPassword, getRandomUsername } from "./rnd.js";
-import { useSettingsContext } from "../context/settings.js";
-
-const logger = new Logger("RegistrationPage");
export function RegistrationPage({
- onComplete,
- onCancel
+ onRegistrationSuccesful,
+ routeCancel,
}: {
- onComplete: () => void;
- onCancel: () => void;
+ onRegistrationSuccesful: (user: string, password: string) => void;
+ routeCancel: RouteDefinition<Record<string, never>>;
}): VNode {
const { i18n } = useTranslationContext();
const { config } = useBankCoreApiContext();
@@ -44,50 +48,58 @@ export function RegistrationPage({
<p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
);
}
- return <RegistrationForm onComplete={onComplete} onCancel={onCancel} />;
+ return (
+ <RegistrationForm
+ onRegistrationSuccesful={onRegistrationSuccesful}
+ routeCancel={routeCancel}
+ />
+ );
}
export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9-]*$/;
export const PHONE_REGEX = /^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$/;
-export const EMAIL_REGEX = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
+export const EMAIL_REGEX = /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/;
-/**
+/**
* Collect and submit registration data.
*/
-function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, onCancel: () => void }): VNode {
- const backend = useBackendState();
+function RegistrationForm({
+ onRegistrationSuccesful,
+ routeCancel,
+}: {
+ onRegistrationSuccesful: (user: string, password: string) => void;
+ routeCancel: RouteDefinition<Record<string, never>>;
+}): VNode {
const [username, setUsername] = useState<string | undefined>();
const [name, setName] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
- const [phone, setPhone] = useState<string | undefined>();
- const [email, setEmail] = useState<string | undefined>();
+ // const [phone, setPhone] = useState<string | undefined>();
+ // const [email, setEmail] = useState<string | undefined>();
const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
- const [notification, notify, handleError] = useLocalNotification()
+ const [notification, notify, handleError] = useLocalNotification();
const settings = useSettingsContext();
- const { api } = useBankCoreApiContext()
+ const { api } = useBankCoreApiContext();
// const { register } = useTestingAPI();
const { i18n } = useTranslationContext();
const errors = undefinedIfEmpty({
- name: !name
- ? i18n.str`Missing name`
- : undefined,
+ name: !name ? i18n.str`Missing name` : undefined,
username: !username
? i18n.str`Missing username`
: !USERNAME_REGEX.test(username)
? i18n.str`Use letters and numbers only, and start with a lowercase letter`
: undefined,
- phone: !phone
- ? undefined
- : !PHONE_REGEX.test(phone)
- ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
- : undefined,
- email: !email
- ? undefined
- : !EMAIL_REGEX.test(email)
- ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
- : undefined,
+ // phone: !phone
+ // ? undefined
+ // : !PHONE_REGEX.test(phone)
+ // ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ // : undefined,
+ // email: !email
+ // ? undefined
+ // : !EMAIL_REGEX.test(email)
+ // ? i18n.str`Use letters and numbers only, and start with a lowercase letter`
+ // : undefined,
password: !password ? i18n.str`Missing password` : undefined,
repeatPassword: !repeatPassword
? i18n.str`Missing password`
@@ -96,106 +108,97 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
: undefined,
});
- async function doRegistrationAndLogin(name: string, username: string, password: string, onComplete: () => void) {
+ async function doRegistrationAndLogin(
+ name: string,
+ username: string,
+ password: string,
+ onComplete: () => void,
+ ) {
await handleError(async () => {
- createAccount: {
- const resp = await api.createAccount("" as AccessToken, { name, username, password });
- if (resp.type === "fail") {
- switch (resp.case) {
- case HttpStatusCode.BadRequest: return notify({
+ const resp = await api.createAccount("" as AccessToken, {
+ name,
+ username,
+ password,
+ });
+ if (resp.type === "ok") {
+ onComplete();
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.BadRequest:
+ return notify({
type: "error",
title: i18n.str`Server replied with invalid phone or email.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
type: "error",
title: i18n.str`Registration is disabled because the bank ran out of bonus credit.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- case HttpStatusCode.Unauthorized: return notify({
+ });
+ case HttpStatusCode.Unauthorized:
+ return notify({
type: "error",
title: i18n.str`No enough permission to create that account.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: return notify({
+ });
+ case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
+ return notify({
type: "error",
title: i18n.str`That account id is already taken.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: return notify({
+ });
+ case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
+ return notify({
type: "error",
title: i18n.str`That username is already taken.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: return notify({
+ });
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return notify({
type: "error",
title: i18n.str`That username can't be used because is reserved.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return notify({
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return notify({
type: "error",
title: i18n.str`Only admin is allow to set debt limit.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- case TalerErrorCode.BANK_MISSING_TAN_INFO: return notify({
+ });
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return notify({
type: "error",
title: i18n.str`No information for the selected authentication channel.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: return notify({
+ });
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return notify({
type: "error",
title: i18n.str`Authentication channel is not supported.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: return notify({
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
+ return notify({
type: "error",
title: i18n.str`Only admin can create accounts with second factor authentication.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
- default: assertUnreachable(resp)
- }
- }
- }
- login: {
- const resp = await api.getAuthenticationAPI(username).createAccessToken(password, {
- scope: "readwrite",
- duration: { d_us: "forever" },
- refreshable: true,
- })
-
- if (resp.type === "ok") {
- backend.logIn({ username, token: resp.body.access_token });
- } else {
- switch (resp.case) {
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- title: i18n.str`Wrong credentials for "${username}"`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`Account not found`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: assertUnreachable(resp)
- }
+ });
+ default:
+ assertUnreachable(resp);
}
-
}
- onComplete()
- })
+ });
}
async function doRegistrationStep() {
@@ -204,18 +207,21 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
setUsername(undefined);
setPassword(undefined);
setRepeatPassword(undefined);
- onComplete();
- })
+ onRegistrationSuccesful(username, password);
+ });
}
- async function doRandomRegistration(tries: number = 3) {
+ async function doRandomRegistration() {
const user = getRandomUsername();
- const pass = settings.simplePasswordForRandomAccounts ? "123" : getRandomPassword();
- const username = `_${user.first}-${user.second}_`
- const name = `${user.first}, ${user.second}`
- await doRegistrationAndLogin(name, username, pass, onComplete)
-
+ const password = settings.simplePasswordForRandomAccounts
+ ? "123"
+ : getRandomPassword();
+ const username = `_${user.first}-${user.second}_`;
+ const name = `${user.first}, ${user.second}`;
+ await doRegistrationAndLogin(name, username, password, () => {
+ onRegistrationSuccesful(username, password);
+ });
}
return (
@@ -228,7 +234,9 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
</div>
<div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
- <form class="space-y-6" noValidate
+ <form
+ class="space-y-6"
+ noValidate
onSubmit={(e) => {
e.preventDefault();
}}
@@ -236,7 +244,10 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
autoCorrect="off"
>
<div>
- <label for="username" class="block text-sm font-medium leading-6 text-gray-900">
+ <label
+ for="username"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
<i18n.Translate>Username</i18n.Translate>
<b style={{ color: "red" }}> *</b>
</label>
@@ -265,7 +276,10 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
<div>
<div class="flex items-center justify-between">
- <label for="password" class="block text-sm font-medium leading-6 text-gray-900">
+ <label
+ for="password"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
<i18n.Translate>Password</i18n.Translate>
<b style={{ color: "red" }}> *</b>
</label>
@@ -294,7 +308,10 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
<div>
<div class="flex items-center justify-between">
- <label for="register-repeat" class="block text-sm font-medium leading-6 text-gray-900">
+ <label
+ for="register-repeat"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
<i18n.Translate>Repeat password</i18n.Translate>
<b style={{ color: "red" }}> *</b>
</label>
@@ -323,7 +340,10 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
<div>
<div class="flex items-center justify-between">
- <label for="name" class="block text-sm font-medium leading-6 text-gray-900">
+ <label
+ for="name"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >
<i18n.Translate>Name</i18n.Translate>
<b style={{ color: "red" }}> *</b>
</label>
@@ -403,50 +423,43 @@ function RegistrationForm({ onComplete, onCancel }: { onComplete: () => void, on
</div> */}
<div class="flex w-full justify-between">
- <button type="submit"
+ <a
+ href={routeCancel.url({})}
class="ring-1 ring-gray-600 rounded-md bg-white disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 text-black shadow-sm hover:bg-white-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
- onClick={(e) => {
- e.preventDefault()
- onCancel()
- }}
>
<i18n.Translate>Cancel</i18n.Translate>
- </button>
- <button type="submit"
+ </a>
+ <button
+ type="submit"
class=" rounded-md bg-indigo-600 disabled:bg-gray-300 px-3 py-1.5 text-sm font-semibold leading-6 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"
disabled={!!errors}
onClick={async (e) => {
- e.preventDefault()
+ e.preventDefault();
- doRegistrationStep()
+ doRegistrationStep();
}}
>
<i18n.Translate>Register</i18n.Translate>
</button>
</div>
-
</form>
- {settings.allowRandomAccountCreation &&
+ {settings.allowRandomAccountCreation && (
<p class="mt-10 text-center text-sm text-gray-500 border-t">
- <button type="submit"
+ <button
+ type="submit"
class="flex mt-4 w-full justify-center rounded-md bg-green-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600"
onClick={(e) => {
- e.preventDefault()
- doRandomRegistration()
+ e.preventDefault();
+ doRandomRegistration();
}}
>
<i18n.Translate>Create a random temporary user</i18n.Translate>
</button>
</p>
- }
+ )}
</div>
</div>
-
</Fragment>
);
}
-
-export function assertUnreachable(x: never): never {
- throw new Error("Didn't expect to get here");
-}
diff --git a/packages/demobank-ui/src/pages/SolveChallengePage.tsx b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
index 095a0f492..6d2d6512e 100644
--- a/packages/demobank-ui/src/pages/SolveChallengePage.tsx
+++ b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
@@ -18,13 +18,12 @@ import {
AbsoluteTime,
Amounts,
HttpStatusCode,
- Logger,
TalerCorebankApi,
TalerError,
TalerErrorCode,
TranslatedString,
assertUnreachable,
- parsePaytoUri
+ parsePaytoUri,
} from "@gnu-taler/taler-util";
import {
Attention,
@@ -32,7 +31,7 @@ import {
LocalNotificationBanner,
ShowInputErrorLabel,
useLocalNotification,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { Fragment, VNode, h } from "preact";
@@ -43,40 +42,41 @@ import { useWithdrawalDetails } from "../hooks/access.js";
import { useBackendState } from "../hooks/backend.js";
import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js";
import { useConversionInfo } from "../hooks/circuit.js";
+import { RouteDefinition } from "../route.js";
import { undefinedIfEmpty } from "../utils.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
import { OperationNotFound } from "./WithdrawalQRCode.js";
-const logger = new Logger("SolveChallenge");
-
export function SolveChallengePage({
- onContinue,
+ onChallengeCompleted,
+ routeClose,
}: {
- onContinue: () => void;
+ onChallengeCompleted: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
}): VNode {
- const { api } = useBankCoreApiContext()
+ const { api } = useBankCoreApiContext();
const { i18n } = useTranslationContext();
const [bankState, updateBankState] = useBankState();
const [code, setCode] = useState<string | undefined>(undefined);
- const [notification, notify, handleError] = useLocalNotification()
+ const [notification, notify, handleError] = useLocalNotification();
const { state } = useBackendState();
- const creds = state.status !== "loggedIn" ? undefined : state
+ const creds = state.status !== "loggedIn" ? undefined : state;
if (!bankState.currentChallenge) {
- return <div>
- <span>no challenge to solve </span>
- <button type="button"
- class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
- onClick={() => {
- onContinue()
- }}
- >
- <i18n.Translate>Continue</i18n.Translate>
- </button>
- </div>
+ return (
+ <div>
+ <span>no challenge to solve </span>
+ <a
+ href={routeClose.url({})}
+ class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </a>
+ </div>
+ );
}
- const ch = bankState.currentChallenge
+ const ch = bankState.currentChallenge;
const errors = undefinedIfEmpty({
code: !code ? i18n.str`required` : undefined,
});
@@ -86,34 +86,38 @@ export function SolveChallengePage({
await handleError(async () => {
const resp = await api.sendChallenge(creds, ch.id);
if (resp.type === "ok") {
- const newCh = structuredClone(ch)
- newCh.sent = AbsoluteTime.now()
- newCh.info = resp.body
- updateBankState("currentChallenge", newCh)
+ const newCh = structuredClone(ch);
+ newCh.sent = AbsoluteTime.now();
+ newCh.info = resp.body;
+ updateBankState("currentChallenge", newCh);
} else {
switch (resp.case) {
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- 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,
- })
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- 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,
- })
- case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return notify({
- type: "error",
- 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,
- })
- default: assertUnreachable(resp)
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ 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,
+ });
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ 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,
+ });
+ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
+ return notify({
+ type: "error",
+ 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,
+ });
+ default:
+ assertUnreachable(resp);
}
}
- })
+ });
}
async function completeChallenge() {
@@ -121,55 +125,68 @@ export function SolveChallengePage({
await handleError(async () => {
{
const resp = await api.confirmChallenge(creds, ch.id, {
- tan: code
+ tan: code,
});
if (resp.type === "fail") {
- setCode("")
+ setCode("");
switch (resp.case) {
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`Challenge not found.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- title: i18n.str`This user is not authorized to complete this challenge.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.TooManyRequests: return notify({
- type: "error",
- title: i18n.str`Too many attemps, try another code.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED: return notify({
- type: "error",
- title: i18n.str`The confirmation code is wrong, try again.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: return notify({
- type: "error",
- title: i18n.str`The operation expired.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: assertUnreachable(resp)
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Challenge not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`This user is not authorized to complete this challenge.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.TooManyRequests:
+ return notify({
+ type: "error",
+ title: i18n.str`Too many attemps, try another code.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED:
+ return notify({
+ type: "error",
+ title: i18n.str`The confirmation code is wrong, try again.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation expired.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ default:
+ assertUnreachable(resp);
}
}
}
{
const resp = await (async (ch: ChallengeInProgess) => {
switch (ch.operation) {
- case "delete-account": return await api.deleteAccount(creds, ch.id)
- case "update-account": return await api.updateAccount(creds, ch.request, ch.id)
- case "update-password": return await api.updatePassword(creds, ch.request, ch.id)
- case "create-transaction": return await api.createTransaction(creds, ch.request, ch.id)
- case "confirm-withdrawal": return await api.confirmWithdrawalById(creds, ch.request, ch.id)
- case "create-cashout": return await api.createCashout(creds, ch.request, ch.id)
- default: assertUnreachable(ch)
+ case "delete-account":
+ return await api.deleteAccount(creds, ch.id);
+ case "update-account":
+ return await api.updateAccount(creds, ch.request, ch.id);
+ case "update-password":
+ return await api.updatePassword(creds, ch.request, ch.id);
+ case "create-transaction":
+ return await api.createTransaction(creds, ch.request, ch.id);
+ case "confirm-withdrawal":
+ return await api.confirmWithdrawalById(creds, ch.request, ch.id);
+ case "create-cashout":
+ return await api.createCashout(creds, ch.request, ch.id);
+ default:
+ assertUnreachable(ch);
}
})(ch);
@@ -180,36 +197,43 @@ export function SolveChallengePage({
title: i18n.str`The operation failed.`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
+ });
}
- // another challenge required
+ // another challenge required, save the request and the ID
+ // @ts-expect-error no need to check the type of request, since it will be the same as the previous request
updateBankState("currentChallenge", {
operation: ch.operation,
id: String(resp.body.challenge_id),
sent: AbsoluteTime.never(),
- request: ch.request as any,
- })
+ request: ch.request,
+ });
return notify({
type: "info",
title: i18n.str`The operation needs another confirmation to complete.`,
- })
+ });
}
- updateBankState("currentChallenge", undefined)
- return onContinue()
+ updateBankState("currentChallenge", undefined);
+ return onChallengeCompleted();
}
- })
+ });
}
const subtitle = ((op): TranslatedString => {
switch (op) {
- case "delete-account": return i18n.str`Account delete`
- case "update-account": return i18n.str`Account update`
- case "update-password": return i18n.str`Password update`
- case "create-transaction": return i18n.str`Wire transfer`
- case "confirm-withdrawal": return i18n.str`Withdrawal`
- case "create-cashout": return i18n.str`Cashout`
+ case "delete-account":
+ return i18n.str`Account delete`;
+ case "update-account":
+ return i18n.str`Account update`;
+ case "update-password":
+ return i18n.str`Password update`;
+ case "create-transaction":
+ return i18n.str`Wire transfer`;
+ case "confirm-withdrawal":
+ return i18n.str`Withdrawal`;
+ case "create-cashout":
+ return i18n.str`Cashout`;
}
- })(ch.operation)
+ })(ch.operation);
return (
<Fragment>
@@ -217,25 +241,29 @@ export function SolveChallengePage({
<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">
- <span class="text-sm text-black font-semibold leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Confirm the operation</i18n.Translate>
</span>
</h2>
- <span>
- {subtitle}
- </span>
+ <span>{subtitle}</span>
</div>
<div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
- <ChallengeDetails challenge={bankState.currentChallenge} onStart={startChallenge} />
- {ch.info &&
+ <ChallengeDetails
+ challenge={bankState.currentChallenge}
+ onStart={startChallenge}
+ />
+ {ch.info && (
<div class="mt-3 text-sm leading-6">
<form
class="bg-white shadow-sm ring-1 ring-gray-900/5"
autoCapitalize="none"
autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
+ onSubmit={(e) => {
+ e.preventDefault();
}}
>
<div class="px-4 py-6 sm:p-8">
@@ -252,314 +280,408 @@ export function SolveChallengePage({
class="block w-full 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"
value={code ?? ""}
required
-
name="answer"
id="answer"
autocomplete="off"
onChange={(e): void => {
- setCode(e.currentTarget.value)
+ setCode(e.currentTarget.value);
}}
/>
</div>
- <ShowInputErrorLabel message={errors?.code} isDirty={code !== undefined} />
+ <ShowInputErrorLabel
+ message={errors?.code}
+ isDirty={code !== undefined}
+ />
</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">
- <button type="button"
+ <a
+ href={routeClose.url({})}
class="inline-flex items-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
- onClick={() => {
- updateBankState("currentChallenge", undefined)
- onContinue()
- }}
>
<i18n.Translate>Cancel</i18n.Translate>
- </button>
- <button type="submit"
+ </a>
+ <button
+ type="submit"
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"
disabled={!!errors}
onClick={(e) => {
- completeChallenge()
+ completeChallenge();
+ e.preventDefault();
}}
>
<i18n.Translate>Confirm</i18n.Translate>
</button>
</div>
</form>
-
- {/* <ShouldBeSameUser username={details.username}> */}
- {/* </ShouldBeSameUser> */}
</div>
- }
+ )}
</div>
</div>
</Fragment>
-
);
}
-function ChallengeDetails({ challenge, onStart }: { challenge: ChallengeInProgess, onStart: () => void }): VNode {
+function ChallengeDetails({
+ challenge,
+ onStart,
+}: {
+ challenge: ChallengeInProgess;
+ onStart: () => void;
+}): VNode {
const { i18n, dateLocale } = useTranslationContext();
const { config } = useBankCoreApiContext();
- return <div class="px-4 mt-4 ">
- <div class="w-full">
- <div class="flex justify-center">
-
- {challenge.info ?
- <button type="submit"
- 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={(e) => {
- onStart()
- }}
- >
- <i18n.Translate>Send again</i18n.Translate>
- </button>
- :
- <button type="submit"
- 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={(e) => {
- onStart()
- }}
- >
- <i18n.Translate>Send code</i18n.Translate>
- </button>
- }
- </div>
- <div class="mt-6 border-t border-gray-100">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- <span class="text-sm text-black font-semibold leading-6 " id="availability-label">
- <i18n.Translate>Operation details</i18n.Translate>
- </span>
- </h2>
- <dl class="divide-y divide-gray-100">
- {((): VNode => {
- switch (challenge.operation) {
- case "delete-account": 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">Account</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{challenge.request}</dd>
- </div>
- case "create-transaction": {
- const payto = parsePaytoUri(challenge.request.payto_uri)!
- return <Fragment>
- {challenge.request.amount &&
- <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount value={Amounts.parseOrThrow(challenge.request.amount)} spec={config.currency_specification} />
- </dd>
- </div>
- }
- {payto.isKnown && payto.targetType === "iban" &&
- <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">To account</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {payto.iban}
- </dd>
- </div>
- }
- </Fragment>
- }
- case "confirm-withdrawal": return <ShowWithdrawalDetails id={challenge.request} />
- case "create-cashout": {
- return <ShowCashoutDetails request={challenge.request} />
- }
- case "update-account": {
- return <Fragment>
- {challenge.request.cashout_payto_uri !== undefined &&
- <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">Cashout account</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.request.cashout_payto_uri}
- </dd>
- </div>
- }
- {challenge.request.contact_data?.email !== undefined &&
- <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">Email</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.request.contact_data?.email}
- </dd>
- </div>
- }
- {challenge.request.contact_data?.phone !== undefined &&
- <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">Phone</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.request.contact_data?.phone}
- </dd>
- </div>
- }
- {challenge.request.debit_threshold !== undefined &&
- <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">Debit threshold</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount value={Amounts.parseOrThrow(challenge.request.debit_threshold)} spec={config.currency_specification} />
- </dd>
- </div>
- }
- {challenge.request.is_public !== undefined &&
- <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">Is this account public?</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.request.is_public ? "enable" : "disable"}
- </dd>
- </div>
- }
- {challenge.request.name !== undefined &&
- <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">Name</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.request.name}
- </dd>
- </div>
- }
- {challenge.request.tan_channel !== undefined &&
+ return (
+ <div class="px-4 mt-4 ">
+ <div class="w-full">
+ <div class="flex justify-center">
+ {challenge.info ? (
+ <button
+ type="submit"
+ 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={(e) => {
+ onStart();
+ e.preventDefault();
+ }}
+ >
+ <i18n.Translate>Send again</i18n.Translate>
+ </button>
+ ) : (
+ <button
+ type="submit"
+ 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={(e) => {
+ onStart();
+ e.preventDefault();
+ }}
+ >
+ <i18n.Translate>Send code</i18n.Translate>
+ </button>
+ )}
+ </div>
+ <div class="mt-6 border-t border-gray-100">
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Operation details</i18n.Translate>
+ </span>
+ </h2>
+ <dl class="divide-y divide-gray-100">
+ {((): VNode => {
+ switch (challenge.operation) {
+ case "delete-account":
+ 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">Authentication channel</dt>
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ Account
+ </dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.request.tan_channel}
+ {challenge.request}
</dd>
</div>
- }
- </Fragment>
+ );
+ case "create-transaction": {
+ const payto = parsePaytoUri(challenge.request.payto_uri)!;
+ return (
+ <Fragment>
+ {challenge.request.amount && (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ Amount
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={Amounts.parseOrThrow(
+ challenge.request.amount,
+ )}
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ {payto.isKnown && payto.targetType === "iban" && (
+ <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">
+ To account
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {payto.iban}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ case "confirm-withdrawal":
+ return <ShowWithdrawalDetails id={challenge.request} />;
+ case "create-cashout": {
+ return <ShowCashoutDetails request={challenge.request} />;
+ }
+ case "update-account": {
+ return (
+ <Fragment>
+ {challenge.request.cashout_payto_uri !== undefined && (
+ <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">
+ Cashout account
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.cashout_payto_uri}
+ </dd>
+ </div>
+ )}
+ {challenge.request.contact_data?.email !== undefined && (
+ <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">
+ Email
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.contact_data?.email}
+ </dd>
+ </div>
+ )}
+ {challenge.request.contact_data?.phone !== undefined && (
+ <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">
+ Phone
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.contact_data?.phone}
+ </dd>
+ </div>
+ )}
+ {challenge.request.debit_threshold !== undefined && (
+ <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">
+ Debit threshold
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={Amounts.parseOrThrow(
+ challenge.request.debit_threshold,
+ )}
+ spec={config.currency_specification}
+ />
+ </dd>
+ </div>
+ )}
+ {challenge.request.is_public !== undefined && (
+ <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">
+ Is this account public?
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.is_public ? "enable" : "disable"}
+ </dd>
+ </div>
+ )}
+ {challenge.request.name !== undefined && (
+ <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">
+ Name
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.name}
+ </dd>
+ </div>
+ )}
+ {challenge.request.tan_channel !== undefined && (
+ <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">
+ Authentication channel
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.tan_channel}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
+ }
+ case "update-password": {
+ return (
+ <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">
+ New password
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.request.new_password}
+ </dd>
+ </div>
+ </Fragment>
+ );
+ }
+ default:
+ assertUnreachable(challenge);
}
- case "update-password": {
- return <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">New password</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.request.new_password}
- </dd>
- </div>
- </Fragment>
- }
- default: assertUnreachable(challenge)
- }
- })()}
+ })()}
- {challenge.info &&
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- <span class="text-sm text-black font-semibold leading-6 " id="availability-label">
- <i18n.Translate>Challenge details</i18n.Translate>
- </span>
- </h2>
- }
- {challenge.sent.t_ms !== "never" &&
- <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>Sent at</i18n.Translate></dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {format(challenge.sent.t_ms, "dd/MM/yyyy HH:mm:ss", { locale: dateLocale })}
- </dd>
- </div>
- }
- {challenge.info &&
- <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">
- {((ch: TalerCorebankApi.TanChannel): VNode => {
- switch (ch) {
- case TalerCorebankApi.TanChannel.SMS: return <i18n.Translate>To phone</i18n.Translate>
- case TalerCorebankApi.TanChannel.EMAIL: return <i18n.Translate>To email</i18n.Translate>
- default: assertUnreachable(ch)
- }
- })(challenge.info.tan_channel)}
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.info.tan_info}
- </dd>
- </div>
- }
-
- </dl>
+ {challenge.info && (
+ <h2 class="text-base font-semibold leading-7 text-gray-900">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Challenge details</i18n.Translate>
+ </span>
+ </h2>
+ )}
+ {challenge.sent.t_ms !== "never" && (
+ <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>Sent at</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {format(challenge.sent.t_ms, "dd/MM/yyyy HH:mm:ss", {
+ locale: dateLocale,
+ })}
+ </dd>
+ </div>
+ )}
+ {challenge.info && (
+ <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">
+ {((ch: TalerCorebankApi.TanChannel): VNode => {
+ switch (ch) {
+ case TalerCorebankApi.TanChannel.SMS:
+ return <i18n.Translate>To phone</i18n.Translate>;
+ case TalerCorebankApi.TanChannel.EMAIL:
+ return <i18n.Translate>To email</i18n.Translate>;
+ default:
+ assertUnreachable(ch);
+ }
+ })(challenge.info.tan_channel)}
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {challenge.info.tan_info}
+ </dd>
+ </div>
+ )}
+ </dl>
+ </div>
</div>
</div>
- </div>
+ );
}
function ShowWithdrawalDetails({ id }: { id: string }): VNode {
- const { i18n } = useTranslationContext();
- const details = useWithdrawalDetails(id)
+ const details = useWithdrawalDetails(id);
const { config } = useBankCoreApiContext();
if (!details) {
- return <Loading />
+ return <Loading />;
}
if (details instanceof TalerError) {
- return <ErrorLoadingWithDebug error={details} />
+ return <ErrorLoadingWithDebug error={details} />;
}
if (details.type === "fail") {
switch (details.case) {
case HttpStatusCode.BadRequest:
- case HttpStatusCode.NotFound: return <OperationNotFound onClose={undefined} />
- default: assertUnreachable(details)
+ case HttpStatusCode.NotFound:
+ return <OperationNotFound routeClose={undefined} />;
+ default:
+ assertUnreachable(details);
}
}
- return <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">Amount</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount value={Amounts.parseOrThrow(details.body.amount)} spec={config.currency_specification} />
- </dd>
- </div>
- {details.body.selected_reserve_pub !== undefined &&
- <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">Withdraw id</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0" title={details.body.selected_reserve_pub}>
- {details.body.selected_reserve_pub.substring(0, 16)}...
- </dd>
- </div>
- }
- {details.body.selected_exchange_account !== undefined &&
+ return (
+ <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">To account</dt>
+ <dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {details.body.selected_exchange_account}
+ <RenderAmount
+ value={Amounts.parseOrThrow(details.body.amount)}
+ spec={config.currency_specification}
+ />
</dd>
</div>
- }
- </Fragment>
+ {details.body.selected_reserve_pub !== undefined && (
+ <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">
+ Withdraw id
+ </dt>
+ <dd
+ class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0"
+ title={details.body.selected_reserve_pub}
+ >
+ {details.body.selected_reserve_pub.substring(0, 16)}...
+ </dd>
+ </div>
+ )}
+ {details.body.selected_exchange_account !== undefined && (
+ <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">
+ To account
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.body.selected_exchange_account}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
}
-function ShowCashoutDetails({ request }: { request: TalerCorebankApi.CashoutRequest }): VNode {
+function ShowCashoutDetails({
+ request,
+}: {
+ request: TalerCorebankApi.CashoutRequest;
+}): VNode {
const { i18n } = useTranslationContext();
const info = useConversionInfo();
if (!info) {
- return <Loading />
+ return <Loading />;
}
if (info instanceof TalerError) {
- return <ErrorLoadingWithDebug error={info} />
+ return <ErrorLoadingWithDebug error={info} />;
}
if (info.type === "fail") {
switch (info.case) {
case HttpStatusCode.NotImplemented: {
- return <Attention type="danger" title={i18n.str`Cashout not implemented`}>
- </Attention>;
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`Cashout not implemented`}
+ ></Attention>
+ );
}
- default: assertUnreachable(info.case)
+ default:
+ assertUnreachable(info.case);
}
}
-
- return <Fragment>
- {request.subject !== undefined &&
+ return (
+ <Fragment>
+ {request.subject !== undefined && (
+ <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">Subject</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {request.subject}
+ </dd>
+ </div>
+ )}
<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">Subject</dt>
+ <dt class="text-sm font-medium leading-6 text-gray-900">Debit</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {request.subject}
+ <RenderAmount
+ value={Amounts.parseOrThrow(request.amount_credit)}
+ spec={info.body.regional_currency_specification}
+ />
</dd>
</div>
- }
- <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">Debit</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount value={Amounts.parseOrThrow(request.amount_credit)} spec={info.body.regional_currency_specification} />
- </dd>
- </div>
- <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">Credit</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount value={Amounts.parseOrThrow(request.amount_credit)} spec={info.body.fiat_currency_specification} />
- </dd>
- </div>
- </Fragment>
-} \ No newline at end of file
+ <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">Credit</dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <RenderAmount
+ value={Amounts.parseOrThrow(request.amount_credit)}
+ spec={info.body.fiat_currency_specification}
+ />
+ </dd>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index c3d1c3f7e..1e48b818a 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -18,59 +18,73 @@ import {
AmountJson,
Amounts,
HttpStatusCode,
- Logger,
TranslatedString,
- parseWithdrawUri
+ assertUnreachable,
+ parseWithdrawUri,
} from "@gnu-taler/taler-util";
import {
+ Attention,
+ LocalNotificationBanner,
notifyError,
useLocalNotification,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import { VNode, h } from "preact";
import { forwardRef } from "preact/compat";
import { useState } from "preact/hooks";
-import { Attention } from "@gnu-taler/web-util/browser";
+import { privatePages } from "../Routing.js";
import { useBankCoreApiContext } from "../context/config.js";
import { useBackendState } from "../hooks/backend.js";
+import { useBankState } from "../hooks/bank-state.js";
import { usePreferences } from "../hooks/preferences.js";
-import { undefinedIfEmpty, withRuntimeErrorHandling } from "../utils.js";
+import { RouteDefinition } from "../route.js";
+import { undefinedIfEmpty } from "../utils.js";
import { OperationState } from "./OperationState/index.js";
import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
-import { LocalNotificationBanner } from "@gnu-taler/web-util/browser";
-import { useBankState } from "../hooks/bank-state.js";
-const logger = new Logger("WalletWithdrawForm");
const RefAmount = forwardRef(InputAmount);
-
-function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
+function OldWithdrawalForm({
+ onOperationCreated,
+ limit,
+ routeCancel,
+ focus,
+}: {
limit: AmountJson;
focus?: boolean;
- goToConfirmOperation: (operationId: string) => void;
- onCancel: () => void;
+ onOperationCreated: (wopid: string) => void;
+ routeCancel: RouteDefinition<Record<string, never>>;
}): VNode {
const { i18n } = useTranslationContext();
- const [settings] = usePreferences()
+ const [settings] = usePreferences();
const [bankState, updateBankState] = useBankState();
const { state: credentials } = useBackendState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
- const { api } = useBankCoreApiContext()
- const [amountStr, setAmountStr] = useState<string | undefined>(`${settings.maxWithdrawalAmount}`);
- const [notification, notify, handleError] = useLocalNotification()
-
- if (!!bankState.currentWithdrawalOperationId) {
- return <Attention type="warning" title={i18n.str`There is an operation already`}>
- <span ref={focus ? doAutoFocus : undefined} />
- <i18n.Translate>
- Complete or cancel the operation in</i18n.Translate> <a class="font-semibold text-yellow-700 hover:text-yellow-600" href={`#/operation/${bankState.currentWithdrawalOperationId}`}>
- <i18n.Translate>this page</i18n.Translate>
- </a>
+ const { api } = useBankCoreApiContext();
+ const [amountStr, setAmountStr] = useState<string | undefined>(
+ `${settings.maxWithdrawalAmount}`,
+ );
+ const [notification, notify, handleError] = useLocalNotification();
- </Attention>
+ if (bankState.currentWithdrawalOperationId) {
+ return (
+ <Attention type="warning" title={i18n.str`There is an operation already`}>
+ <span ref={focus ? doAutoFocus : undefined} />
+ <i18n.Translate>
+ Complete or cancel the operation in
+ </i18n.Translate>{" "}
+ <a
+ class="font-semibold text-yellow-700 hover:text-yellow-600"
+ href={privatePages.operationDetails.url({
+ wopid: bankState.currentWithdrawalOperationId,
+ })}
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </Attention>
+ );
}
const trimmedAmountStr = amountStr?.trim();
@@ -101,10 +115,14 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
if (!uri) {
return notifyError(
i18n.str`Server responded with an invalid withdraw URI`,
- i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`);
+ i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`,
+ );
} else {
- updateBankState("currentWithdrawalOperationId", uri.withdrawalOperationId)
- goToConfirmOperation(uri.withdrawalOperationId);
+ updateBankState(
+ "currentWithdrawalOperationId",
+ uri.withdrawalOperationId,
+ );
+ onOperationCreated(uri.withdrawalOperationId);
}
} else {
switch (resp.case) {
@@ -114,7 +132,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
title: i18n.str`The operation was rejected due to insufficient funds`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
+ });
break;
}
case HttpStatusCode.Unauthorized: {
@@ -123,7 +141,7 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
title: i18n.str`The operation was rejected due to insufficient funds`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
+ });
break;
}
case HttpStatusCode.NotFound: {
@@ -132,159 +150,184 @@ function OldWithdrawalForm({ goToConfirmOperation, limit, onCancel, focus }: {
title: i18n.str`Account not found`,
description: resp.detail.hint as TranslatedString,
debug: resp.detail,
- })
+ });
break;
}
- default: assertUnreachable(resp)
+ default:
+ assertUnreachable(resp);
}
}
- })
+ });
}
- return <form
- class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mt-4"
- autoCapitalize="none"
- autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
- }}
- >
- <LocalNotificationBanner notification={notification} />
+ return (
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2 mt-4"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <LocalNotificationBanner notification={notification} />
- <div class="px-4 py-6 ">
- <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label for="withdraw-amount">{i18n.str`Amount`}</label>
- <RefAmount
- currency={limit.currency}
- value={amountStr}
- name="withdraw-amount"
- onChange={(v) => {
- setAmountStr(v);
- }}
- error={errors?.amount}
- ref={focus ? doAutoFocus : undefined}
- />
+ <div class="px-4 py-6 ">
+ <div class="grid max-w-xs grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label for="withdraw-amount">{i18n.str`Amount`}</label>
+ <RefAmount
+ currency={limit.currency}
+ value={amountStr}
+ name="withdraw-amount"
+ onChange={(v) => {
+ setAmountStr(v);
+ }}
+ error={errors?.amount}
+ ref={focus ? doAutoFocus : undefined}
+ />
+ </div>
</div>
- </div>
- <div class="mt-4">
- <div class="sm:inline">
-
- <button type="button"
- class=" inline-flex px-6 py-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
- onClick={(e) => {
- e.preventDefault();
- setAmountStr("50.00")
- }}
- >
- 50.00
- </button>
- <button type="button"
- class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-r-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
- onClick={(e) => {
- e.preventDefault();
- setAmountStr("25.00")
- }}
- >
-
- 25.00
- </button>
- </div>
- <div class="mt-4 sm:inline">
- <button type="button"
- class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-l-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
- onClick={(e) => {
- e.preventDefault();
- setAmountStr("10.00")
- }}
- >
- 10.00
- </button>
- <button type="button"
- class=" inline-flex px-6 py-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
- onClick={(e) => {
- e.preventDefault();
- setAmountStr("5.00")
- }}
- >
- 5.00
- </button>
+ <div class="mt-4">
+ <div class="sm:inline">
+ <button
+ type="button"
+ class=" inline-flex px-6 py-4 text-sm items-center rounded-l-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("50.00");
+ }}
+ >
+ 50.00
+ </button>
+ <button
+ type="button"
+ class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-r-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("25.00");
+ }}
+ >
+ 25.00
+ </button>
+ </div>
+ <div class="mt-4 sm:inline">
+ <button
+ type="button"
+ class=" -ml-px -mr-px inline-flex px-6 py-4 text-sm items-center rounded-l-md sm:rounded-none bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("10.00");
+ }}
+ >
+ 10.00
+ </button>
+ <button
+ type="button"
+ class=" inline-flex px-6 py-4 text-sm items-center rounded-r-md bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10"
+ onClick={(e) => {
+ e.preventDefault();
+ setAmountStr("5.00");
+ }}
+ >
+ 5.00
+ </button>
+ </div>
</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">
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
- >
- <i18n.Translate>Cancel</i18n.Translate></button>
- <button type="submit"
- 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"
- // disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
- onClick={(e) => {
- e.preventDefault()
- doStart()
- }}
- >
- <i18n.Translate>Continue</i18n.Translate>
- </button>
- </div>
-
- </form>
+ <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
+ <a
+ href={routeCancel.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
+ 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"
+ // disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
+ onClick={(e) => {
+ e.preventDefault();
+ doStart();
+ }}
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </button>
+ </div>
+ </form>
+ );
}
-
export function WalletWithdrawForm({
focus,
limit,
- onCancel,
+ routeCancel,
onAuthorizationRequired,
- goToConfirmOperation,
+ onOperationCreated,
+ onOperationAborted,
}: {
limit: AmountJson;
focus?: boolean;
- onAuthorizationRequired: () => void,
- goToConfirmOperation: (operationId: string) => void;
- onCancel: () => void;
+ onAuthorizationRequired: () => void;
+ onOperationCreated: (wopid: string) => void;
+ onOperationAborted: () => void;
+ routeCancel: RouteDefinition<Record<string, never>>;
}): VNode {
const { i18n } = useTranslationContext();
- const [settings, updateSettings] = usePreferences()
+ const [settings, updateSettings] = usePreferences();
- return (<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>Prepare your wallet</i18n.Translate></h2>
- <p class="mt-1 text-sm text-gray-500">
- <i18n.Translate>After using your wallet you will need to confirm or cancel the operation on this site.</i18n.Translate>
- </p>
- </div>
-
- <div class="col-span-2">
- {settings.showInstallWallet &&
- <Attention title={i18n.str`You need a GNU Taler Wallet`} onClose={() => {
- updateSettings("showInstallWallet", false);
- }}>
+ return (
+ <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>Prepare your wallet</i18n.Translate>
+ </h2>
+ <p class="mt-1 text-sm text-gray-500">
<i18n.Translate>
- If you don't have one yet you can follow the instruction in</i18n.Translate> <a target="_blank" rel="noreferrer noopener" class="font-semibold text-blue-700 hover:text-blue-600" href="https://taler.net/en/wallet.html">
- <i18n.Translate>this page</i18n.Translate>
- </a>
- </Attention>
- }
+ After using your wallet you will need to confirm or cancel the
+ operation on this site.
+ </i18n.Translate>
+ </p>
+ </div>
- {!settings.fastWithdrawal ?
- <OldWithdrawalForm
- focus={focus}
- limit={limit}
- onCancel={onCancel}
- goToConfirmOperation={goToConfirmOperation}
- />
- :
- <OperationState
- currency={limit.currency}
- onAuthorizationRequired={onAuthorizationRequired}
- onClose={onCancel}
- />
- }
+ <div class="col-span-2">
+ {settings.showInstallWallet && (
+ <Attention
+ title={i18n.str`You need a GNU Taler Wallet`}
+ onClose={() => {
+ updateSettings("showInstallWallet", false);
+ }}
+ >
+ <i18n.Translate>
+ If you don't have one yet you can follow the instruction in
+ </i18n.Translate>{" "}
+ <a
+ target="_blank"
+ rel="noreferrer noopener"
+ class="font-semibold text-blue-700 hover:text-blue-600"
+ href="https://taler.net/en/wallet.html"
+ >
+ <i18n.Translate>this page</i18n.Translate>
+ </a>
+ </Attention>
+ )}
+
+ {!settings.fastWithdrawal ? (
+ <OldWithdrawalForm
+ focus={focus}
+ limit={limit}
+ routeCancel={routeCancel}
+ onOperationCreated={onOperationCreated}
+ />
+ ) : (
+ <OperationState
+ currency={limit.currency}
+ onAuthorizationRequired={onAuthorizationRequired}
+ routeClose={routeCancel}
+ onAbort={onOperationAborted}
+ // route={routeCancel}
+ />
+ )}
+ </div>
</div>
- </div>
);
}
-
diff --git a/packages/demobank-ui/src/pages/WireTransfer.tsx b/packages/demobank-ui/src/pages/WireTransfer.tsx
index 25d43a832..190afd66e 100644
--- a/packages/demobank-ui/src/pages/WireTransfer.tsx
+++ b/packages/demobank-ui/src/pages/WireTransfer.tsx
@@ -1,18 +1,47 @@
-import { Amounts, HttpStatusCode, TalerError } from "@gnu-taler/taler-util";
-import { Loading, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
+/*
+ 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 {
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Loading,
+ notifyInfo,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import { useAccountDetails } from "../hooks/access.js";
import { useBackendState } from "../hooks/backend.js";
import { LoginForm } from "./LoginForm.js";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
+import { RouteDefinition } from "../route.js";
-export function WireTransfer({ toAccount, onAuthorizationRequired, onCancel, onSuccess }: {
+export function WireTransfer({
+ toAccount,
+ onAuthorizationRequired,
+ routeCancel,
+ onSuccess,
+}: {
onSuccess?: () => void;
- toAccount?: string,
- onCancel?: () => void,
- onAuthorizationRequired: () => void,
+ toAccount?: string;
+ routeCancel?: RouteDefinition<Record<string, never>>;
+ onAuthorizationRequired: () => void;
}): VNode {
const { i18n } = useTranslationContext();
const r = useBackendState();
@@ -20,16 +49,19 @@ export function WireTransfer({ toAccount, onAuthorizationRequired, onCancel, onS
const result = useAccountDetails(account);
if (!result) {
- return <Loading />
+ return <Loading />;
}
if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />
+ return <ErrorLoadingWithDebug error={result} />;
}
if (result.type === "fail") {
switch (result.case) {
- case HttpStatusCode.Unauthorized: return <LoginForm currentUser={account} />
- case HttpStatusCode.NotFound: return <LoginForm currentUser={account} />
- default: assertUnreachable(result)
+ case HttpStatusCode.Unauthorized:
+ return <LoginForm currentUser={account} />;
+ case HttpStatusCode.NotFound:
+ return <LoginForm currentUser={account} />;
+ default:
+ assertUnreachable(result);
}
}
const { body: data } = result;
@@ -50,9 +82,9 @@ export function WireTransfer({ toAccount, onAuthorizationRequired, onCancel, onS
onAuthorizationRequired={onAuthorizationRequired}
onSuccess={() => {
notifyInfo(i18n.str`Wire transfer created!`);
- if (onSuccess) onSuccess()
+ if (onSuccess) onSuccess();
}}
- onCancel={onCancel}
+ routeCancel={routeCancel}
/>
);
}
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index 34faed7ec..66c27ef4c 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -18,20 +18,20 @@ import {
AbsoluteTime,
AmountJson,
HttpStatusCode,
- Logger,
PaytoUri,
PaytoUriIBAN,
PaytoUriTalerBank,
TalerErrorCode,
TranslatedString,
- WithdrawUriResult
+ WithdrawUriResult,
+ assertUnreachable,
} from "@gnu-taler/taler-util";
import {
Attention,
LocalNotificationBanner,
notifyInfo,
useLocalNotification,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { mutate } from "swr";
@@ -41,20 +41,17 @@ import { useBankState } from "../hooks/bank-state.js";
import { usePreferences } from "../hooks/preferences.js";
import { LoginForm } from "./LoginForm.js";
import { RenderAmount } from "./PaytoWireTransferForm.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
-
-const logger = new Logger("WithdrawalConfirmationQuestion");
interface Props {
onAborted: () => void;
withdrawUri: WithdrawUriResult;
details: {
- account: PaytoUri,
- reserve: string,
- username: string,
- amount: AmountJson,
- },
- onAuthorizationRequired: () => void,
+ account: PaytoUri;
+ reserve: string;
+ username: string;
+ amount: AmountJson;
+ };
+ onAuthorizationRequired: () => void;
}
/**
* Additional authentication required to complete the operation.
@@ -67,101 +64,116 @@ export function WithdrawalConfirmationQuestion({
withdrawUri,
}: Props): VNode {
const { i18n } = useTranslationContext();
- const [settings] = usePreferences()
+ const [settings] = usePreferences();
const { state: credentials } = useBackendState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials
- const [, updateBankState] = useBankState()
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const [, updateBankState] = useBankState();
- const [notification, notify, handleError] = useLocalNotification()
+ const [notification, notify, handleError] = useLocalNotification();
- const { config, api } = useBankCoreApiContext()
+ const { config, api } = useBankCoreApiContext();
async function doTransfer() {
await handleError(async () => {
if (!creds) return;
- const resp = await api.confirmWithdrawalById(creds, withdrawUri.withdrawalOperationId);
+ const resp = await api.confirmWithdrawalById(
+ creds,
+ withdrawUri.withdrawalOperationId,
+ );
if (resp.type === "ok") {
- mutate(() => true)// clean any info that we have
+ mutate(() => true); // clean any info that we have
if (!settings.showWithdrawalSuccess) {
- notifyInfo(i18n.str`Wire transfer completed!`)
+ notifyInfo(i18n.str`Wire transfer completed!`);
}
} else {
switch (resp.case) {
- case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: return notify({
- type: "error",
- title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return notify({
- type: "error",
- 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,
- });
- case HttpStatusCode.BadRequest: return notify({
- type: "error",
- title: i18n.str`The operation id is invalid.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`The operation was not found.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({
- type: "error",
- title: i18n.str`Your balance is not enough for the operation.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
+ case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT:
+ return notify({
+ type: "error",
+ title: i18n.str`The withdrawal has been aborted previously and can't be confirmed`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return notify({
+ type: "error",
+ 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,
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Your balance is not enough for the operation.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "confirm-withdrawal",
id: String(resp.body.challenge_id),
sent: AbsoluteTime.never(),
request: withdrawUri.withdrawalOperationId,
- })
- return onAuthorizationRequired()
+ });
+ return onAuthorizationRequired();
}
- default: assertUnreachable(resp)
+ default:
+ assertUnreachable(resp);
}
}
- })
+ });
}
async function doCancel() {
await handleError(async () => {
if (!creds) return;
- const resp = await api.abortWithdrawalById(creds, withdrawUri.withdrawalOperationId);
+ const resp = await api.abortWithdrawalById(
+ creds,
+ withdrawUri.withdrawalOperationId,
+ );
if (resp.type === "ok") {
onAborted();
} else {
switch (resp.case) {
- case HttpStatusCode.Conflict: return notify({
- type: "error",
- title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`
- });
- case HttpStatusCode.BadRequest: return notify({
- type: "error",
- title: i18n.str`The operation id is invalid.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`The operation was not found.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
+ case HttpStatusCode.Conflict:
+ return notify({
+ type: "error",
+ title: i18n.str`The reserve operation has been confirmed previously and can't be aborted`,
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
default: {
- assertUnreachable(resp)
+ assertUnreachable(resp);
}
}
}
- })
+ });
}
return (
@@ -174,74 +186,100 @@ export function WithdrawalConfirmationQuestion({
<i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
</h3>
<div class="mt-3 text-sm leading-6">
-
<ShouldBeSameUser username={details.username}>
<div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-2 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
<form
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
+ onSubmit={(e) => {
+ e.preventDefault();
}}
>
<div class="px-4 mt-4">
<div class="w-full">
<div class="px-4 sm:px-0 text-sm">
- <p><i18n.Translate>Wire transfer details</i18n.Translate></p>
+ <p>
+ <i18n.Translate>Wire transfer details</i18n.Translate>
+ </p>
</div>
<div class="mt-6 border-t border-gray-100">
<dl class="divide-y divide-gray-100">
{((): VNode => {
switch (details.account.targetType) {
case "iban": {
- const p = details.account as PaytoUriIBAN
- const name = p.params["receiver-name"]
- return <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>Taler Exchange operator's account</i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
- </div>
- {name &&
+ const p = details.account as PaytoUriIBAN;
+ const name = p.params["receiver-name"];
+ return (
+ <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>Taler Exchange operator's name</i18n.Translate>
+ <i18n.Translate>
+ Taler Exchange operator's account
+ </i18n.Translate>
</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {p.iban}
+ </dd>
</div>
- }
- </Fragment>
+ {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>
+ Taler Exchange operator's name
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {p.params["receiver-name"]}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
}
case "x-taler-bank": {
- const p = details.account as PaytoUriTalerBank
- const name = p.params["receiver-name"]
- return <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>Taler Exchange operator's account</i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
- </div>
- {name &&
+ const p = details.account as PaytoUriTalerBank;
+ const name = p.params["receiver-name"];
+ return (
+ <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>Taler Exchange operator's name</i18n.Translate>
+ <i18n.Translate>
+ Taler Exchange operator's account
+ </i18n.Translate>
</dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {p.account}
+ </dd>
</div>
- }
- </Fragment>
+ {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>
+ Taler Exchange operator's name
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {p.params["receiver-name"]}
+ </dd>
+ </div>
+ )}
+ </Fragment>
+ );
}
default:
- 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>Taler Exchange operator'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}</dd>
- </div>
-
+ 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>
+ Taler Exchange operator'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}
+ </dd>
+ </div>
+ );
}
})()}
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
@@ -249,58 +287,73 @@ export function WithdrawalConfirmationQuestion({
<i18n.Translate>Amount</i18n.Translate>
</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- <RenderAmount value={details.amount} spec={config.currency_specification} />
+ <RenderAmount
+ value={details.amount}
+ spec={config.currency_specification}
+ />
</dd>
</div>
</dl>
</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">
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
+ <button
+ type="button"
+ class="text-sm font-semibold leading-6 text-gray-900"
onClick={doCancel}
>
- <i18n.Translate>Cancel</i18n.Translate></button>
- <button type="submit"
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <button
+ type="submit"
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={(e) => {
- e.preventDefault()
- doTransfer()
+ e.preventDefault();
+ doTransfer();
}}
>
<i18n.Translate>Transfer</i18n.Translate>
</button>
</div>
-
</form>
</div>
</ShouldBeSameUser>
</div>
</div>
</div>
-
- </Fragment >
+ </Fragment>
);
}
-export function ShouldBeSameUser({ username, children }: { username: string, children: ComponentChildren }): VNode {
+export function ShouldBeSameUser({
+ username,
+ children,
+}: {
+ username: string;
+ children: ComponentChildren;
+}): VNode {
const { state: credentials } = useBackendState();
- const { i18n } = useTranslationContext()
+ const { i18n } = useTranslationContext();
if (credentials.status === "loggedOut") {
- return <Fragment>
- <Attention type="info" title={i18n.str`Authentication required`} />
- <LoginForm currentUser={username} fixedUser />
- </Fragment>
+ return (
+ <Fragment>
+ <Attention type="info" title={i18n.str`Authentication required`} />
+ <LoginForm currentUser={username} fixedUser />
+ </Fragment>
+ );
}
if (credentials.username !== username) {
- return <Fragment>
- <Attention type="warning" title={i18n.str`This operation was created with other username`} />
- <LoginForm currentUser={username} fixedUser />
- </Fragment>
+ return (
+ <Fragment>
+ <Attention
+ type="warning"
+ title={i18n.str`This operation was created with other username`}
+ />
+ <LoginForm currentUser={username} fixedUser />
+ </Fragment>
+ );
}
- return <Fragment>
- {children}
- </Fragment>
-} \ No newline at end of file
+ return <Fragment>{children}</Fragment>;
+}
diff --git a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
index e0e2bf0f5..e69a4dfb2 100644
--- a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
@@ -14,32 +14,26 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import {
- Logger,
- parseWithdrawUri,
- stringifyWithdrawUri
-} from "@gnu-taler/taler-util";
-import {
- Attention,
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import { parseWithdrawUri, stringifyWithdrawUri } from "@gnu-taler/taler-util";
+import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { useBankCoreApiContext } from "../context/config.js";
import { useBankState } from "../hooks/bank-state.js";
+import { RouteDefinition } from "../route.js";
import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
-const logger = new Logger("AccountPage");
-
export function WithdrawalOperationPage({
operationId,
onAuthorizationRequired,
- onContinue,
+ onOperationAborted,
+ routeClose,
}: {
onAuthorizationRequired: () => void;
operationId: string;
- onContinue: () => void;
+ onOperationAborted: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
}): VNode {
- const { api } = useBankCoreApiContext()
+ const { api } = useBankCoreApiContext();
const uri = stringifyWithdrawUri({
bankIntegrationApiBaseUrl: api.getIntegrationAPI().baseUrl,
withdrawalOperationId: operationId,
@@ -48,25 +42,26 @@ export function WithdrawalOperationPage({
const { i18n } = useTranslationContext();
const [, updateBankState] = useBankState();
-
if (!parsedUri) {
- return <Attention type="danger" title={i18n.str`The Withdrawal URI is not valid`}>
- {uri}
- </Attention>
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The Withdrawal URI is not valid`}
+ >
+ {uri}
+ </Attention>
+ );
}
return (
<WithdrawalQRCode
withdrawUri={parsedUri}
onAuthorizationRequired={onAuthorizationRequired}
- onClose={() => {
- updateBankState("currentWithdrawalOperationId", undefined)
- onContinue()
+ onOperationAborted={() => {
+ updateBankState("currentWithdrawalOperationId", undefined);
+ onOperationAborted();
}}
+ routeClose={routeClose}
/>
);
}
-
-export function assertUnreachable(x: never): never {
- throw new Error("Didn't expect to get here");
-}
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 30c1fe998..3cf552f39 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -17,26 +17,29 @@
import {
Amounts,
HttpStatusCode,
- Logger,
TalerError,
WithdrawUriResult,
- parsePaytoUri
+ assertUnreachable,
+ parsePaytoUri,
} from "@gnu-taler/taler-util";
-import { Attention, Loading, notifyInfo, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import {
+ Attention,
+ Loading,
+ notifyInfo,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
import { useWithdrawalDetails } from "../hooks/access.js";
+import { RouteDefinition } from "../route.js";
import { QrCodeSection } from "./QrCodeSection.js";
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
-import { assertUnreachable } from "./WithdrawalOperationPage.js";
-
-const logger = new Logger("WithdrawalQRCode");
interface Props {
withdrawUri: WithdrawUriResult;
- onClose: () => void;
- onAuthorizationRequired: () => void,
-
+ onOperationAborted: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
+ onAuthorizationRequired: () => void;
}
/**
* Offer the QR code (and a clickable taler://-link) to
@@ -45,90 +48,107 @@ interface Props {
*/
export function WithdrawalQRCode({
withdrawUri,
- onClose,
+ onOperationAborted,
+ routeClose,
onAuthorizationRequired,
}: Props): VNode {
const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
if (!result) {
- return <Loading />
+ return <Loading />;
}
if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />
+ return <ErrorLoadingWithDebug error={result} />;
}
if (result.type === "fail") {
switch (result.case) {
case HttpStatusCode.BadRequest:
- case HttpStatusCode.NotFound: return <OperationNotFound onClose={onClose} />
- default: assertUnreachable(result)
+ case HttpStatusCode.NotFound:
+ return <OperationNotFound routeClose={routeClose} />;
+ default:
+ assertUnreachable(result);
}
}
const { body: data } = result;
if (data.status === "aborted") {
- return <section id="main" class="content">
- <h1 class="nav">{i18n.str`Operation aborted`}</h1>
- <section>
- <p>
- <i18n.Translate>
- The wire transfer to the Taler Exchange operator's account was aborted, your balance
- was not affected.
- </i18n.Translate>
- </p>
- <p>
- <i18n.Translate>
- You can close this page now or continue to the account page.
- </i18n.Translate>
- </p>
- <a class="pure-button pure-button-primary"
- style={{ float: "right" }}
- onClick={async (e) => {
- e.preventDefault();
- onClose()
- }}>
- {i18n.str`Continue`}
- </a>
-
+ return (
+ <section id="main" class="content">
+ <h1 class="nav">{i18n.str`Operation aborted`}</h1>
+ <section>
+ <p>
+ <i18n.Translate>
+ The wire transfer to the Taler Exchange operator's account was
+ aborted, your balance was not affected.
+ </i18n.Translate>
+ </p>
+ <p>
+ <i18n.Translate>
+ You can close this page now or continue to the account page.
+ </i18n.Translate>
+ </p>
+ <a
+ href={routeClose.url({})}
+ class="pure-button pure-button-primary"
+ style={{ float: "right" }}
+ >
+ <i18n.Translate>Continue</i18n.Translate>
+ </a>
+ </section>
</section>
- </section>
+ );
}
if (data.status === "confirmed") {
- return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
- <div>
- <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
- <svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
- </svg>
- </div>
- <div class="mt-3 text-center sm:mt-5">
- <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
- <i18n.Translate>Withdrawal confirmed</i18n.Translate>
- </h3>
- <div class="mt-2">
- <p class="text-sm text-gray-500">
- <i18n.Translate>
- The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.
- </i18n.Translate>
- </p>
+ return (
+ <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
+ <div>
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
+ <svg
+ class="h-6 w-6 text-green-600"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M4.5 12.75l6 6 9-13.5"
+ />
+ </svg>
+ </div>
+ <div class="mt-3 text-center sm:mt-5">
+ <h3
+ class="text-base font-semibold leading-6 text-gray-900"
+ id="modal-title"
+ >
+ <i18n.Translate>Withdrawal confirmed</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ The wire transfer to the Taler operator has been initiated.
+ You will soon receive the requested amount in your Taler
+ wallet.
+ </i18n.Translate>
+ </p>
+ </div>
</div>
</div>
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeClose.url({})}
+ class="inline-flex w-full justify-center 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"
+ >
+ <i18n.Translate>Done</i18n.Translate>
+ </a>
+ </div>
</div>
- <div class="mt-5 sm:mt-6">
- <button type="button"
- class="inline-flex w-full justify-center 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 (e) => {
- e.preventDefault();
- onClose()
- }}>
- <i18n.Translate>Done</i18n.Translate>
- </button>
- </div>
- </div>
-
-
+ );
}
if (data.status === "pending") {
return (
@@ -136,33 +156,55 @@ export function WithdrawalQRCode({
withdrawUri={withdrawUri}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
- onClose()
+ onOperationAborted();
}}
/>
);
}
- const account = !data.selected_exchange_account ? undefined : parsePaytoUri(data.selected_exchange_account)
+ const account = !data.selected_exchange_account
+ ? undefined
+ : parsePaytoUri(data.selected_exchange_account);
if (!data.selected_reserve_pub && account) {
- return <Attention type="danger"
- title={i18n.str`The operation is marked as 'selected' but some step in the withdrawal failed`} >
- <i18n.Translate>The account is selected but no withdrawal identification found.</i18n.Translate>
- </Attention>
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation is marked as 'selected' but some step in the withdrawal failed`}
+ >
+ <i18n.Translate>
+ The account is selected but no withdrawal identification found.
+ </i18n.Translate>
+ </Attention>
+ );
}
if (!account && data.selected_reserve_pub) {
- return <Attention type="danger"
- title={i18n.str`The operation is marked as 'selected' but some step in the withdrawal failed`}>
- <i18n.Translate>There is a withdrawal identification but no account has been selected or the selected account is invalid.</i18n.Translate>
- </Attention>
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation is marked as 'selected' but some step in the withdrawal failed`}
+ >
+ <i18n.Translate>
+ There is a withdrawal identification but no account has been selected
+ or the selected account is invalid.
+ </i18n.Translate>
+ </Attention>
+ );
}
if (!account || !data.selected_reserve_pub) {
- return <Attention type="danger"
- title={i18n.str`The operation is marked as 'selected' but some step in the withdrawal failed`}>
- <i18n.Translate>No withdrawal ID found and no account has been selected or the selected account is invalid.</i18n.Translate>
- </Attention>
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`The operation is marked as 'selected' but some step in the withdrawal failed`}
+ >
+ <i18n.Translate>
+ No withdrawal ID found and no account has been selected or the
+ selected account is invalid.
+ </i18n.Translate>
+ </Attention>
+ );
}
return (
@@ -172,53 +214,71 @@ export function WithdrawalQRCode({
username: data.username,
account,
reserve: data.selected_reserve_pub,
- amount: Amounts.parseOrThrow(data.amount)
+ amount: Amounts.parseOrThrow(data.amount),
}}
onAuthorizationRequired={onAuthorizationRequired}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
- onClose()
+ onOperationAborted();
}}
/>
);
}
-
-export function OperationNotFound({ onClose }: { onClose: (() => void) | undefined }): VNode {
+export function OperationNotFound({
+ routeClose,
+}: {
+ routeClose: RouteDefinition<Record<string, never>> | undefined;
+}): VNode {
const { i18n } = useTranslationContext();
- return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
- <div>
- <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100 ">
- <svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
- </svg>
- </div>
+ return (
+ <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
+ <div>
+ <div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100 ">
+ <svg
+ class="h-6 w-6 text-red-600"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
+ />
+ </svg>
+ </div>
- <div class="mt-3 text-center sm:mt-5">
- <h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
- <i18n.Translate>Operation not found</i18n.Translate>
- </h3>
- <div class="mt-2">
- <p class="text-sm text-gray-500">
- <i18n.Translate>
- This operation is not known by the server. The operation id is wrong or the
- server deleted the operation information before reaching here.
- </i18n.Translate>
- </p>
+ <div class="mt-3 text-center sm:mt-5">
+ <h3
+ class="text-base font-semibold leading-6 text-gray-900"
+ id="modal-title"
+ >
+ <i18n.Translate>Operation not found</i18n.Translate>
+ </h3>
+ <div class="mt-2">
+ <p class="text-sm text-gray-500">
+ <i18n.Translate>
+ This operation is not known by the server. The operation id is
+ wrong or the server deleted the operation information before
+ reaching here.
+ </i18n.Translate>
+ </p>
+ </div>
</div>
</div>
+ {routeClose && (
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeClose.url({})}
+ class="inline-flex w-full justify-center 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"
+ >
+ <i18n.Translate>Cotinue to dashboard</i18n.Translate>
+ </a>
+ </div>
+ )}
</div>
- {onClose &&
- <div class="mt-5 sm:mt-6">
- <button type="button"
- class="inline-flex w-full justify-center 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 (e) => {
- e.preventDefault();
- onClose()
- }}>
- <i18n.Translate>Cotinue to dashboard</i18n.Translate>
- </button>
- </div>
- }
- </div>
-} \ No newline at end of file
+ );
+}
diff --git a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
index d2f3ae83e..670bbaea0 100644
--- a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
+++ b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -1,40 +1,66 @@
+/*
+ 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { Cashouts } from "../../components/Cashouts/index.js";
import { useBackendState } from "../../hooks/backend.js";
import { ProfileNavigation } from "../ProfileNavigation.js";
import { CreateCashout } from "../business/CreateCashout.js";
+import { RouteDefinition } from "../../route.js";
interface Props {
- account: string,
- onClose: () => void,
- onAuthorizationRequired: () => void,
- onSelected: (cid: number) => void
+ account: string;
+ routeClose: RouteDefinition<Record<string, never>>;
+ onAuthorizationRequired: () => void;
+ routeCashoutDetails: RouteDefinition<{ cid: string }>;
}
-export function CashoutListForAccount({ account, onAuthorizationRequired, onSelected, onClose }: Props): VNode {
+export function CashoutListForAccount({
+ account,
+ onAuthorizationRequired,
+ routeCashoutDetails,
+ routeClose,
+}: Props): VNode {
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
- const accountIsTheCurrentUser = credentials.status === "loggedIn" ?
- credentials.username === account : false
-
- return <Fragment>
- {accountIsTheCurrentUser ?
- <ProfileNavigation current="cashouts" />
- :
- <h1 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>Cashout for account {account}</i18n.Translate>
- </h1>
- }
-
- <CreateCashout focus onCancel={onClose} onAuthorizationRequired={onAuthorizationRequired} account={account} />
-
- <Cashouts
- account={account}
- onSelected={onSelected}
- />
- </Fragment>
-}
+ const accountIsTheCurrentUser =
+ credentials.status === "loggedIn"
+ ? credentials.username === account
+ : false;
+ return (
+ <Fragment>
+ {accountIsTheCurrentUser ? (
+ <ProfileNavigation current="cashouts" />
+ ) : (
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Cashout for account {account}</i18n.Translate>
+ </h1>
+ )}
+
+ <CreateCashout
+ focus
+ routeClose={routeClose}
+ onAuthorizationRequired={onAuthorizationRequired}
+ account={account}
+ />
+
+ <Cashouts account={account} routeCashoutDetails={routeCashoutDetails} />
+ </Fragment>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
index 0dfdb39f3..9f8fb72bc 100644
--- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
+++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -1,112 +1,156 @@
-import { AbsoluteTime, HttpStatusCode, TalerCorebankApi, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
-import { Loading, LocalNotificationBanner, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
+/*
+ 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 {
+ AbsoluteTime,
+ HttpStatusCode,
+ TalerCorebankApi,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Loading,
+ LocalNotificationBanner,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { useBankCoreApiContext } from "../../context/config.js";
import { useAccountDetails } from "../../hooks/access.js";
import { useBackendState } from "../../hooks/backend.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { RouteDefinition } from "../../route.js";
import { LoginForm } from "../LoginForm.js";
import { ProfileNavigation } from "../ProfileNavigation.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { AccountForm } from "../admin/AccountForm.js";
-import { useBankState } from "../../hooks/bank-state.js";
export function ShowAccountDetails({
account,
- onClear,
+ routeClose,
onUpdateSuccess,
onAuthorizationRequired,
}: {
- onClear?: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
onUpdateSuccess: () => void;
- onAuthorizationRequired: () => void,
+ onAuthorizationRequired: () => void;
account: string;
}): VNode {
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials
- const { api } = useBankCoreApiContext()
- const accountIsTheCurrentUser = credentials.status === "loggedIn" ?
- credentials.username === account : false
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const { api } = useBankCoreApiContext();
+ const accountIsTheCurrentUser =
+ credentials.status === "loggedIn"
+ ? credentials.username === account
+ : false;
const [update, setUpdate] = useState(false);
- const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.AccountReconfiguration | undefined>();
- const [notification, notify, handleError] = useLocalNotification()
- const [, updateBankState] = useBankState()
+ const [submitAccount, setSubmitAccount] = useState<
+ TalerCorebankApi.AccountReconfiguration | undefined
+ >();
+ const [notification, notify, handleError] = useLocalNotification();
+ const [, updateBankState] = useBankState();
const result = useAccountDetails(account);
if (!result) {
- return <Loading />
+ return <Loading />;
}
if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />
+ return <ErrorLoadingWithDebug error={result} />;
}
if (result.type === "fail") {
switch (result.case) {
case HttpStatusCode.Unauthorized:
- case HttpStatusCode.NotFound: return <LoginForm currentUser={account} />
- default: assertUnreachable(result)
+ case HttpStatusCode.NotFound:
+ return <LoginForm currentUser={account} />;
+ default:
+ assertUnreachable(result);
}
}
async function doUpdate() {
if (!update || !submitAccount || !creds) return;
await handleError(async () => {
- const resp = await api.updateAccount({
- token: creds.token,
- username: account,
- }, submitAccount);
+ const resp = await api.updateAccount(
+ {
+ token: creds.token,
+ username: account,
+ },
+ submitAccount,
+ );
if (resp.type === "ok") {
notifyInfo(i18n.str`Account updated`);
onUpdateSuccess();
} else {
switch (resp.case) {
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- title: i18n.str`The rights to change the account are not sufficient`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`The username was not found`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME: return notify({
- type: "error",
- 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,
- })
- case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return notify({
- type: "error",
- 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,
- })
- case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT: return notify({
- type: "error",
- 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,
- })
- case TalerErrorCode.BANK_MISSING_TAN_INFO: return notify({
- type: "error",
- title: i18n.str`No information for the selected authentication channel.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`The rights to change the account are not sufficient`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The username was not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME:
+ return notify({
+ type: "error",
+ 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,
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return notify({
+ type: "error",
+ 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,
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT:
+ return notify({
+ type: "error",
+ 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,
+ });
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return notify({
+ type: "error",
+ title: i18n.str`No information for the selected authentication channel.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "update-account",
id: String(resp.body.challenge_id),
sent: AbsoluteTime.never(),
request: submitAccount,
- })
- return onAuthorizationRequired()
+ });
+ return onAuthorizationRequired();
}
case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: {
return notify({
@@ -116,39 +160,53 @@ export function ShowAccountDetails({
debug: resp.detail,
});
}
- default: assertUnreachable(resp)
+ default:
+ assertUnreachable(resp);
}
}
- })
-
+ });
}
return (
<Fragment>
<LocalNotificationBanner notification={notification} showDebug={true} />
- {accountIsTheCurrentUser ?
+ {accountIsTheCurrentUser ? (
<ProfileNavigation current="details" />
- :
+ ) : (
<h1 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Account "{account}"</i18n.Translate>
</h1>
-
- }
+ )}
<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">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-semibold leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Change details</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={!update} class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
+ <button
+ type="button"
+ data-enabled={!update}
+ class="bg-indigo-600 data-[enabled=true]:bg-gray-200 relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer rounded-full ring-2 border-gray-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
onClick={() => {
- setUpdate(!update)
- }}>
- <span aria-hidden="true" data-enabled={!update} class="translate-x-5 data-[enabled=true]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ setUpdate(!update);
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={!update}
+ class="translate-x-5 data-[enabled=true]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</h2>
@@ -162,15 +220,14 @@ export function ShowAccountDetails({
onChange={(a) => setSubmitAccount(a)}
>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- {onClear ?
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onClear}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- : <div />
- }
- <button type="submit"
+ <a
+ href={routeClose.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
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"
disabled={!update || !submitAccount}
onClick={doUpdate}
@@ -183,4 +240,3 @@ export function ShowAccountDetails({
</Fragment>
);
}
-
diff --git a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
index 32e100e43..3b35c1fe1 100644
--- a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
+++ b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -1,45 +1,76 @@
-import { notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
+/*
+ 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 {
+ AbsoluteTime,
+ HttpStatusCode,
+ TalerErrorCode,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
-import { ShowInputErrorLabel } from "@gnu-taler/web-util/browser";
import { useBankCoreApiContext } from "../../context/config.js";
import { useBackendState } from "../../hooks/backend.js";
-import { undefinedIfEmpty, withRuntimeErrorHandling } from "../../utils.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { RouteDefinition } from "../../route.js";
+import { undefinedIfEmpty } from "../../utils.js";
import { doAutoFocus } from "../PaytoWireTransferForm.js";
import { ProfileNavigation } from "../ProfileNavigation.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
-import { LocalNotificationBanner } from "@gnu-taler/web-util/browser";
-import { AbsoluteTime, HttpStatusCode, TalerErrorCode } from "@gnu-taler/taler-util";
-import { useBankState } from "../../hooks/bank-state.js";
export function UpdateAccountPassword({
account: accountName,
- onCancel,
+ routeClose,
onUpdateSuccess,
onAuthorizationRequired,
focus,
}: {
- onCancel: () => void;
- focus?: boolean,
- onAuthorizationRequired: () => void,
+ routeClose: RouteDefinition<Record<string, never>>;
+ focus?: boolean;
+ onAuthorizationRequired: () => void;
onUpdateSuccess: () => void;
account: string;
}): VNode {
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
- const token = credentials.status !== "loggedIn" ? undefined : credentials.token
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
const { api } = useBankCoreApiContext();
const [current, setCurrent] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
const [repeat, setRepeat] = useState<string | undefined>();
- const [, updateBankState] = useBankState()
+ const [, updateBankState] = useBankState();
- const accountIsTheCurrentUser = credentials.status === "loggedIn" ?
- credentials.username === accountName : false
+ const accountIsTheCurrentUser =
+ credentials.status === "loggedIn"
+ ? credentials.username === accountName
+ : false;
const errors = undefinedIfEmpty({
- current: !accountIsTheCurrentUser ? undefined : !current ? i18n.str`required` : undefined,
+ current: !accountIsTheCurrentUser
+ ? undefined
+ : !current
+ ? i18n.str`required`
+ : undefined,
password: !password ? i18n.str`required` : undefined,
repeat: !repeat
? i18n.str`required`
@@ -47,8 +78,7 @@ export function UpdateAccountPassword({
? i18n.str`password doesn't match`
: undefined,
});
- const [notification, notify, handleError] = useLocalNotification()
-
+ const [notification, notify, handleError] = useLocalNotification();
async function doChangePassword() {
if (!!errors || !password || !token) return;
@@ -56,54 +86,62 @@ export function UpdateAccountPassword({
const request = {
old_password: current,
new_password: password,
- }
- const resp = await api.updatePassword({ username: accountName, token }, request);
+ };
+ const resp = await api.updatePassword(
+ { username: accountName, token },
+ request,
+ );
if (resp.type === "ok") {
notifyInfo(i18n.str`Password changed`);
onUpdateSuccess();
} else {
switch (resp.case) {
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- title: i18n.str`Not authorized to change the password, maybe the session is invalid.`
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`Account not found`
- })
- 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.`
- })
- 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.`
- })
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`Not authorized to change the password, maybe the session is invalid.`,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ });
+ 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.`,
+ });
+ 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.`,
+ });
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "update-password",
id: String(resp.body.challenge_id),
sent: AbsoluteTime.never(),
request,
- })
- return onAuthorizationRequired()
+ });
+ return onAuthorizationRequired();
}
- default: assertUnreachable(resp)
+ default:
+ assertUnreachable(resp);
}
}
- })
+ });
}
return (
<Fragment>
<LocalNotificationBanner notification={notification} />
- {accountIsTheCurrentUser ?
- <ProfileNavigation current="credentials" /> :
+ {accountIsTheCurrentUser ? (
+ <ProfileNavigation current="credentials" />
+ ) : (
<h1 class="text-base font-semibold leading-6 text-gray-900">
<i18n.Translate>Account "{accountName}"</i18n.Translate>
</h1>
-
- }
+ )}
<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">
@@ -115,8 +153,8 @@ export function UpdateAccountPassword({
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
+ onSubmit={(e) => {
+ e.preventDefault();
}}
>
<div class="px-4 py-6 sm:p-8">
@@ -138,7 +176,7 @@ export function UpdateAccountPassword({
data-error={!!errors?.password && password !== undefined}
value={password ?? ""}
onChange={(e) => {
- setPassword(e.currentTarget.value)
+ setPassword(e.currentTarget.value);
}}
autocomplete="off"
/>
@@ -165,7 +203,7 @@ export function UpdateAccountPassword({
data-error={!!errors?.repeat && repeat !== undefined}
value={repeat ?? ""}
onChange={(e) => {
- setRepeat(e.currentTarget.value)
+ setRepeat(e.currentTarget.value);
}}
// placeholder=""
autocomplete="off"
@@ -175,12 +213,12 @@ export function UpdateAccountPassword({
isDirty={repeat !== undefined}
/>
</div>
- <p class="mt-2 text-sm text-gray-500" >
+ <p class="mt-2 text-sm text-gray-500">
<i18n.Translate>repeat the same password</i18n.Translate>
</p>
</div>
- {accountIsTheCurrentUser ?
+ {accountIsTheCurrentUser ? (
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
@@ -197,7 +235,7 @@ export function UpdateAccountPassword({
data-error={!!errors?.current && current !== undefined}
value={current ?? ""}
onChange={(e) => {
- setCurrent(e.currentTarget.value)
+ setCurrent(e.currentTarget.value);
}}
autocomplete="off"
/>
@@ -206,29 +244,29 @@ export function UpdateAccountPassword({
isDirty={current !== undefined}
/>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>your current password, for security</i18n.Translate>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ your current password, for security
+ </i18n.Translate>
</p>
</div>
- : undefined}
-
+ ) : undefined}
</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">
- {onCancel ?
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- : <div />
- }
- <button type="submit"
+ <a
+ href={routeClose.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
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"
disabled={!!errors}
onClick={(e) => {
- e.preventDefault()
- doChangePassword()
+ e.preventDefault();
+ doChangePassword();
}}
>
<i18n.Translate>Change</i18n.Translate>
@@ -237,6 +275,5 @@ export function UpdateAccountPassword({
</form>
</div>
</Fragment>
-
);
-} \ No newline at end of file
+}
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
index e08fee8bc..05b9d6a72 100644
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
@@ -1,13 +1,47 @@
-import { AmountString, Amounts, PaytoString, TalerCorebankApi, TranslatedString, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
-import { Attention, CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, VNode, h } from "preact";
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ PaytoString,
+ TalerCorebankApi,
+ TranslatedString,
+ assertUnreachable,
+ buildPayto,
+ parsePaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ CopyButton,
+ ShowInputErrorLabel,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { VersionHint, useBankCoreApiContext } from "../../context/config.js";
-import { ErrorMessageMappingFor, PartialButDefined, TanChannel, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js";
+import { useBackendState } from "../../hooks/backend.js";
+import {
+ ErrorMessageMappingFor,
+ TanChannel,
+ undefinedIfEmpty,
+ validateIBAN,
+} from "../../utils.js";
import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { getRandomPassword } from "../rnd.js";
-import { useBackendState } from "../../hooks/backend.js";
const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
const EMAIL_REGEX =
@@ -15,29 +49,29 @@ const EMAIL_REGEX =
const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
export type AccountFormData = {
- debit_threshold?: string,
- isExchange?: boolean,
- isPublic?: boolean,
- name?: string,
- username?: string,
- payto_uri?: string,
- cashout_payto_uri?: string,
- email?: string,
- phone?: string,
- tan_channel?: TanChannel | "remove",
-}
+ debit_threshold?: string;
+ isExchange?: boolean;
+ isPublic?: boolean;
+ name?: string;
+ username?: string;
+ payto_uri?: string;
+ cashout_payto_uri?: string;
+ email?: string;
+ phone?: string;
+ tan_channel?: TanChannel | "remove";
+};
type ChangeByPurposeType = {
- "create": (a: TalerCorebankApi.RegisterAccountRequest | undefined) => void,
- "update": (a: TalerCorebankApi.AccountReconfiguration | undefined) => void,
- "show": undefined
-}
+ create: (a: TalerCorebankApi.RegisterAccountRequest | undefined) => void;
+ update: (a: TalerCorebankApi.AccountReconfiguration | undefined) => void;
+ show: undefined;
+};
/**
* FIXME:
* is_public is missing on PATCH
* account email/password should require 2FA
- *
- *
+ *
+ *
* @param param0
* @returns
*/
@@ -49,14 +83,14 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
focus,
children,
}: {
- focus?: boolean,
- children: ComponentChildren,
- username?: string,
+ focus?: boolean;
+ children: ComponentChildren;
+ username?: string;
template: TalerCorebankApi.AccountData | undefined;
onChange: ChangeByPurposeType[PurposeType];
purpose: PurposeType;
}): VNode {
- const { config, hints } = useBankCoreApiContext()
+ const { config, hints } = useBankCoreApiContext();
const { i18n } = useTranslationContext();
const { state: credentials } = useBackendState();
const [form, setForm] = useState<AccountFormData>({});
@@ -65,87 +99,115 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
ErrorMessageMappingFor<typeof defaultValue> | undefined
>(undefined);
-
const defaultValue: AccountFormData = {
- debit_threshold: Amounts.stringifyValue(template?.debit_threshold ?? config.default_debit_threshold),
+ debit_threshold: Amounts.stringifyValue(
+ template?.debit_threshold ?? config.default_debit_threshold,
+ ),
isExchange: template?.is_taler_exchange,
isPublic: template?.is_public,
name: template?.name ?? "",
- cashout_payto_uri: stringifyIbanPayto(template?.cashout_payto_uri) ?? "" as PaytoString,
- payto_uri: stringifyIbanPayto(template?.payto_uri) ?? "" as PaytoString,
+ cashout_payto_uri:
+ stringifyIbanPayto(template?.cashout_payto_uri) ?? ("" as PaytoString),
+ payto_uri: stringifyIbanPayto(template?.payto_uri) ?? ("" as PaytoString),
email: template?.contact_data?.email ?? "",
phone: template?.contact_data?.phone ?? "",
username: username ?? "",
tan_channel: template?.tan_channel,
- }
-
- const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1
-
- const showingCurrentUserInfo = credentials.status !== "loggedIn" ? false : username === credentials.username
- const userIsAdmin = credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator
-
- const editableUsername = (purpose === "create")
- const editableName = (purpose === "create" || purpose === "update" && (config.allow_edit_name || userIsAdmin))
- const editableCashout = showingCurrentUserInfo && (purpose === "create" || purpose === "update" && (config.allow_edit_cashout_payto_uri || userIsAdmin))
- const editableThreshold = userIsAdmin && (purpose === "create" || purpose === "update")
- const editableAccount = purpose === "create" && userIsAdmin
-
- const hasPhone = !!defaultValue.phone || !!form.phone
- const hasEmail = !!defaultValue.email || !!form.email
+ };
+
+ const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1;
+
+ const showingCurrentUserInfo =
+ credentials.status !== "loggedIn"
+ ? false
+ : username === credentials.username;
+ const userIsAdmin =
+ credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator;
+
+ const editableUsername = purpose === "create";
+ const editableName =
+ purpose === "create" ||
+ (purpose === "update" && (config.allow_edit_name || userIsAdmin));
+ const editableCashout =
+ showingCurrentUserInfo &&
+ (purpose === "create" ||
+ (purpose === "update" &&
+ (config.allow_edit_cashout_payto_uri || userIsAdmin)));
+ const editableThreshold =
+ userIsAdmin && (purpose === "create" || purpose === "update");
+ const editableAccount = purpose === "create" && userIsAdmin;
+
+ const hasPhone = !!defaultValue.phone || !!form.phone;
+ const hasEmail = !!defaultValue.email || !!form.email;
function updateForm(newForm: typeof defaultValue): void {
const cashoutParsed = !newForm.cashout_payto_uri
? undefined
- : buildPayto("iban", newForm.cashout_payto_uri, undefined);;
+ : buildPayto("iban", newForm.cashout_payto_uri, undefined);
const internalParsed = !newForm.payto_uri
? undefined
- : buildPayto("iban", newForm.payto_uri, undefined);;
+ : buildPayto("iban", newForm.payto_uri, undefined);
const trimmedAmountStr = newForm.debit_threshold?.trim();
- const parsedAmount = Amounts.parse(`${config.currency}:${trimmedAmountStr}`);
-
- const errors = undefinedIfEmpty<ErrorMessageMappingFor<typeof defaultValue>>({
- cashout_payto_uri: (!newForm.cashout_payto_uri
- ? undefined :
- !editableCashout ? undefined :
- !cashoutParsed
- ? i18n.str`it doesnt have the pattern of an IBAN number` :
- !cashoutParsed.isKnown || cashoutParsed.targetType !== "iban"
- ? i18n.str`only "IBAN" target are supported` :
- !IBAN_REGEX.test(cashoutParsed.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers` :
- validateIBAN(cashoutParsed.iban, i18n)),
- payto_uri: (!newForm.payto_uri
- ? undefined :
- !editableAccount ? undefined :
- !internalParsed
- ? i18n.str`it doesnt have the pattern of an IBAN number` :
- !internalParsed.isKnown || internalParsed.targetType !== "iban"
- ? i18n.str`only "IBAN" target are supported` :
- !IBAN_REGEX.test(internalParsed.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers` :
- validateIBAN(internalParsed.iban, i18n)),
+ const parsedAmount = Amounts.parse(
+ `${config.currency}:${trimmedAmountStr}`,
+ );
+
+ const errors = undefinedIfEmpty<
+ ErrorMessageMappingFor<typeof defaultValue>
+ >({
+ cashout_payto_uri: !newForm.cashout_payto_uri
+ ? undefined
+ : !editableCashout
+ ? undefined
+ : !cashoutParsed
+ ? i18n.str`it doesnt have the pattern of an IBAN number`
+ : !cashoutParsed.isKnown || cashoutParsed.targetType !== "iban"
+ ? i18n.str`only "IBAN" target are supported`
+ : !IBAN_REGEX.test(cashoutParsed.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : validateIBAN(cashoutParsed.iban, i18n),
+ payto_uri: !newForm.payto_uri
+ ? undefined
+ : !editableAccount
+ ? undefined
+ : !internalParsed
+ ? i18n.str`it doesnt have the pattern of an IBAN number`
+ : !internalParsed.isKnown || internalParsed.targetType !== "iban"
+ ? i18n.str`only "IBAN" target are supported`
+ : !IBAN_REGEX.test(internalParsed.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : validateIBAN(internalParsed.iban, i18n),
email: !newForm.email
- ? undefined :
- !EMAIL_REGEX.test(newForm.email)
- ? i18n.str`it doesnt have the pattern of an email` :
- undefined,
+ ? undefined
+ : !EMAIL_REGEX.test(newForm.email)
+ ? i18n.str`it doesnt have the pattern of an email`
+ : undefined,
phone: !newForm.phone
- ? undefined :
- !newForm.phone.startsWith("+") // FIXME: better phone number check
- ? i18n.str`should start with +` :
- !REGEX_JUST_NUMBERS_REGEX.test(newForm.phone)
+ ? undefined
+ : !newForm.phone.startsWith("+") // FIXME: better phone number check
+ ? i18n.str`should start with +`
+ : !REGEX_JUST_NUMBERS_REGEX.test(newForm.phone)
? i18n.str`phone number can't have other than numbers`
- :
- undefined,
- debit_threshold: !editableThreshold ? undefined :
- !trimmedAmountStr ? undefined :
- !parsedAmount ? i18n.str`not valid` :
- undefined,
- name: !editableName ? undefined : //disabled
- !newForm.name ? i18n.str`required` : undefined,
- username: !editableUsername ? undefined : !newForm.username ? i18n.str`required` : undefined,
+ : undefined,
+ debit_threshold: !editableThreshold
+ ? undefined
+ : !trimmedAmountStr
+ ? undefined
+ : !parsedAmount
+ ? i18n.str`not valid`
+ : undefined,
+ name: !editableName
+ ? undefined // disabled
+ : !newForm.name
+ ? i18n.str`required`
+ : undefined,
+ username: !editableUsername
+ ? undefined
+ : !newForm.username
+ ? i18n.str`required`
+ : undefined,
});
setErrors(errors);
@@ -153,20 +215,26 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
if (!onChange) return;
if (errors) {
- onChange(undefined)
+ onChange(undefined);
} else {
- const cashout = !newForm.cashout_payto_uri ? undefined : buildPayto("iban", newForm.cashout_payto_uri, undefined)
- const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout)
+ const cashout = !newForm.cashout_payto_uri
+ ? undefined
+ : buildPayto("iban", newForm.cashout_payto_uri, undefined);
+ const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout);
- const internal = !newForm.payto_uri ? undefined : buildPayto("iban", newForm.payto_uri, undefined);
- const internalURI = !internal ? undefined : stringifyPaytoUri(internal)
+ const internal = !newForm.payto_uri
+ ? undefined
+ : buildPayto("iban", newForm.payto_uri, undefined);
+ const internalURI = !internal ? undefined : stringifyPaytoUri(internal);
- const threshold = !parsedAmount ? undefined : Amounts.stringify(parsedAmount)
+ const threshold = !parsedAmount
+ ? undefined
+ : Amounts.stringify(parsedAmount);
switch (purpose) {
case "create": {
- //typescript doesn't correctly narrow a generic type
- const callback = onChange as ChangeByPurposeType["create"]
+ // typescript doesn't correctly narrow a generic type
+ const callback = onChange as ChangeByPurposeType["create"];
const result: TalerCorebankApi.RegisterAccountRequest = {
name: newForm.name!,
password: getRandomPassword(),
@@ -180,15 +248,17 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
payto_uri: internalURI,
is_public: !!newForm.isPublic,
is_taler_exchange: !!newForm.isExchange,
- // @ts-ignore
- tan_channel: newForm.tan_channel === "remove" ? null : newForm.tan_channel,
- }
- callback(result)
+ tan_channel:
+ newForm.tan_channel === "remove"
+ ? undefined
+ : newForm.tan_channel,
+ };
+ callback(result);
return;
}
case "update": {
- //typescript doesn't correctly narrow a generic type
- const callback = onChange as ChangeByPurposeType["update"]
+ // typescript doesn't correctly narrow a generic type
+ const callback = onChange as ChangeByPurposeType["update"];
const result: TalerCorebankApi.AccountReconfiguration = {
cashout_payto_uri: cashoutURI,
@@ -199,17 +269,17 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
debit_threshold: threshold,
is_public: !!newForm.isPublic,
name: newForm.name,
- // @ts-ignore
- tan_channel: newForm?.tan_channel === "remove" ? null : newForm.tan_channel,
- }
- callback(result)
+ tan_channel:
+ newForm.tan_channel === "remove" ? null : newForm.tan_channel,
+ };
+ callback(result);
return;
}
case "show": {
return;
}
default: {
- assertUnreachable(purpose)
+ assertUnreachable(purpose);
}
}
}
@@ -219,13 +289,12 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
+ onSubmit={(e) => {
+ e.preventDefault();
}}
>
<div class="px-4 py-6 sm:p-8">
<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"
@@ -256,8 +325,10 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
isDirty={form.username !== undefined}
/>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>account identification in the bank</i18n.Translate>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ account identification in the bank
+ </i18n.Translate>
</p>
</div>
@@ -290,26 +361,30 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
isDirty={form.name !== undefined}
/>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>name of the person owner the account</i18n.Translate>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ name of the person owner the account
+ </i18n.Translate>
</p>
</div>
-
<PaytoField
type="iban"
name="internal-account"
label={i18n.str`Internal IBAN`}
- help={purpose === "create" ?
- i18n.str`if empty a random account number will be assigned` :
- i18n.str`account identification for bank transfer`}
+ help={
+ purpose === "create"
+ ? i18n.str`if empty a random account number will be assigned`
+ : i18n.str`account identification for bank transfer`
+ }
value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString}
disabled={!editableAccount}
error={errors?.payto_uri}
onChange={(e) => {
- form.payto_uri = e as PaytoString
- updateForm(structuredClone(form))
- }} />
+ form.payto_uri = e as PaytoString;
+ updateForm(structuredClone(form));
+ }}
+ />
<div class="sm:col-span-5">
<label
@@ -369,71 +444,114 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
</div>
</div>
- {showingCurrentUserInfo &&
+ {showingCurrentUserInfo && (
<PaytoField
type="iban"
name="cashout-account"
label={i18n.str`Cashout IBAN`}
help={i18n.str`account number where the money is going to be sent when doing cashouts`}
- value={(form.cashout_payto_uri ?? defaultValue.cashout_payto_uri) as PaytoString}
+ value={
+ (form.cashout_payto_uri ??
+ defaultValue.cashout_payto_uri) as PaytoString
+ }
disabled={!editableCashout}
error={errors?.cashout_payto_uri}
onChange={(e) => {
- form.cashout_payto_uri = e as PaytoString
- updateForm(structuredClone(form))
- }} />
- }
+ form.cashout_payto_uri = e as PaytoString;
+ updateForm(structuredClone(form));
+ }}
+ />
+ )}
<div class="sm:col-span-5">
- <label for="debit" class="block text-sm font-medium leading-6 text-gray-900">{i18n.str`Max debt`}</label>
+ <label
+ for="debit"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Max debt`}</label>
<InputAmount
name="debit"
left
currency={config.currency}
value={form.debit_threshold ?? defaultValue.debit_threshold}
- onChange={!editableThreshold ? undefined : (e) => {
- form.debit_threshold = e as AmountString
- updateForm(structuredClone(form))
- }}
+ onChange={
+ !editableThreshold
+ ? undefined
+ : (e) => {
+ form.debit_threshold = e as AmountString;
+ updateForm(structuredClone(form));
+ }
+ }
/>
<ShowInputErrorLabel
- message={errors?.debit_threshold ? String(errors?.debit_threshold) : undefined}
+ message={
+ errors?.debit_threshold
+ ? String(errors?.debit_threshold)
+ : undefined
+ }
isDirty={form.debit_threshold !== undefined}
/>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>how much is user able to transfer after zero balance</i18n.Translate>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ how much is user able to transfer after zero balance
+ </i18n.Translate>
</p>
</div>
- {purpose !== "create" || !userIsAdmin ? undefined :
+ {purpose !== "create" || !userIsAdmin ? undefined : (
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Is this a Taler Exchange?</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={form.isExchange ?? defaultValue.isExchange ? "true" : "false"} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
+ <button
+ type="button"
+ data-enabled={
+ form.isExchange ?? defaultValue.isExchange
+ ? "true"
+ : "false"
+ }
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
onClick={() => {
- form.isExchange = !form.isExchange
- updateForm(structuredClone(form))
- }}>
- <span aria-hidden="true" data-enabled={form.isExchange ?? defaultValue.isExchange ? "true" : "false"} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ form.isExchange = !form.isExchange;
+ updateForm(structuredClone(form));
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={
+ form.isExchange ?? defaultValue.isExchange
+ ? "true"
+ : "false"
+ }
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
</div>
- }
+ )}
{/* channel, not shown if old cashout api */}
- {OLD_CASHOUT_API ? undefined : config.supported_tan_channels.length === 0 ?
+ {OLD_CASHOUT_API ? undefined : config.supported_tan_channels
+ .length === 0 ? (
<div class="sm:col-span-5">
- <Attention type="warning" title={i18n.str`No cashout channel available`}>
+ <Attention
+ type="warning"
+ title={i18n.str`No cashout channel available`}
+ >
<i18n.Translate>
This server doesn't support second factor authentication.
</i18n.Translate>
</Attention>
</div>
- :
+ ) : (
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
@@ -443,85 +561,166 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
</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">
- {config.supported_tan_channels.indexOf(TanChannel.EMAIL) === -1 ? undefined :
- <label onClick={(e) => {
- if (!hasEmail) return;
- if (form.tan_channel === TanChannel.EMAIL) {
- form.tan_channel = "remove"
- } else {
- form.tan_channel = TanChannel.EMAIL
+ {config.supported_tan_channels.indexOf(TanChannel.EMAIL) ===
+ -1 ? undefined : (
+ <label
+ onClick={(e) => {
+ if (!hasEmail) return;
+ if (form.tan_channel === TanChannel.EMAIL) {
+ form.tan_channel = "remove";
+ } else {
+ form.tan_channel = TanChannel.EMAIL;
+ }
+ updateForm(structuredClone(form));
+ e.preventDefault();
+ }}
+ data-disabled={purpose === "show" || !hasEmail}
+ data-selected={
+ (form.tan_channel ?? defaultValue.tan_channel) ===
+ TanChannel.EMAIL
}
- updateForm(structuredClone(form))
- e.preventDefault()
- }} data-disabled={purpose === "show" || !hasEmail} data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.EMAIL}
- 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" />
+ 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 ">
+ <span
+ id="project-type-0-label"
+ class="block text-sm font-medium text-gray-900 "
+ >
<i18n.Translate>Using email</i18n.Translate>
</span>
- {purpose !== "show" && !hasEmail && i18n.str`add a email in your profile to enable this option`}
+ {purpose !== "show" &&
+ !hasEmail &&
+ i18n.str`add a email in your profile to enable this option`}
</span>
</span>
- <svg data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+ <svg
+ data-selected={
+ (form.tan_channel ?? defaultValue.tan_channel) ===
+ TanChannel.EMAIL
+ }
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
</svg>
</label>
- }
-
- {config.supported_tan_channels.indexOf(TanChannel.SMS) === -1 ? undefined :
- <label onClick={(e) => {
- if (!hasPhone) return;
- if (form.tan_channel === TanChannel.SMS) {
- form.tan_channel = "remove"
- } else {
- form.tan_channel = TanChannel.SMS
+ )}
+
+ {config.supported_tan_channels.indexOf(TanChannel.SMS) ===
+ -1 ? undefined : (
+ <label
+ onClick={(e) => {
+ if (!hasPhone) return;
+ if (form.tan_channel === TanChannel.SMS) {
+ form.tan_channel = "remove";
+ } else {
+ form.tan_channel = TanChannel.SMS;
+ }
+ updateForm(structuredClone(form));
+ e.preventDefault();
+ }}
+ data-disabled={purpose === "show" || !hasPhone}
+ data-selected={
+ (form.tan_channel ?? defaultValue.tan_channel) ===
+ TanChannel.SMS
}
- updateForm(structuredClone(form))
- e.preventDefault()
- }} data-disabled={purpose === "show" || !hasPhone} data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.SMS}
- 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" />
+ 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-1-label" class="block text-sm font-medium text-gray-900">
+ <span
+ id="project-type-1-label"
+ class="block text-sm font-medium text-gray-900"
+ >
<i18n.Translate>Using SMS</i18n.Translate>
</span>
- {purpose !== "show" && !hasPhone && i18n.str`add a phone number in your profile to enable this option`}
+ {purpose !== "show" &&
+ !hasPhone &&
+ i18n.str`add a phone number in your profile to enable this option`}
</span>
</span>
- <svg data-selected={(form.tan_channel ?? defaultValue.tan_channel) === TanChannel.SMS} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+ <svg
+ data-selected={
+ (form.tan_channel ?? defaultValue.tan_channel) ===
+ TanChannel.SMS
+ }
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
</svg>
</label>
- }
+ )}
</div>
</div>
</div>
- }
+ )}
<div class="sm:col-span-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
- <span class="text-sm text-black font-medium leading-6 " id="availability-label">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
<i18n.Translate>Is this account public?</i18n.Translate>
</span>
</span>
- <button type="button" data-enabled={form.isPublic ?? defaultValue.isPublic ? "true" : "false"} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
-
+ <button
+ type="button"
+ data-enabled={
+ form.isPublic ?? defaultValue.isPublic ? "true" : "false"
+ }
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
onClick={() => {
- form.isPublic = !form.isPublic
- updateForm(structuredClone(form))
- }}>
- <span aria-hidden="true" data-enabled={form.isPublic ?? defaultValue.isPublic ? "true" : "false"} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ form.isPublic = !form.isPublic;
+ updateForm(structuredClone(form));
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={
+ form.isPublic ?? defaultValue.isPublic ? "true" : "false"
+ }
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>public accounts have their balance publicly accesible</i18n.Translate>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ public accounts have their balance publicly accesible
+ </i18n.Translate>
</p>
</div>
-
</div>
</div>
{children}
@@ -530,15 +729,16 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
}
function stringifyIbanPayto(s: PaytoString | undefined): string | undefined {
- if (s === undefined) return undefined
- const p = parsePaytoUri(s)
- if (p === undefined) return undefined
- if (!p.isKnown) return undefined
- if (p.targetType !== "iban") return undefined
- return p.iban
+ if (s === undefined) return undefined;
+ const p = parsePaytoUri(s);
+ if (p === undefined) return undefined;
+ if (!p.isKnown) return undefined;
+ if (p.targetType !== "iban") return undefined;
+ return p.iban;
}
-{/* <div class="sm:col-span-5">
+{
+ /* <div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
for="cashout"
@@ -572,112 +772,129 @@ function stringifyIbanPayto(s: PaytoString | undefined): string | undefined {
<p class="mt-2 text-sm text-gray-500" >
<i18n.Translate></i18n.Translate>
</p>
- </div> */}
+ </div> */
+}
-function PaytoField({ name, label, help, type, value, disabled, onChange, error }: { error: TranslatedString | undefined, name: string, label: TranslatedString, help: TranslatedString, onChange: (s: string) => void, type: "iban" | "x-taler-bank" | "bitcoin", disabled?: boolean, value: string | undefined }): VNode {
+function PaytoField({
+ name,
+ label,
+ help,
+ type,
+ value,
+ disabled,
+ onChange,
+ error,
+}: {
+ error: TranslatedString | undefined;
+ name: string;
+ label: TranslatedString;
+ help: TranslatedString;
+ onChange: (s: string) => void;
+ type: "iban" | "x-taler-bank" | "bitcoin";
+ disabled?: boolean;
+ value: string | undefined;
+}): VNode {
if (type === "iban") {
- return <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for={name}
- >
- {label}
- </label>
- <div class="mt-2">
- <div class="flex justify-between">
- <input
- type="text"
- class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name={name}
- id={name}
- disabled={disabled}
- value={value ?? ""}
- onChange={(e) => {
- onChange(e.currentTarget.value)
- }}
- />
- <CopyButton
- class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
- getContent={() => value ?? ""}
- />
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for={name}
+ >
+ {label}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name={name}
+ id={name}
+ disabled={disabled}
+ value={value ?? ""}
+ onChange={(e) => {
+ onChange(e.currentTarget.value);
+ }}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
</div>
- <ShowInputErrorLabel
- message={error}
- isDirty={value !== undefined}
- />
+ <p class="mt-2 text-sm text-gray-500">{help}</p>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- {help}
- </p>
- </div>
+ );
}
if (type === "x-taler-bank") {
- return <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for={name}
- >
- {label}
- </label>
- <div class="mt-2">
- <div class="flex justify-between">
- <input
- type="text"
- class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name={name}
- id={name}
- disabled={disabled}
- value={value ?? ""}
- />
- <CopyButton
- class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
- getContent={() => value ?? ""}
- />
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for={name}
+ >
+ {label}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name={name}
+ id={name}
+ disabled={disabled}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ </div>
+ <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
</div>
- <ShowInputErrorLabel
- message={error}
- isDirty={value !== undefined}
- />
+ <p class="mt-2 text-sm text-gray-500">
+ {/* <i18n.Translate>internal account id</i18n.Translate> */}
+ {help}
+ </p>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- {/* <i18n.Translate>internal account id</i18n.Translate> */}
- {help}
- </p>
- </div>
+ );
}
if (type === "bitcoin") {
- return <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for={name}
- >
- {label}
- </label>
- <div class="mt-2">
- <div class="flex justify-between">
- <input
- type="text"
- class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name={name}
- id={name}
- disabled={disabled}
- value={value ?? ""}
- />
- <CopyButton
- class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
- getContent={() => value ?? ""}
- />
- <ShowInputErrorLabel
- message={error}
- isDirty={value !== undefined}
- />
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for={name}
+ >
+ {label}
+ </label>
+ <div class="mt-2">
+ <div class="flex justify-between">
+ <input
+ type="text"
+ class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name={name}
+ id={name}
+ disabled={disabled}
+ value={value ?? ""}
+ />
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() => value ?? ""}
+ />
+ <ShowInputErrorLabel
+ message={error}
+ isDirty={value !== undefined}
+ />
+ </div>
</div>
+ <p class="mt-2 text-sm text-gray-500">
+ {/* <i18n.Translate>bitcoin address</i18n.Translate> */}
+ {help}
+ </p>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- {/* <i18n.Translate>bitcoin address</i18n.Translate> */}
- {help}
- </p>
- </div>
+ );
}
- assertUnreachable(type)
+ assertUnreachable(type);
}
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx
index 4ec25660b..1cee4c58a 100644
--- a/packages/demobank-ui/src/pages/admin/AccountList.tsx
+++ b/packages/demobank-ui/src/pages/admin/AccountList.tsx
@@ -1,143 +1,205 @@
-import { Amounts, HttpStatusCode, TalerError } from "@gnu-taler/taler-util";
+/*
+ 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 {
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { useBankCoreApiContext } from "../../context/config.js";
import { useBusinessAccounts } from "../../hooks/circuit.js";
import { RenderAmount } from "../PaytoWireTransferForm.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
+import { RouteDefinition } from "../../route.js";
interface Props {
- onCreateAccount: () => void;
+ routeCreate: RouteDefinition<Record<string, never>>;
- onShowAccountDetails: (aid: string) => void;
- onRemoveAccount: (aid: string) => void;
- onUpdateAccountPassword: (aid: string) => void;
- onShowCashoutForAccount: (aid: string) => void;
+ routeShowAccount: RouteDefinition<{ account: string }>;
+ routeRemoveAccount: RouteDefinition<{ account: string }>;
+ routeUpdatePasswordAccount: RouteDefinition<{ account: string }>;
+ routeShowCashoutsAccount: RouteDefinition<{ account: string }>;
}
-export function AccountList({ onRemoveAccount, onShowAccountDetails, onUpdateAccountPassword, onShowCashoutForAccount, onCreateAccount }: Props): VNode {
+export function AccountList({
+ routeCreate,
+ routeRemoveAccount,
+ routeShowAccount,
+ routeShowCashoutsAccount,
+ routeUpdatePasswordAccount,
+}: Props): VNode {
const result = useBusinessAccounts();
const { i18n } = useTranslationContext();
- const { config } = useBankCoreApiContext()
+ const { config } = useBankCoreApiContext();
if (!result) {
- return <Loading />
+ return <Loading />;
}
if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />
+ return <ErrorLoadingWithDebug error={result} />;
}
if (result.data.type === "fail") {
switch (result.data.case) {
- case HttpStatusCode.Unauthorized: return <Fragment />
- default: assertUnreachable(result.data.case)
+ case HttpStatusCode.Unauthorized:
+ return <Fragment />;
+ default:
+ assertUnreachable(result.data.case);
}
}
const { accounts } = result.data.body;
- return <Fragment>
- <div class="px-4 sm:px-6 lg:px-8 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>Accounts</i18n.Translate>
- </h1>
- <p class="mt-2 text-sm text-gray-700">
- <i18n.Translate>A list of all business account in the bank.</i18n.Translate>
- </p>
- </div>
- <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
- <button type="button" class="block rounded-md bg-indigo-600 px-3 py-2 text-center 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={(e) => {
- e.preventDefault()
- onCreateAccount()
- }}>
- <i18n.Translate>Create account</i18n.Translate>
- </button>
+ return (
+ <Fragment>
+ <div class="px-4 sm:px-6 lg:px-8 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>Accounts</i18n.Translate>
+ </h1>
+ <p class="mt-2 text-sm text-gray-700">
+ <i18n.Translate>
+ A list of all business account in the bank.
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
+ <a
+ href={routeCreate.url({})}
+ type="button"
+ class="block rounded-md bg-indigo-600 px-3 py-2 text-center 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"
+ >
+ <i18n.Translate>Create account</i18n.Translate>
+ </a>
+ </div>
</div>
- </div>
- <div class="mt-8 flow-root">
- <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></div>
- ) : (
- <table class="min-w-full divide-y divide-gray-300">
- <thead>
- <tr>
- <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">{i18n.str`Username`}</th>
- <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Name`}</th>
- <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">{i18n.str`Balance`}</th>
- <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
- <span class="sr-only">{i18n.str`Actions`}</span>
- </th>
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-200">
- {accounts.map((item, idx) => {
- const balance = !item.balance
- ? undefined
- : Amounts.parse(item.balance.amount);
- const noBalance = Amounts.isZero(item.balance.amount)
- const balanceIsDebit =
- item.balance &&
- item.balance.credit_debit_indicator == "debit";
-
- return <tr key={idx}>
- <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
- <a href="#" class="text-indigo-600 hover:text-indigo-900"
- onClick={(e) => {
- e.preventDefault();
- onShowAccountDetails(item.username)
- }}
- >
- {item.username}
- </a>
-
-
- </td>
- <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
- {item.name}
- </td>
- <td data-negative={noBalance ? undefined : balanceIsDebit ? "true" : "false"} class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600 ">
- {!balance ? (
- i18n.str`unknown`
- ) : (
- <span class="amount">
- <RenderAmount value={balance} negative={balanceIsDebit} spec={config.currency_specification} />
- </span>
- )}
- </td>
- <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
- <a href="#" class="text-indigo-600 hover:text-indigo-900"
- onClick={(e) => {
- e.preventDefault();
- onUpdateAccountPassword(item.username)
- }}
- >
- <i18n.Translate>change password</i18n.Translate>
- </a>
- <br />
- {noBalance ?
- <a href="#" class="text-indigo-600 hover:text-indigo-900" onClick={(e) => {
- e.preventDefault();
- onRemoveAccount(item.username)
- }}
- >
- <i18n.Translate>remove</i18n.Translate>
- </a>
- : undefined}
- </td>
+ <div class="mt-8 flow-root">
+ <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></div>
+ ) : (
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0"
+ >{i18n.str`Username`}</th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Name`}</th>
+ <th
+ scope="col"
+ class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Balance`}</th>
+ <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
+ <span class="sr-only">{i18n.str`Actions`}</span>
+ </th>
</tr>
- })}
+ </thead>
+ <tbody class="divide-y divide-gray-200">
+ {accounts.map((item, idx) => {
+ const balance = !item.balance
+ ? undefined
+ : Amounts.parse(item.balance.amount);
+ const noBalance = Amounts.isZero(item.balance.amount);
+ const balanceIsDebit =
+ item.balance &&
+ item.balance.credit_debit_indicator == "debit";
- </tbody>
- </table>
- )}
+ return (
+ <tr key={idx}>
+ <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
+ <a
+ href={routeShowAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {item.username}
+ </a>
+ </td>
+ <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
+ {item.name}
+ </td>
+ <td
+ data-negative={
+ noBalance
+ ? undefined
+ : balanceIsDebit
+ ? "true"
+ : "false"
+ }
+ class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 data-[negative=false]:text-green-600 data-[negative=true]:text-red-600 "
+ >
+ {!balance ? (
+ i18n.str`unknown`
+ ) : (
+ <span class="amount">
+ <RenderAmount
+ value={balance}
+ negative={balanceIsDebit}
+ spec={config.currency_specification}
+ />
+ </span>
+ )}
+ </td>
+ <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
+ <a
+ href={routeUpdatePasswordAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>change password</i18n.Translate>
+ </a>
+ <br />
+ <a
+ href={routeShowCashoutsAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>cashouts</i18n.Translate>
+ </a>
+ <br />
+ {noBalance ? (
+ <a
+ href={routeRemoveAccount.url({
+ account: item.username,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ <i18n.Translate>remove</i18n.Translate>
+ </a>
+ ) : undefined}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ )}
+ </div>
</div>
</div>
</div>
- </div>
- </Fragment>
-
-} \ No newline at end of file
+ </Fragment>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
index b7ef3aa00..4a8eb5b97 100644
--- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
+++ b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
@@ -1,8 +1,43 @@
-import { AmountString, Amounts, CurrencySpecification, HttpStatusCode, TalerCorebankApi, TalerError, assertUnreachable } from "@gnu-taler/taler-util";
-import { Attention, useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { format, getDate, getHours, getMonth, getYear, setDate, setHours, setMonth, setYear, sub } from "date-fns";
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ CurrencySpecification,
+ HttpStatusCode,
+ TalerCorebankApi,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ format,
+ getDate,
+ getHours,
+ getMonth,
+ getYear,
+ setDate,
+ setHours,
+ setMonth,
+ setYear,
+ sub,
+} from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
+import { privatePages } from "../../Routing.js";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
import { Transactions } from "../../components/Transactions/index.js";
import { useBankCoreApiContext } from "../../context/config.js";
@@ -10,247 +45,464 @@ import { useConversionInfo, useLastMonitorInfo } from "../../hooks/circuit.js";
import { RenderAmount } from "../PaytoWireTransferForm.js";
import { WireTransfer } from "../WireTransfer.js";
import { AccountList } from "./AccountList.js";
+import { RouteDefinition } from "../../route.js";
/**
* Query account information and show QR code if there is pending withdrawal
*/
interface Props {
- onCreateAccount: () => void;
- onShowAccountDetails: (aid: string) => void;
- onRemoveAccount: (aid: string) => void;
- onUpdateAccountPassword: (aid: string) => void;
- onShowCashoutForAccount: (aid: string) => void;
+ routeCreate: RouteDefinition<Record<string, never>>;
+
+ routeShowAccount: RouteDefinition<{ account: string }>;
+ routeRemoveAccount: RouteDefinition<{ account: string }>;
+ routeUpdatePasswordAccount: RouteDefinition<{ account: string }>;
+ routeShowCashoutsAccount: RouteDefinition<{ account: string }>;
onAuthorizationRequired: () => void;
}
-export function AdminHome({ onCreateAccount, onAuthorizationRequired, onRemoveAccount, onShowAccountDetails, onShowCashoutForAccount, onUpdateAccountPassword }: Props): VNode {
- return <Fragment>
- <Metrics />
- <WireTransfer onAuthorizationRequired={onAuthorizationRequired} />
-
- <Transactions account="admin" />
- <AccountList
- onCreateAccount={onCreateAccount}
- onRemoveAccount={onRemoveAccount}
- onShowCashoutForAccount={onShowCashoutForAccount}
- onShowAccountDetails={onShowAccountDetails}
- onUpdateAccountPassword={onUpdateAccountPassword}
- />
+export function AdminHome({
+ routeCreate,
+ routeRemoveAccount,
+ routeShowAccount,
+ routeShowCashoutsAccount,
+ routeUpdatePasswordAccount,
+ onAuthorizationRequired,
+}: Props): VNode {
+ return (
+ <Fragment>
+ <Metrics />
+ <WireTransfer onAuthorizationRequired={onAuthorizationRequired} />
- </Fragment>
+ <Transactions account="admin" />
+ <AccountList
+ routeCreate={routeCreate}
+ routeRemoveAccount={routeRemoveAccount}
+ routeShowAccount={routeShowAccount}
+ routeShowCashoutsAccount={routeShowCashoutsAccount}
+ routeUpdatePasswordAccount={routeUpdatePasswordAccount}
+ />
+ </Fragment>
+ );
}
-function getDateForTimeframe(which: number, timeframe: TalerCorebankApi.MonitorTimeframeParam, locale: Locale): string {
- const time = Date.now()
+function getDateForTimeframe(
+ which: number,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+ locale: Locale,
+): string {
+ const time = Date.now();
switch (timeframe) {
- case TalerCorebankApi.MonitorTimeframeParam.hour: return `${format(setHours(time, which), "HH", { locale })}hs`;
- case TalerCorebankApi.MonitorTimeframeParam.day: return format(setDate(time, which), "EEEE", { locale });
- case TalerCorebankApi.MonitorTimeframeParam.month: return format(setMonth(time, which), "MMMM", { locale });
- case TalerCorebankApi.MonitorTimeframeParam.year: return format(setYear(time, which), "yyyy", { locale });
- case TalerCorebankApi.MonitorTimeframeParam.decade: return format(setYear(time, which), "yyyy", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.hour:
+ return `${format(setHours(time, which), "HH", { locale })}hs`;
+ case TalerCorebankApi.MonitorTimeframeParam.day:
+ return format(setDate(time, which), "EEEE", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.month:
+ return format(setMonth(time, which), "MMMM", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.year:
+ return format(setYear(time, which), "yyyy", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.decade:
+ return format(setYear(time, which), "yyyy", { locale });
}
- assertUnreachable(timeframe)
+ assertUnreachable(timeframe);
}
-export function getTimeframesForDate(time: Date, timeframe: TalerCorebankApi.MonitorTimeframeParam): { current: number, previous: number } {
+export function getTimeframesForDate(
+ time: Date,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+): { current: number; previous: number } {
switch (timeframe) {
- case TalerCorebankApi.MonitorTimeframeParam.hour: return {
- current: getHours(sub(time, { hours: 1 })),
- previous: getHours(sub(time, { hours: 2 }))
- }
- case TalerCorebankApi.MonitorTimeframeParam.day: return {
- current: getDate(sub(time, { days: 1 })),
- previous: getDate(sub(time, { days: 2 }))
- }
- case TalerCorebankApi.MonitorTimeframeParam.month: return {
- current: getMonth(sub(time, { months: 1 })),
- previous: getMonth(sub(time, { months: 2 }))
- }
- case TalerCorebankApi.MonitorTimeframeParam.year: return {
- current: getYear(sub(time, { years: 1 })),
- previous: getYear(sub(time, { years: 2 }))
- }
- case TalerCorebankApi.MonitorTimeframeParam.decade: return {
- current: getYear(sub(time, { years: 10 })),
- previous: getYear(sub(time, { years: 20 }))
- }
- default: assertUnreachable(timeframe)
+ case TalerCorebankApi.MonitorTimeframeParam.hour:
+ return {
+ current: getHours(sub(time, { hours: 1 })),
+ previous: getHours(sub(time, { hours: 2 })),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.day:
+ return {
+ current: getDate(sub(time, { days: 1 })),
+ previous: getDate(sub(time, { days: 2 })),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.month:
+ return {
+ current: getMonth(sub(time, { months: 1 })),
+ previous: getMonth(sub(time, { months: 2 })),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.year:
+ return {
+ current: getYear(sub(time, { years: 1 })),
+ previous: getYear(sub(time, { years: 2 })),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.decade:
+ return {
+ current: getYear(sub(time, { years: 10 })),
+ previous: getYear(sub(time, { years: 20 })),
+ };
+ default:
+ assertUnreachable(timeframe);
}
}
-
function Metrics(): VNode {
- const { i18n, dateLocale } = useTranslationContext()
- const [metricType, setMetricType] = useState<TalerCorebankApi.MonitorTimeframeParam>(TalerCorebankApi.MonitorTimeframeParam.hour);
+ const { i18n, dateLocale } = useTranslationContext();
+ const [metricType, setMetricType] =
+ useState<TalerCorebankApi.MonitorTimeframeParam>(
+ TalerCorebankApi.MonitorTimeframeParam.hour,
+ );
const { config } = useBankCoreApiContext();
- const respInfo = useConversionInfo()
- const params = getTimeframesForDate(new Date(), metricType)
+ const respInfo = useConversionInfo();
+ const params = getTimeframesForDate(new Date(), metricType);
const resp = useLastMonitorInfo(params.current, params.previous, metricType);
if (!resp) return <Fragment />;
if (resp instanceof TalerError) {
- return <ErrorLoadingWithDebug error={resp} />
+ return <ErrorLoadingWithDebug error={resp} />;
}
if (!respInfo) return <Fragment />;
if (respInfo instanceof TalerError) {
- return <ErrorLoadingWithDebug error={respInfo} />
+ return <ErrorLoadingWithDebug error={respInfo} />;
}
if (respInfo.type === "fail") {
switch (respInfo.case) {
case HttpStatusCode.NotImplemented: {
- return <Attention type="danger" title={i18n.str`Cashout not implemented`}>
- </Attention>;
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`Cashout not implemented`}
+ ></Attention>
+ );
}
- default: assertUnreachable(respInfo.case)
+ default:
+ assertUnreachable(respInfo.case);
}
}
if (resp.current.type !== "ok" || resp.previous.type !== "ok") {
- return <Fragment />
+ return <Fragment />;
}
- return <Fragment>
- <div class="sm:hidden">
- <label for="tabs" class="sr-only"><i18n.Translate>Select a section</i18n.Translate></label>
- <select id="tabs" name="tabs" class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500" onChange={(e) => {
- // const op = e.currentTarget.value as typeof metricType
- setMetricType(e.currentTarget.value as any)
- }}>
- <option value={TalerCorebankApi.MonitorTimeframeParam.hour} selected={metricType == TalerCorebankApi.MonitorTimeframeParam.hour}><i18n.Translate>Last hour</i18n.Translate></option>
- <option value={TalerCorebankApi.MonitorTimeframeParam.day} selected={metricType == TalerCorebankApi.MonitorTimeframeParam.day}><i18n.Translate>Last day</i18n.Translate></option>
- <option value={TalerCorebankApi.MonitorTimeframeParam.month} selected={metricType == TalerCorebankApi.MonitorTimeframeParam.month}><i18n.Translate>Last month</i18n.Translate></option>
- <option value={TalerCorebankApi.MonitorTimeframeParam.year} selected={metricType == TalerCorebankApi.MonitorTimeframeParam.year}><i18n.Translate>Last year</i18n.Translate></option>
- </select>
- </div>
- <div class="hidden sm:block">
- <nav class="isolate flex divide-x divide-gray-200 rounded-lg shadow" aria-label="Tabs">
- <a href="#" onClick={(e) => { e.preventDefault(); setMetricType(TalerCorebankApi.MonitorTimeframeParam.hour) }} data-selected={metricType == TalerCorebankApi.MonitorTimeframeParam.hour} class="rounded-l-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10" >
- <span><i18n.Translate>Last hour</i18n.Translate></span>
- <span aria-hidden="true" data-selected={metricType == TalerCorebankApi.MonitorTimeframeParam.hour} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
- </a>
- <a href="#" onClick={(e) => { e.preventDefault(); setMetricType(TalerCorebankApi.MonitorTimeframeParam.day) }} data-selected={metricType == TalerCorebankApi.MonitorTimeframeParam.day} aria-current="page" class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
- <span><i18n.Translate>Last day</i18n.Translate></span>
- <span aria-hidden="true" data-selected={metricType == TalerCorebankApi.MonitorTimeframeParam.day} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
- </a>
- <a href="#" onClick={(e) => { e.preventDefault(); setMetricType(TalerCorebankApi.MonitorTimeframeParam.month) }} data-selected={metricType == TalerCorebankApi.MonitorTimeframeParam.month} class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
- <span><i18n.Translate>Last month</i18n.Translate></span>
- <span aria-hidden="true" data-selected={metricType == TalerCorebankApi.MonitorTimeframeParam.month} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
- </a>
- <a href="#" onClick={(e) => { e.preventDefault(); setMetricType(TalerCorebankApi.MonitorTimeframeParam.year) }} data-selected={metricType == TalerCorebankApi.MonitorTimeframeParam.year} class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10">
- <span><i18n.Translate>Last Year</i18n.Translate></span>
- <span aria-hidden="true" data-selected={metricType == TalerCorebankApi.MonitorTimeframeParam.year} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span>
- </a>
- </nav>
- </div>
-
- <div class="w-full flex justify-between">
- <h1 class="text-base font-semibold leading-7 text-gray-900 mt-5">
- {i18n.str`Trading volume on ${getDateForTimeframe(params.current, metricType, dateLocale)} compared to ${getDateForTimeframe(params.previous, metricType, dateLocale)}`}
- </h1>
- </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 :
- <Fragment>
- <div class="px-4 py-5 sm:p-6">
- <dt class="text-base font-normal text-gray-900">
- <i18n.Translate>Cashin</i18n.Translate>
- </dt>
- <MetricValue
- current={resp.current.body.cashinFiatVolume}
- previous={resp.previous.body.cashinFiatVolume}
- spec={respInfo.body.fiat_currency_specification}
- />
- </div>
- <div class="px-4 py-5 sm:p-6">
- <dt class="text-base font-normal text-gray-900">
- <i18n.Translate>Cashout</i18n.Translate>
- </dt>
- <MetricValue
- current={resp.current.body.cashoutFiatVolume}
- previous={resp.previous.body.cashoutFiatVolume}
- spec={respInfo.body.fiat_currency_specification}
- />
- </div>
- </Fragment>
- }
- <div class="px-4 py-5 sm:p-6">
- <dt class="text-base font-normal text-gray-900">
- <i18n.Translate>Payin</i18n.Translate>
- </dt>
- <MetricValue
- current={resp.current.body.talerInVolume}
- previous={resp.previous.body.talerInVolume}
- spec={config.currency_specification}
- />
+ return (
+ <Fragment>
+ <div class="sm:hidden">
+ <label for="tabs" class="sr-only">
+ <i18n.Translate>Select a section</i18n.Translate>
+ </label>
+ <select
+ id="tabs"
+ name="tabs"
+ class="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
+ onChange={(e) => {
+ // const op = e.currentTarget.value as typeof metricType
+ setMetricType(
+ e.currentTarget
+ .value as unknown as TalerCorebankApi.MonitorTimeframeParam,
+ );
+ }}
+ >
+ <option
+ value={TalerCorebankApi.MonitorTimeframeParam.hour}
+ selected={metricType == TalerCorebankApi.MonitorTimeframeParam.hour}
+ >
+ <i18n.Translate>Last hour</i18n.Translate>
+ </option>
+ <option
+ value={TalerCorebankApi.MonitorTimeframeParam.day}
+ selected={metricType == TalerCorebankApi.MonitorTimeframeParam.day}
+ >
+ <i18n.Translate>Last day</i18n.Translate>
+ </option>
+ <option
+ value={TalerCorebankApi.MonitorTimeframeParam.month}
+ selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.month
+ }
+ >
+ <i18n.Translate>Last month</i18n.Translate>
+ </option>
+ <option
+ value={TalerCorebankApi.MonitorTimeframeParam.year}
+ selected={metricType == TalerCorebankApi.MonitorTimeframeParam.year}
+ >
+ <i18n.Translate>Last year</i18n.Translate>
+ </option>
+ </select>
</div>
- <div class="px-4 py-5 sm:p-6">
- <dt class="text-base font-normal text-gray-900">
- <i18n.Translate>Payout</i18n.Translate>
- </dt>
- <MetricValue
- current={resp.current.body.talerOutVolume}
- previous={resp.previous.body.talerOutVolume}
- spec={config.currency_specification}
- />
+ <div class="hidden sm:block">
+ {/* FIXME: This should be LINKS */}
+ <nav
+ class="isolate flex divide-x divide-gray-200 rounded-lg shadow"
+ aria-label="Tabs"
+ >
+ <button
+ type="button"
+ onClick={(e) => {
+ e.preventDefault();
+ setMetricType(TalerCorebankApi.MonitorTimeframeParam.hour);
+ }}
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.hour
+ }
+ class="rounded-l-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Last hour</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.hour
+ }
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </button>
+ <button
+ type="button"
+ onClick={(e) => {
+ e.preventDefault();
+ setMetricType(TalerCorebankApi.MonitorTimeframeParam.day);
+ }}
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.day
+ }
+ class=" text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Last day</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.day
+ }
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </button>
+ <button
+ type="button"
+ onClick={(e) => {
+ e.preventDefault();
+ setMetricType(TalerCorebankApi.MonitorTimeframeParam.month);
+ }}
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.month
+ }
+ class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Last month</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.month
+ }
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </button>
+ <button
+ type="button"
+ onClick={(e) => {
+ e.preventDefault();
+ setMetricType(TalerCorebankApi.MonitorTimeframeParam.year);
+ }}
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.year
+ }
+ class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"
+ >
+ <span>
+ <i18n.Translate>Last Year</i18n.Translate>
+ </span>
+ <span
+ aria-hidden="true"
+ data-selected={
+ metricType == TalerCorebankApi.MonitorTimeframeParam.year
+ }
+ class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
+ ></span>
+ </button>
+ </nav>
</div>
- </dl>
- <div class="flex justify-end mt-2">
- <a href="#/download-stats"
- 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"
- ><i18n.Translate>
- download stats as CSV
- </i18n.Translate></a>
- </div>
- </Fragment>
+ <div class="w-full flex justify-between">
+ <h1 class="text-base font-semibold leading-7 text-gray-900 mt-5">
+ {i18n.str`Trading volume on ${getDateForTimeframe(
+ params.current,
+ metricType,
+ dateLocale,
+ )} compared to ${getDateForTimeframe(
+ params.previous,
+ metricType,
+ dateLocale,
+ )}`}
+ </h1>
+ </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 : (
+ <Fragment>
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Cashin</i18n.Translate>
+ </dt>
+ <MetricValue
+ current={resp.current.body.cashinFiatVolume}
+ previous={resp.previous.body.cashinFiatVolume}
+ spec={respInfo.body.fiat_currency_specification}
+ />
+ </div>
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Cashout</i18n.Translate>
+ </dt>
+ <MetricValue
+ current={resp.current.body.cashoutFiatVolume}
+ previous={resp.previous.body.cashoutFiatVolume}
+ spec={respInfo.body.fiat_currency_specification}
+ />
+ </div>
+ </Fragment>
+ )}
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Payin</i18n.Translate>
+ </dt>
+ <MetricValue
+ current={resp.current.body.talerInVolume}
+ previous={resp.previous.body.talerInVolume}
+ spec={config.currency_specification}
+ />
+ </div>
+ <div class="px-4 py-5 sm:p-6">
+ <dt class="text-base font-normal text-gray-900">
+ <i18n.Translate>Payout</i18n.Translate>
+ </dt>
+ <MetricValue
+ current={resp.current.body.talerOutVolume}
+ previous={resp.previous.body.talerOutVolume}
+ spec={config.currency_specification}
+ />
+ </div>
+ </dl>
+ <div class="flex justify-end mt-2">
+ <a
+ href={privatePages.statsDownload.url({})}
+ 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"
+ >
+ <i18n.Translate>download stats as CSV</i18n.Translate>
+ </a>
+ </div>
+ </Fragment>
+ );
}
-
-function MetricValue({ current, previous, spec }: { spec: CurrencySpecification, current: AmountString | undefined, previous: AmountString | undefined }): VNode {
- const { i18n } = useTranslationContext()
+function MetricValue({
+ current,
+ previous,
+ spec,
+}: {
+ spec: CurrencySpecification;
+ current: AmountString | undefined;
+ previous: AmountString | undefined;
+}): VNode {
+ const { i18n } = useTranslationContext();
const cmp = current && previous ? Amounts.cmp(current, previous) : 0;
- const cv = !current ? undefined : Amounts.stringifyValue(current)
- const currAmount = !cv ? undefined : Number.parseFloat(cv)
- const prevAmount = !previous ? undefined : Number.parseFloat(Amounts.stringifyValue(previous))
-
- const rate = !currAmount || Number.isNaN(currAmount) || !prevAmount || Number.isNaN(prevAmount) ? 0 :
- cmp === -1 ? 1 - Math.round(currAmount) / Math.round(prevAmount) :
- cmp === 1 ? (Math.round(currAmount) / Math.round(prevAmount)) - 1 : 0;
+ const cv = !current ? undefined : Amounts.stringifyValue(current);
+ const currAmount = !cv ? undefined : Number.parseFloat(cv);
+ const prevAmount = !previous
+ ? undefined
+ : Number.parseFloat(Amounts.stringifyValue(previous));
- const negative = cmp === 0 ? undefined : cmp === -1
- const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%`
- return <Fragment>
- <dd class="mt-1 block ">
- <div class="flex justify-start text-2xl items-baseline font-semibold text-indigo-600">
- {!current ? "-" : <RenderAmount value={Amounts.parseOrThrow(current)} spec={spec} hideSmall />}
- </div>
- <div class="flex flex-col">
+ const rate =
+ !currAmount ||
+ Number.isNaN(currAmount) ||
+ !prevAmount ||
+ Number.isNaN(prevAmount)
+ ? 0
+ : cmp === -1
+ ? 1 - Math.round(currAmount) / Math.round(prevAmount)
+ : cmp === 1
+ ? Math.round(currAmount) / Math.round(prevAmount) - 1
+ : 0;
- <div class="flex justify-end items-baseline text-2xl font-semibold text-indigo-600">
- <small class="ml-2 text-sm font-medium text-gray-500">
- <i18n.Translate>from</i18n.Translate> {!previous ? "-" : <RenderAmount value={Amounts.parseOrThrow(previous)} spec={spec} hideSmall />}
- </small>
+ const negative = cmp === 0 ? undefined : cmp === -1;
+ const rateStr = `${(Math.abs(rate) * 100).toFixed(2)}%`;
+ return (
+ <Fragment>
+ <dd class="mt-1 block ">
+ <div class="flex justify-start text-2xl items-baseline font-semibold text-indigo-600">
+ {!current ? (
+ "-"
+ ) : (
+ <RenderAmount
+ value={Amounts.parseOrThrow(current)}
+ spec={spec}
+ hideSmall
+ />
+ )}
</div>
- {!!rate &&
- <span data-negative={negative} class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 text-green-800 data-[negative=true]:bg-red-100 px-2 py-1 text-xs font-medium data-[negative=true]:text-red-700 whitespace-pre">
- {negative ?
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75" />
- </svg>
- :
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75" />
- </svg>
- }
-
- {negative ?
- <span class="sr-only"><i18n.Translate>Descreased by</i18n.Translate></span> :
- <span class="sr-only"><i18n.Translate>Increased by</i18n.Translate></span>
- }
- {rateStr}
- </span>
- }
- </div>
+ <div class="flex flex-col">
+ <div class="flex justify-end items-baseline text-2xl font-semibold text-indigo-600">
+ <small class="ml-2 text-sm font-medium text-gray-500">
+ <i18n.Translate>from</i18n.Translate>{" "}
+ {!previous ? (
+ "-"
+ ) : (
+ <RenderAmount
+ value={Amounts.parseOrThrow(previous)}
+ spec={spec}
+ hideSmall
+ />
+ )}
+ </small>
+ </div>
+ {!!rate && (
+ <span
+ data-negative={negative}
+ class="flex items-center gap-x-1.5 w-fit rounded-md bg-green-100 text-green-800 data-[negative=true]:bg-red-100 px-2 py-1 text-xs font-medium data-[negative=true]:text-red-700 whitespace-pre"
+ >
+ {negative ? (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75"
+ />
+ </svg>
+ ) : (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ fill="none"
+ viewBox="0 0 24 24"
+ stroke-width="1.5"
+ stroke="currentColor"
+ class="w-6 h-6"
+ >
+ <path
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ d="M12 19.5v-15m0 0l-6.75 6.75M12 4.5l6.75 6.75"
+ />
+ </svg>
+ )}
- </dd>
- </Fragment>
+ {negative ? (
+ <span class="sr-only">
+ <i18n.Translate>Descreased by</i18n.Translate>
+ </span>
+ ) : (
+ <span class="sr-only">
+ <i18n.Translate>Increased by</i18n.Translate>
+ </span>
+ )}
+ {rateStr}
+ </span>
+ )}
+ </div>
+ </dd>
+ </Fragment>
+ );
}
diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
index 1cfbd8234..c4e4266f9 100644
--- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -1,28 +1,57 @@
-import { HttpStatusCode, TalerCorebankApi, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
-import { Attention, LocalNotificationBanner, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
+/*
+ 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 {
+ HttpStatusCode,
+ TalerCorebankApi,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ LocalNotificationBanner,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { mutate } from "swr";
import { useBankCoreApiContext } from "../../context/config.js";
import { useBackendState } from "../../hooks/backend.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
-import { getRandomPassword } from "../rnd.js";
-import { AccountForm, AccountFormData } from "./AccountForm.js";
+import { RouteDefinition } from "../../route.js";
+import { AccountForm } from "./AccountForm.js";
export function CreateNewAccount({
- onCancel,
+ routeCancel,
onCreateSuccess,
}: {
- onCancel: () => void;
+ routeCancel: RouteDefinition<Record<string, never>>;
onCreateSuccess: () => void;
}): VNode {
const { i18n } = useTranslationContext();
- const { state: credentials } = useBackendState()
- const token = credentials.status !== "loggedIn" ? undefined : credentials.token
+ const { state: credentials } = useBackendState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
const { api } = useBankCoreApiContext();
- const [submitAccount, setSubmitAccount] = useState<TalerCorebankApi.RegisterAccountRequest | undefined>();
- const [notification, notify, handleError] = useLocalNotification()
+ const [submitAccount, setSubmitAccount] = useState<
+ TalerCorebankApi.RegisterAccountRequest | undefined
+ >();
+ const [notification, notify, handleError] = useLocalNotification();
async function doCreate() {
if (!submitAccount || !token) return;
@@ -41,83 +70,108 @@ export function CreateNewAccount({
const resp = await api.createAccount(token, submitAccount);
if (resp.type === "ok") {
- mutate(() => true)// clean account list
+ mutate(() => true); // clean account list
notifyInfo(
i18n.str`Account created with password "${submitAccount.password}". The user must change the password on the next login.`,
);
onCreateSuccess();
} else {
switch (resp.case) {
- case HttpStatusCode.BadRequest: return notify({
- type: "error",
- title: i18n.str`Server replied that phone or email is invalid`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- title: i18n.str`The rights to perform the operation are not sufficient`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: return notify({
- type: "error",
- title: i18n.str`Account username is already taken`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: return notify({
- type: "error",
- title: i18n.str`Account id is already taken`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({
- type: "error",
- title: i18n.str`Bank ran out of bonus credit.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: return notify({
- type: "error",
- title: i18n.str`Account username can't be used because is reserved`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return notify({
- type: "error",
- title: i18n.str`Only admin is allow to set debt limit.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_MISSING_TAN_INFO: return notify({
- type: "error",
- title: i18n.str`No information for the selected authentication channel.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: return notify({
- type: "error",
- title: i18n.str`Authentication channel is not supported.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: return notify({
- type: "error",
- title: i18n.str`Only admin can create accounts with second factor authentication.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- default: assertUnreachable(resp)
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`Server replied that phone or email is invalid`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`The rights to perform the operation are not sufficient`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
+ return notify({
+ type: "error",
+ title: i18n.str`Account username is already taken`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
+ return notify({
+ type: "error",
+ title: i18n.str`Account id is already taken`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Bank ran out of bonus credit.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return notify({
+ type: "error",
+ title: i18n.str`Account username can't be used because is reserved`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Only admin is allow to set debt limit.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return notify({
+ type: "error",
+ title: i18n.str`No information for the selected authentication channel.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return notify({
+ type: "error",
+ title: i18n.str`Authentication channel is not supported.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
+ return notify({
+ type: "error",
+ title: i18n.str`Only admin can create accounts with second factor authentication.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ default:
+ assertUnreachable(resp);
}
}
- })
+ });
}
if (!(credentials.status === "loggedIn" && credentials.isUserAdministrator)) {
- return <Attention type="warning" title={i18n.str`Can't create accounts`} onClose={onCancel}>
- <i18n.Translate>Only system admin can create accounts.</i18n.Translate>
- </Attention>
+ return (
+ <Fragment>
+ <Attention type="warning" title={i18n.str`Can't create accounts`}>
+ <i18n.Translate>
+ Only system admin can create accounts.
+ </i18n.Translate>
+ </Attention>
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeCancel.url({})}
+ class="inline-flex w-full justify-center 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"
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </a>
+ </div>
+ </Fragment>
+ );
}
return (
@@ -137,26 +191,24 @@ export function CreateNewAccount({
}}
>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- {onCancel ?
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- : <div />
- }
- <button type="submit"
+ <a
+ href={routeCancel.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
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"
disabled={!submitAccount}
onClick={(e) => {
- e.preventDefault()
- doCreate()
+ e.preventDefault();
+ doCreate();
}}
>
<i18n.Translate>Create</i18n.Translate>
</button>
</div>
-
</AccountForm>
</div>
);
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
index beadad957..36e1a4eac 100644
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
@@ -1,5 +1,36 @@
-import { AbsoluteTime, Amounts, HttpStatusCode, TalerError, TalerErrorCode, TranslatedString } from "@gnu-taler/taler-util";
-import { Attention, Loading, LocalNotificationBanner, ShowInputErrorLabel, notifyInfo, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser";
+/*
+ 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 {
+ AbsoluteTime,
+ Amounts,
+ HttpStatusCode,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ LocalNotificationBanner,
+ ShowInputErrorLabel,
+ notifyInfo,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
@@ -9,43 +40,46 @@ import { useBackendState } from "../../hooks/backend.js";
import { undefinedIfEmpty } from "../../utils.js";
import { LoginForm } from "../LoginForm.js";
import { doAutoFocus } from "../PaytoWireTransferForm.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { useBankState } from "../../hooks/bank-state.js";
+import { RouteDefinition } from "../../route.js";
export function RemoveAccount({
account,
- onCancel,
+ routeCancel,
onUpdateSuccess,
onAuthorizationRequired,
focus,
}: {
focus?: boolean;
- onAuthorizationRequired: () => void,
- onCancel: () => void;
+ onAuthorizationRequired: () => void;
+ routeCancel: RouteDefinition<Record<string, never>>;
onUpdateSuccess: () => void;
account: string;
}): VNode {
const { i18n } = useTranslationContext();
const result = useAccountDetails(account);
- const [accountName, setAccountName] = useState<string | undefined>()
+ const [accountName, setAccountName] = useState<string | undefined>();
const { state } = useBackendState();
- const token = state.status !== "loggedIn" ? undefined : state.token
- const { api } = useBankCoreApiContext()
- const [notification, notify, handleError] = useLocalNotification()
- const [, updateBankState] = useBankState()
+ const token = state.status !== "loggedIn" ? undefined : state.token;
+ const { api } = useBankCoreApiContext();
+ const [notification, notify, handleError] = useLocalNotification();
+ const [, updateBankState] = useBankState();
if (!result) {
- return <Loading />
+ return <Loading />;
}
if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />
+ return <ErrorLoadingWithDebug error={result} />;
}
if (result.type === "fail") {
switch (result.case) {
- case HttpStatusCode.Unauthorized: return <LoginForm currentUser={account} />
- case HttpStatusCode.NotFound: return <LoginForm currentUser={account} />
- default: assertUnreachable(result)
+ case HttpStatusCode.Unauthorized:
+ return <LoginForm currentUser={account} />;
+ case HttpStatusCode.NotFound:
+ return <LoginForm currentUser={account} />;
+ default:
+ assertUnreachable(result);
}
}
@@ -55,9 +89,24 @@ export function RemoveAccount({
}
const isBalanceEmpty = Amounts.isZero(balance);
if (!isBalanceEmpty) {
- return <Attention type="warning" title={i18n.str`Can't delete the account`} onClose={onCancel}>
- <i18n.Translate>The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.</i18n.Translate>
- </Attention>
+ return (
+ <Fragment>
+ <Attention type="warning" title={i18n.str`Can't delete the account`}>
+ <i18n.Translate>
+ The account can't be delete while still holding some balance. First
+ make sure that the owner make a complete cashout.
+ </i18n.Translate>
+ </Attention>
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeCancel.url({})}
+ class="inline-flex w-full justify-center 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"
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </a>
+ </div>
+ </Fragment>
+ );
}
async function doRemove() {
@@ -69,45 +118,49 @@ export function RemoveAccount({
onUpdateSuccess();
} else {
switch (resp.case) {
- case HttpStatusCode.Unauthorized: return notify({
- type: "error",
- title: i18n.str`No enough permission to delete the account.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`The username was not found.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: return notify({
- type: "error",
- title: i18n.str`Can't delete a reserved username.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
- case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO: return notify({
- type: "error",
- title: i18n.str`Can't delete an account with balance different than zero.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- })
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`No enough permission to delete the account.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The username was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return notify({
+ type: "error",
+ title: i18n.str`Can't delete a reserved username.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO:
+ return notify({
+ type: "error",
+ title: i18n.str`Can't delete an account with balance different than zero.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
case HttpStatusCode.Accepted: {
updateBankState("currentChallenge", {
operation: "delete-account",
id: String(resp.body.challenge_id),
sent: AbsoluteTime.never(),
request: account,
- })
- return onAuthorizationRequired()
+ });
+ return onAuthorizationRequired();
}
default: {
- assertUnreachable(resp)
+ assertUnreachable(resp);
}
}
}
- })
+ });
}
const errors = undefinedIfEmpty({
@@ -118,12 +171,14 @@ export function RemoveAccount({
: undefined,
});
-
return (
<div>
<LocalNotificationBanner notification={notification} />
- <Attention type="warning" title={i18n.str`You are going to remove the account`}>
+ <Attention
+ type="warning"
+ title={i18n.str`You are going to remove the account`}
+ >
<i18n.Translate>This step can't be undone.</i18n.Translate>
</Attention>
@@ -137,13 +192,12 @@ export function RemoveAccount({
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
+ onSubmit={(e) => {
+ e.preventDefault();
}}
>
<div class="px-4 py-6 sm:p-8">
<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"
@@ -158,10 +212,12 @@ export function RemoveAccount({
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
name="password"
id="password"
- data-error={!!errors?.accountName && accountName !== undefined}
+ data-error={
+ !!errors?.accountName && accountName !== undefined
+ }
value={accountName ?? ""}
onChange={(e) => {
- setAccountName(e.currentTarget.value)
+ setAccountName(e.currentTarget.value);
}}
placeholder={account}
autocomplete="off"
@@ -171,30 +227,28 @@ export function RemoveAccount({
isDirty={accountName !== undefined}
/>
</div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate>enter the account name that is going to be deleted</i18n.Translate>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ enter the account name that is going to be deleted
+ </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">
- {onCancel ?
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- : <div />
- }
- <button type="submit"
+ <a
+ href={routeCancel.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600"
disabled={!!errors}
onClick={(e) => {
- e.preventDefault()
- doRemove()
+ e.preventDefault();
+ doRemove();
}}
>
<i18n.Translate>Delete</i18n.Translate>
diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
index 254a0d81f..93bd2c89d 100644
--- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx
+++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx
@@ -17,13 +17,13 @@ import {
AbsoluteTime,
Amounts,
HttpStatusCode,
- TalerCorebankApi,
TalerError,
TalerErrorCode,
TranslatedString,
+ assertUnreachable,
encodeCrock,
getRandomBytes,
- parsePaytoUri
+ parsePaytoUri,
} from "@gnu-taler/taler-util";
import {
Attention,
@@ -32,7 +32,7 @@ import {
ShowInputErrorLabel,
notifyInfo,
useLocalNotification,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
@@ -40,24 +40,22 @@ import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js
import { VersionHint, useBankCoreApiContext } from "../../context/config.js";
import { useAccountDetails } from "../../hooks/access.js";
import { useBackendState } from "../../hooks/backend.js";
-import {
- useConversionInfo,
- useEstimator
-} from "../../hooks/circuit.js";
-import {
- TanChannel,
- undefinedIfEmpty
-} from "../../utils.js";
-import { LoginForm } from "../LoginForm.js";
-import { InputAmount, RenderAmount, doAutoFocus } from "../PaytoWireTransferForm.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
import { useBankState } from "../../hooks/bank-state.js";
+import { useConversionInfo, useEstimator } from "../../hooks/circuit.js";
+import { RouteDefinition } from "../../route.js";
+import { TanChannel, undefinedIfEmpty } from "../../utils.js";
+import { LoginForm } from "../LoginForm.js";
+import {
+ InputAmount,
+ RenderAmount,
+ doAutoFocus,
+} from "../PaytoWireTransferForm.js";
interface Props {
account: string;
- focus?: boolean,
- onAuthorizationRequired: () => void,
- onCancel?: () => void;
+ focus?: boolean;
+ onAuthorizationRequired: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
}
type FormType = {
@@ -70,12 +68,11 @@ type ErrorFrom<T> = {
[P in keyof T]+?: string;
};
-
export function CreateCashout({
account: accountName,
onAuthorizationRequired,
focus,
- onCancel,
+ routeClose,
}: Props): VNode {
const { i18n } = useTranslationContext();
const resultAccount = useAccountDetails(accountName);
@@ -84,95 +81,130 @@ export function CreateCashout({
estimateByDebit: calculateFromDebit,
} = useEstimator();
const { state: credentials } = useBackendState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials
- const [, updateBankState] = useBankState()
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const [, updateBankState] = useBankState();
- const { api, config, hints } = useBankCoreApiContext()
- const [form, setForm] = useState<Partial<FormType>>({ isDebit: true, });
- const [notification, notify, handleError] = useLocalNotification()
+ const { api, config, hints } = useBankCoreApiContext();
+ const [form, setForm] = useState<Partial<FormType>>({ isDebit: true });
+ const [notification, notify, handleError] = useLocalNotification();
const info = useConversionInfo();
if (!config.allow_conversion) {
- return <Attention type="warning" title={i18n.str`Unable to create a cashout`} onClose={onCancel}>
- <i18n.Translate>The bank configuration does not support cashout operations.</i18n.Translate>
- </Attention>
+ return (
+ <Fragment>
+ <Attention type="warning" title={i18n.str`Unable to create a cashout`}>
+ <i18n.Translate>
+ The bank configuration does not support cashout operations.
+ </i18n.Translate>
+ </Attention>
+ <div class="mt-5 sm:mt-6">
+ <a
+ href={routeClose.url({})}
+ class="inline-flex w-full justify-center 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"
+ >
+ <i18n.Translate>Close</i18n.Translate>
+ </a>
+ </div>
+ </Fragment>
+ );
}
- const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1
+ const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1;
if (!resultAccount) {
- return <Loading />
+ return <Loading />;
}
if (resultAccount instanceof TalerError) {
- return <ErrorLoadingWithDebug error={resultAccount} />
+ return <ErrorLoadingWithDebug error={resultAccount} />;
}
if (resultAccount.type === "fail") {
switch (resultAccount.case) {
- case HttpStatusCode.Unauthorized: return <LoginForm currentUser={accountName} />
- case HttpStatusCode.NotFound: return <LoginForm currentUser={accountName} />
- default: assertUnreachable(resultAccount)
+ case HttpStatusCode.Unauthorized:
+ return <LoginForm currentUser={accountName} />;
+ case HttpStatusCode.NotFound:
+ return <LoginForm currentUser={accountName} />;
+ default:
+ assertUnreachable(resultAccount);
}
}
if (!info) {
- return <Loading />
+ return <Loading />;
}
if (info instanceof TalerError) {
- return <ErrorLoadingWithDebug error={info} />
+ return <ErrorLoadingWithDebug error={info} />;
}
if (info.type === "fail") {
switch (info.case) {
case HttpStatusCode.NotImplemented: {
- return <Attention type="danger" title={i18n.str`Cashout not implemented`}>
- </Attention>;
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`Cashout not implemented`}
+ ></Attention>
+ );
}
- default: assertUnreachable(info.case)
+ default:
+ assertUnreachable(info.case);
}
}
-
- const conversionInfo = info.body.conversion_rate
+ const conversionInfo = info.body.conversion_rate;
if (!conversionInfo) {
- return <div>conversion enabled but server replied without conversion_rate</div>
+ return (
+ <div>conversion enabled but server replied without conversion_rate</div>
+ );
}
const account = {
balance: Amounts.parseOrThrow(resultAccount.body.balance.amount),
- balanceIsDebit: resultAccount.body.balance.credit_debit_indicator == "debit",
- debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold)
- }
+ balanceIsDebit:
+ resultAccount.body.balance.credit_debit_indicator == "debit",
+ debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold),
+ };
- const { fiat_currency, regional_currency, fiat_currency_specification, regional_currency_specification } = info.body
+ const {
+ fiat_currency,
+ regional_currency,
+ fiat_currency_specification,
+ regional_currency_specification,
+ } = info.body;
const regionalZero = Amounts.zeroOfCurrency(regional_currency);
const fiatZero = Amounts.zeroOfCurrency(fiat_currency);
const limit = account.balanceIsDebit
? Amounts.sub(account.debitThreshold, account.balance).amount
: Amounts.add(account.balance, account.debitThreshold).amount;
- const zeroCalc = { debit: regionalZero, credit: fiatZero, beforeFee: fiatZero };
+ const zeroCalc = {
+ debit: regionalZero,
+ credit: fiatZero,
+ beforeFee: fiatZero,
+ };
const [calc, setCalc] = useState(zeroCalc);
const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee);
- const sellRate = conversionInfo.cashout_ratio
+ const sellRate = conversionInfo.cashout_ratio;
/**
* can be in regional currency or fiat currency
* 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 () => {
if (Amounts.isNonZero(inputAmount)) {
- const resp = await (form.isDebit ?
- calculateFromDebit(inputAmount, sellFee) :
- calculateFromCredit(inputAmount, sellFee));
- setCalc(resp)
+ const resp = await (form.isDebit
+ ? calculateFromDebit(inputAmount, sellFee)
+ : calculateFromCredit(inputAmount, sellFee));
+ setCalc(resp);
}
- })
+ });
}
- doAsync()
+ doAsync();
}, [form.amount, form.isDebit]);
const balanceAfter = Amounts.sub(account.balance, calc.debit).amount;
@@ -198,10 +230,13 @@ export function CreateCashout({
const trimmedAmountStr = form.amount?.trim();
async function createCashout() {
- const request_uid = encodeCrock(getRandomBytes(32))
+ const request_uid = encodeCrock(getRandomBytes(32));
await handleError(async () => {
- //new cashout api doesn't require channel
- const validChannel = !OLD_CASHOUT_API || config.supported_tan_channels.length === 0 || form.channel
+ // new cashout api doesn't require channel
+ const validChannel =
+ !OLD_CASHOUT_API ||
+ config.supported_tan_channels.length === 0 ||
+ form.channel;
if (!creds || !form.subject || !validChannel) return;
const request = {
@@ -210,10 +245,10 @@ export function CreateCashout({
amount_debit: Amounts.stringify(calc.debit),
subject: form.subject,
tan_channel: form.channel,
- }
- const resp = await api.createCashout(creds, request)
+ };
+ const resp = await api.createCashout(creds, request);
if (resp.type === "ok") {
- notifyInfo(i18n.str`Cashout created`)
+ notifyInfo(i18n.str`Cashout created`);
} else {
switch (resp.case) {
case HttpStatusCode.Accepted: {
@@ -222,102 +257,127 @@ export function CreateCashout({
id: String(resp.body.challenge_id),
sent: AbsoluteTime.never(),
request,
- })
- return onAuthorizationRequired()
+ });
+ return onAuthorizationRequired();
}
- case HttpStatusCode.NotFound: return notify({
- type: "error",
- title: i18n.str`Account not found`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: return notify({
- type: "error",
- title: i18n.str`Duplicated request detected, check if the operation succeded or try again.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case TalerErrorCode.BANK_BAD_CONVERSION: return notify({
- type: "error",
- title: i18n.str`The conversion rate was incorrectly applied`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({
- type: "error",
- title: i18n.str`The account does not have sufficient funds`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case HttpStatusCode.NotImplemented: return notify({
- type: "error",
- title: i18n.str`Cashouts are not supported`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return notify({
- type: "error",
- title: i18n.str`Missing cashout URI in the profile`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return notify({
- type: "error",
- title: i18n.str`Sending the confirmation message failed, retry later or contact the administrator.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
+ return notify({
+ type: "error",
+ title: i18n.str`Duplicated request detected, check if the operation succeded or try again.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_BAD_CONVERSION:
+ return notify({
+ type: "error",
+ title: i18n.str`The conversion rate was incorrectly applied`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`The account does not have sufficient funds`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case HttpStatusCode.NotImplemented:
+ return notify({
+ type: "error",
+ title: i18n.str`Cashouts are not supported`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return notify({
+ type: "error",
+ title: i18n.str`Missing cashout URI in the profile`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
+ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
+ return notify({
+ type: "error",
+ title: i18n.str`Sending the confirmation message failed, retry later or contact the administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ });
}
- assertUnreachable(resp)
+ assertUnreachable(resp);
}
- })
+ });
}
- const cashoutDisabled = config.supported_tan_channels.length < 1 || !resultAccount.body.cashout_payto_uri
- console.log("disab", cashoutDisabled)
- const cashoutAccount = !resultAccount.body.cashout_payto_uri ? undefined :
- parsePaytoUri(resultAccount.body.cashout_payto_uri);
- const cashoutAccountName = !cashoutAccount ? undefined : cashoutAccount.targetPath
+ const cashoutDisabled =
+ config.supported_tan_channels.length < 1 ||
+ !resultAccount.body.cashout_payto_uri;
+ console.log("disab", cashoutDisabled);
+ const cashoutAccount = !resultAccount.body.cashout_payto_uri
+ ? undefined
+ : parsePaytoUri(resultAccount.body.cashout_payto_uri);
+ const cashoutAccountName = !cashoutAccount
+ ? undefined
+ : cashoutAccount.targetPath;
return (
<div>
<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">
-
<section class="mt-4 rounded-sm px-4 py-6 p-8 ">
- <h2 id="summary-heading" class="font-medium text-lg"><i18n.Translate>Cashout</i18n.Translate></h2>
+ <h2 id="summary-heading" class="font-medium text-lg">
+ <i18n.Translate>Cashout</i18n.Translate>
+ </h2>
<dl class="mt-4 space-y-4">
<div class="justify-between items-center flex">
- <dt class="text-sm text-gray-600"><i18n.Translate>Convertion rate</i18n.Translate></dt>
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Convertion rate</i18n.Translate>
+ </dt>
<dd class="text-sm text-gray-900">{sellRate}</dd>
</div>
-
<div class="flex items-center justify-between border-t-2 afu pt-4">
<dt class="flex items-center text-sm text-gray-600">
- <span><i18n.Translate>Balance</i18n.Translate></span>
+ <span>
+ <i18n.Translate>Balance</i18n.Translate>
+ </span>
</dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={account.balance} spec={regional_currency_specification} />
+ <RenderAmount
+ value={account.balance}
+ spec={regional_currency_specification}
+ />
</dd>
</div>
<div class="flex items-center justify-between border-t-2 afu pt-4">
<dt class="flex items-center text-sm text-gray-600">
- <span><i18n.Translate>Fee</i18n.Translate></span>
+ <span>
+ <i18n.Translate>Fee</i18n.Translate>
+ </span>
</dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={sellFee} spec={fiat_currency_specification} />
+ <RenderAmount
+ value={sellFee}
+ spec={fiat_currency_specification}
+ />
</dd>
</div>
- {cashoutAccountName ?
+ {cashoutAccountName ? (
<div class="flex items-center justify-between border-t-2 afu pt-4">
<dt class="flex items-center text-sm text-gray-600">
- <span><i18n.Translate>To account</i18n.Translate></span>
+ <span>
+ <i18n.Translate>To account</i18n.Translate>
+ </span>
</dt>
- <dd class="text-sm text-gray-900">
- {cashoutAccountName}
- </dd>
- </div> :
+ <dd class="text-sm text-gray-900">{cashoutAccountName}</dd>
+ </div>
+ ) : (
<div class="flex items-center justify-between border-t-2 afu pt-4">
<Attention type="warning" title={i18n.str`No cashout account`}>
<i18n.Translate>
@@ -325,17 +385,15 @@ export function CreateCashout({
</i18n.Translate>
</Attention>
</div>
- }
-
+ )}
</dl>
-
</section>
<form
class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
autoCapitalize="none"
autoCorrect="off"
- onSubmit={e => {
- e.preventDefault()
+ onSubmit={(e) => {
+ e.preventDefault();
}}
>
<div class="px-4 py-6 sm:p-8">
@@ -370,7 +428,6 @@ export function CreateCashout({
isDirty={form.subject !== undefined}
/>
</div>
-
</div>
{/* amount */}
@@ -384,14 +441,25 @@ export function CreateCashout({
? i18n.str`Amount to send`
: i18n.str`Amount to receive`}
</label>
- <button type="button" data-enabled={form.isDebit} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
+ <button
+ type="button"
+ data-enabled={form.isDebit}
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
onClick={() => {
- form.isDebit = !form.isDebit
- updateForm(structuredClone(form))
- }}>
- <span aria-hidden="true" data-enabled={form.isDebit} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
+ form.isDebit = !form.isDebit;
+ updateForm(structuredClone(form));
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={form.isDebit}
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
</button>
-
</div>
<div class="mt-2">
<InputAmount
@@ -399,53 +467,78 @@ export function CreateCashout({
left
currency={limit.currency}
value={trimmedAmountStr}
- onChange={cashoutDisabled ? undefined : (value) => {
- form.amount = value;
- updateForm(structuredClone(form));
- }}
+ onChange={
+ cashoutDisabled
+ ? undefined
+ : (value) => {
+ form.amount = value;
+ updateForm(structuredClone(form));
+ }
+ }
/>
<ShowInputErrorLabel
message={errors?.amount}
isDirty={form.amount !== undefined}
/>
</div>
-
</div>
{Amounts.isZero(calc.credit) ? undefined : (
<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>Total cost</i18n.Translate></dt>
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Total cost</i18n.Translate>
+ </dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={calc.debit} negative withColor spec={regional_currency_specification} />
+ <RenderAmount
+ value={calc.debit}
+ negative
+ withColor
+ spec={regional_currency_specification}
+ />
</dd>
</div>
-
<div class="flex items-center justify-between border-t-2 afu pt-4">
<dt class="flex items-center text-sm text-gray-600">
- <span><i18n.Translate>Balance left</i18n.Translate></span>
+ <span>
+ <i18n.Translate>Balance left</i18n.Translate>
+ </span>
</dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={balanceAfter} spec={regional_currency_specification} />
+ <RenderAmount
+ value={balanceAfter}
+ spec={regional_currency_specification}
+ />
</dd>
</div>
- {Amounts.isZero(sellFee) || Amounts.isZero(calc.beforeFee) ? undefined : (
+ {Amounts.isZero(sellFee) ||
+ 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><i18n.Translate>Before fee</i18n.Translate></span>
+ <span>
+ <i18n.Translate>Before fee</i18n.Translate>
+ </span>
</dt>
<dd class="text-sm text-gray-900">
- <RenderAmount value={calc.beforeFee} spec={fiat_currency_specification} />
+ <RenderAmount
+ value={calc.beforeFee}
+ spec={fiat_currency_specification}
+ />
</dd>
</div>
)}
<div class="flex justify-between items-center border-t-2 afu pt-4">
- <dt class="text-lg text-gray-900 font-medium"><i18n.Translate>Total cashout transfer</i18n.Translate></dt>
+ <dt class="text-lg text-gray-900 font-medium">
+ <i18n.Translate>Total cashout transfer</i18n.Translate>
+ </dt>
<dd class="text-lg text-gray-900 font-medium">
- <RenderAmount value={calc.credit} withColor spec={fiat_currency_specification} />
+ <RenderAmount
+ value={calc.credit}
+ withColor
+ spec={fiat_currency_specification}
+ />
</dd>
</div>
</dl>
@@ -453,15 +546,20 @@ export function CreateCashout({
)}
{/* channel, not shown if new cashout api */}
- {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels.length === 0 ?
+ {!OLD_CASHOUT_API ? undefined : config.supported_tan_channels
+ .length === 0 ? (
<div class="sm:col-span-5">
- <Attention type="warning" title={i18n.str`No cashout channel available`}>
+ <Attention
+ type="warning"
+ title={i18n.str`No cashout channel available`}
+ >
<i18n.Translate>
- Before doing a cashout the server need to provide an second channel to confirm the operation
+ Before doing a cashout the server need to provide an
+ second channel to confirm the operation
</i18n.Translate>
</Attention>
</div>
- :
+ ) : (
<div class="sm:col-span-5">
<label
class="block text-sm font-medium leading-6 text-gray-900"
@@ -471,72 +569,124 @@ export function CreateCashout({
</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">
- {config.supported_tan_channels.indexOf(TanChannel.EMAIL) === -1 ? undefined :
- <label onClick={() => {
- if (!resultAccount.body.contact_data?.email) return;
- form.channel = TanChannel.EMAIL
- updateForm(structuredClone(form))
- }} data-disabled={!resultAccount.body.contact_data?.email} data-selected={form.channel === TanChannel.EMAIL}
- 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" />
+ {config.supported_tan_channels.indexOf(
+ TanChannel.EMAIL,
+ ) === -1 ? undefined : (
+ <label
+ onClick={() => {
+ if (!resultAccount.body.contact_data?.email) return;
+ form.channel = TanChannel.EMAIL;
+ updateForm(structuredClone(form));
+ }}
+ data-disabled={
+ !resultAccount.body.contact_data?.email
+ }
+ data-selected={form.channel === TanChannel.EMAIL}
+ 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 ">
+ <span
+ id="project-type-0-label"
+ class="block text-sm font-medium text-gray-900 "
+ >
<i18n.Translate>Email</i18n.Translate>
</span>
- {!resultAccount.body.contact_data?.email && i18n.str`add a email in your profile to enable this option`}
+ {!resultAccount.body.contact_data?.email &&
+ i18n.str`add a email in your profile to enable this option`}
</span>
</span>
- <svg data-selected={form.channel === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+ <svg
+ data-selected={form.channel === TanChannel.EMAIL}
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
</svg>
</label>
- }
-
- {config.supported_tan_channels.indexOf(TanChannel.SMS) === -1 ? undefined :
- <label onClick={() => {
- if (!resultAccount.body.contact_data?.phone) return;
- form.channel = TanChannel.SMS
- updateForm(structuredClone(form))
- }} data-disabled={!resultAccount.body.contact_data?.phone} data-selected={form.channel === TanChannel.SMS}
- 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" />
+ )}
+
+ {config.supported_tan_channels.indexOf(TanChannel.SMS) ===
+ -1 ? undefined : (
+ <label
+ onClick={() => {
+ if (!resultAccount.body.contact_data?.phone) return;
+ form.channel = TanChannel.SMS;
+ updateForm(structuredClone(form));
+ }}
+ data-disabled={
+ !resultAccount.body.contact_data?.phone
+ }
+ data-selected={form.channel === TanChannel.SMS}
+ 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-1-label" class="block text-sm font-medium text-gray-900">
+ <span
+ id="project-type-1-label"
+ class="block text-sm font-medium text-gray-900"
+ >
<i18n.Translate>SMS</i18n.Translate>
</span>
- {!resultAccount.body.contact_data?.phone && i18n.str`add a phone number in your profile to enable this option`}
+ {!resultAccount.body.contact_data?.phone &&
+ i18n.str`add a phone number in your profile to enable this option`}
</span>
</span>
- <svg data-selected={form.channel === TanChannel.SMS} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
+ <svg
+ data-selected={form.channel === TanChannel.SMS}
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
</svg>
</label>
- }
+ )}
</div>
</div>
-
</div>
- }
+ )}
</div>
</div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- {onCancel ?
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- : <div />
- }
- <button type="submit"
+ <a
+ href={routeClose.url({})}
+ type="button"
+ class="text-sm font-semibold leading-6 text-gray-900"
+ >
+ <i18n.Translate>Cancel</i18n.Translate>
+ </a>
+ <button
+ type="submit"
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"
disabled={!!errors}
onClick={(e) => {
- e.preventDefault()
- createCashout()
+ e.preventDefault();
+ createCashout();
}}
>
<i18n.Translate>Cashout</i18n.Translate>
diff --git a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
index 478d631fb..589e29793 100644
--- a/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
+++ b/packages/demobank-ui/src/pages/business/ShowCashoutDetails.tsx
@@ -17,93 +17,99 @@ import {
Amounts,
HttpStatusCode,
TalerError,
- TalerErrorCode,
- TranslatedString
+ assertUnreachable,
} from "@gnu-taler/taler-util";
import {
Attention,
Loading,
- LocalNotificationBanner,
- ShowInputErrorLabel,
- useLocalNotification,
- useTranslationContext
+ useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { Fragment, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { mutate } from "swr";
+import { VNode, h } from "preact";
import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
-import { useBankCoreApiContext } from "../../context/config.js";
-import { useBackendState } from "../../hooks/backend.js";
-import {
- useCashoutDetails, useConversionInfo
-} from "../../hooks/circuit.js";
-import {
- undefinedIfEmpty
-} from "../../utils.js";
+import { useCashoutDetails, useConversionInfo } from "../../hooks/circuit.js";
+import { RouteDefinition } from "../../route.js";
import { RenderAmount } from "../PaytoWireTransferForm.js";
-import { assertUnreachable } from "../WithdrawalOperationPage.js";
interface Props {
id: string;
- onCancel: () => void;
+ routeClose: RouteDefinition<Record<string, never>>;
}
-export function ShowCashoutDetails({
- id,
- onCancel,
-}: Props): VNode {
+export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
const { i18n, dateLocale } = useTranslationContext();
- const { state } = useBackendState();
- const cid = Number.parseInt(id, 10)
+ const cid = Number.parseInt(id, 10);
const result = useCashoutDetails(Number.isNaN(cid) ? undefined : cid);
const info = useConversionInfo();
if (Number.isNaN(cid)) {
- return <Attention type="danger" title={i18n.str`cashout id should be a number`} />
+ return (
+ <Attention
+ type="danger"
+ title={i18n.str`cashout id should be a number`}
+ />
+ );
}
if (!result) {
- return <Loading />
+ return <Loading />;
}
if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />
+ return <ErrorLoadingWithDebug error={result} />;
}
if (result.type === "fail") {
switch (result.case) {
- case HttpStatusCode.NotFound: return <Attention type="warning" title={i18n.str`This cashout not found. Maybe already aborted.`}>
- </Attention>
- case HttpStatusCode.NotImplemented: return <Attention type="warning" title={i18n.str`Cashouts are not supported`}>
- </Attention>
- default: assertUnreachable(result)
+ case HttpStatusCode.NotFound:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`This cashout not found. Maybe already aborted.`}
+ ></Attention>
+ );
+ case HttpStatusCode.NotImplemented:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Cashouts are not supported`}
+ ></Attention>
+ );
+ default:
+ assertUnreachable(result);
}
}
if (!info) {
- return <Loading />
+ return <Loading />;
}
if (info instanceof TalerError) {
- return <ErrorLoadingWithDebug error={info} />
+ return <ErrorLoadingWithDebug error={info} />;
}
if (info.type === "fail") {
switch (info.case) {
case HttpStatusCode.NotImplemented: {
- return <Attention type="danger" title={i18n.str`Cashout not implemented`} />
+ return (
+ <Attention type="danger" title={i18n.str`Cashout not implemented`} />
+ );
}
- default: assertUnreachable(info.case)
+ default:
+ assertUnreachable(info.case);
}
}
- const { fiat_currency_specification, regional_currency_specification } = info.body
+ const { fiat_currency_specification, regional_currency_specification } =
+ info.body;
return (
<div>
<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">
-
<section class="rounded-sm px-4">
- <h2 id="summary-heading" class="font-medium text-lg"><i18n.Translate>Cashout detail</i18n.Translate></h2>
+ <h2 id="summary-heading" class="font-medium text-lg">
+ <i18n.Translate>Cashout detail</i18n.Translate>
+ </h2>
<dl class="mt-8 space-y-4">
<div class="justify-between items-center flex">
- <dt class="text-sm text-gray-600"><i18n.Translate>Subject</i18n.Translate></dt>
+ <dt class="text-sm text-gray-600">
+ <i18n.Translate>Subject</i18n.Translate>
+ </dt>
<dd class="text-sm ">{result.body.subject}</dd>
</div>
</dl>
@@ -113,48 +119,64 @@ export function ShowCashoutDetails({
<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">
<dl class="space-y-4">
-
- {result.body.creation_time.t_s !== "never" ?
+ {result.body.creation_time.t_s !== "never" ? (
<div class="justify-between items-center flex ">
- <dt class=" text-gray-600"><i18n.Translate>Created</i18n.Translate></dt>
+ <dt class=" text-gray-600">
+ <i18n.Translate>Created</i18n.Translate>
+ </dt>
<dd class="text-sm ">
- {format(result.body.creation_time.t_s * 1000, "dd/MM/yyyy HH:mm:ss", { locale: dateLocale })}
+ {format(
+ result.body.creation_time.t_s * 1000,
+ "dd/MM/yyyy HH:mm:ss",
+ { locale: dateLocale },
+ )}
</dd>
</div>
- : undefined}
+ ) : undefined}
<div class="flex justify-between items-center border-t-2 afu pt-4">
- <dt class="text-gray-600"><i18n.Translate>Debited</i18n.Translate></dt>
+ <dt class="text-gray-600">
+ <i18n.Translate>Debited</i18n.Translate>
+ </dt>
<dd class=" font-medium">
- <RenderAmount value={Amounts.parseOrThrow(result.body.amount_debit)} negative withColor spec={regional_currency_specification} />
+ <RenderAmount
+ value={Amounts.parseOrThrow(result.body.amount_debit)}
+ negative
+ withColor
+ spec={regional_currency_specification}
+ />
</dd>
</div>
<div class="flex items-center justify-between border-t-2 afu pt-4">
<dt class="flex items-center text-gray-600">
- <span><i18n.Translate>Credited</i18n.Translate></span>
-
+ <span>
+ <i18n.Translate>Credited</i18n.Translate>
+ </span>
</dt>
<dd class="text-sm ">
- <RenderAmount value={Amounts.parseOrThrow(result.body.amount_credit)} withColor spec={fiat_currency_specification} />
+ <RenderAmount
+ value={Amounts.parseOrThrow(result.body.amount_credit)}
+ withColor
+ spec={fiat_currency_specification}
+ />
</dd>
</div>
-
</dl>
</div>
</div>
</div>
-
</div>
-
</div>
<br />
<div style={{ display: "flex", justifyContent: "space-between" }}>
- <button type="button" class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
+ <a
+ href={routeClose.url({})}
+ class="text-sm font-semibold leading-6 text-gray-900"
>
- <i18n.Translate>Cancel</i18n.Translate></button>
+ <i18n.Translate>Close</i18n.Translate>
+ </a>
</div>
</div>
);
diff --git a/packages/demobank-ui/src/pages/rnd.ts b/packages/demobank-ui/src/pages/rnd.ts
index 46111425e..d04a1515d 100644
--- a/packages/demobank-ui/src/pages/rnd.ts
+++ b/packages/demobank-ui/src/pages/rnd.ts
@@ -1,5 +1,19 @@
-import { createEddsaKeyPair, encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"
+/*
+ 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 { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
const noun = [
"people",
@@ -1526,8 +1540,8 @@ const noun = [
"tomorrow",
"wake",
"wrap",
- "yesterday"
-]
+ "yesterday",
+];
const adj = [
"abandoned",
@@ -2877,17 +2891,17 @@ const adj = [
"zealous",
"zesty",
"zigzag",
-]
+];
-export function getRandomUsername(): { first: string, second: string } {
- const n = Math.floor(Math.random() * noun.length)
- const a = Math.floor(Math.random() * adj.length)
+export function getRandomUsername(): { first: string; second: string } {
+ const n = Math.floor(Math.random() * noun.length);
+ const a = Math.floor(Math.random() * adj.length);
return {
first: adj[a],
- second: noun[n]
- }
+ second: noun[n],
+ };
}
export function getRandomPassword(): string {
- return encodeCrock(getRandomBytes(16))
-} \ No newline at end of file
+ return encodeCrock(getRandomBytes(16));
+}