aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-03-08 14:09:31 -0300
committerSebastian <sebasjm@gmail.com>2024-03-08 14:09:31 -0300
commitddd32a690bd13b1eb1aef1356a1d59fd64e254bf (patch)
tree44126872f6e8195a3617e2002c696c0afa13fb0d /packages/demobank-ui/src/pages
parente0e82cdf07930d766081e42203c5a4e66d43191f (diff)
downloadwallet-core-ddd32a690bd13b1eb1aef1356a1d59fd64e254bf.tar.xz
demobank => bank
Diffstat (limited to 'packages/demobank-ui/src/pages')
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/index.ts134
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/state.ts117
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/stories.tsx29
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/test.ts31
-rw-r--r--packages/demobank-ui/src/pages/AccountPage/views.tsx144
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.stories.tsx29
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx237
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx226
-rw-r--r--packages/demobank-ui/src/pages/OperationState/index.ts157
-rw-r--r--packages/demobank-ui/src/pages/OperationState/state.ts232
-rw-r--r--packages/demobank-ui/src/pages/OperationState/stories.tsx29
-rw-r--r--packages/demobank-ui/src/pages/OperationState/test.ts31
-rw-r--r--packages/demobank-ui/src/pages/OperationState/views.tsx440
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.stories.tsx35
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx237
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.stories.tsx35
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx792
-rw-r--r--packages/demobank-ui/src/pages/ProfileNavigation.tsx202
-rw-r--r--packages/demobank-ui/src/pages/PublicHistoriesPage.tsx95
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.stories.tsx32
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx150
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx415
-rw-r--r--packages/demobank-ui/src/pages/SolveChallengePage.tsx716
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx366
-rw-r--r--packages/demobank-ui/src/pages/WireTransfer.tsx103
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx355
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx71
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx310
-rw-r--r--packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx85
-rw-r--r--packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx244
-rw-r--r--packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx301
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx901
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountList.tsx244
-rw-r--r--packages/demobank-ui/src/pages/admin/AdminHome.tsx541
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx204
-rw-r--r--packages/demobank-ui/src/pages/admin/DownloadStats.tsx585
-rw-r--r--packages/demobank-ui/src/pages/admin/RemoveAccount.tsx267
-rw-r--r--packages/demobank-ui/src/pages/index.stories.tsx20
-rw-r--r--packages/demobank-ui/src/pages/regional/ConversionConfig.tsx978
-rw-r--r--packages/demobank-ui/src/pages/regional/CreateCashout.tsx809
-rw-r--r--packages/demobank-ui/src/pages/regional/ShowCashoutDetails.tsx192
-rw-r--r--packages/demobank-ui/src/pages/rnd.ts2907
42 files changed, 0 insertions, 14028 deletions
diff --git a/packages/demobank-ui/src/pages/AccountPage/index.ts b/packages/demobank-ui/src/pages/AccountPage/index.ts
deleted file mode 100644
index 0223b12db..000000000
--- a/packages/demobank-ui/src/pages/AccountPage/index.ts
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- 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,
- 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;
- onOperationCreated: (wopid: string) => void;
- onClose: () => void;
- tab: "charge-wallet" | "wire-transfer" | undefined;
- routeClose: RouteDefinition;
- routeCashout: RouteDefinition;
- routeChargeWallet: RouteDefinition;
- routeWireTransfer: RouteDefinition<{
- account?: string;
- subject?: string;
- amount?: string;
- }>;
- routePublicAccounts: RouteDefinition;
- routeCreateWireTransfer: RouteDefinition<{
- account?: string;
- subject?: string;
- amount?: string;
- }>;
- routeOperationDetails: RouteDefinition<{ wopid: string }>;
- routeSolveSecondFactor: RouteDefinition;
-}
-
-export type State =
- | State.Loading
- | State.LoadingError
- | State.Ready
- | State.InvalidIban
- | State.UserNotFound;
-
-export namespace State {
- export interface Loading {
- status: "loading";
- error: undefined;
- }
-
- export interface LoadingError {
- status: "loading-error";
- error: TalerError;
- }
-
- export interface BaseInfo {
- error: undefined;
- }
-
- export interface Ready extends BaseInfo {
- status: "ready";
- error: undefined;
- account: string;
- tab: "charge-wallet" | "wire-transfer" | undefined;
- limit: AmountJson;
- onAuthorizationRequired: () => void;
- onOperationCreated: (wopid: string) => void;
- onClose: () => void;
- routeClose: RouteDefinition;
- routeCashout: RouteDefinition;
- routeChargeWallet: RouteDefinition;
- routePublicAccounts: RouteDefinition;
- routeWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
- }>;
- routeCreateWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
- }>;
- routeOperationDetails: RouteDefinition<{ wopid: string }>;
- routeSolveSecondFactor: RouteDefinition;
- }
-
- export interface InvalidIban {
- status: "invalid-iban";
- error: TalerCorebankApi.AccountData;
- }
-
- export interface UserNotFound {
- status: "login";
- reason: "not-found" | "forbidden";
- routeRegister?: RouteDefinition;
- }
-}
-
-export interface Transaction {
- negative: boolean;
- counterpart: string;
- when: AbsoluteTime;
- amount: AmountJson | undefined;
- subject: string;
-}
-
-const viewMapping: utils.StateViewMap<State> = {
- loading: Loading,
- login: LoginForm,
- "invalid-iban": InvalidIbanView,
- "loading-error": ErrorLoadingWithDebug,
- ready: ReadyView,
-};
-
-export const AccountPage = utils.compose(
- (p: Props) => useComponentState(p),
- viewMapping,
-);
diff --git a/packages/demobank-ui/src/pages/AccountPage/state.ts b/packages/demobank-ui/src/pages/AccountPage/state.ts
deleted file mode 100644
index e84fef025..000000000
--- a/packages/demobank-ui/src/pages/AccountPage/state.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- 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,
- parsePaytoUri,
-} from "@gnu-taler/taler-util";
-import { useAccountDetails } from "../../hooks/account.js";
-import { Props, State } from "./index.js";
-
-export function useComponentState({
- account,
- tab,
- routeChargeWallet,
- routeCreateWireTransfer,
- routePublicAccounts,
- routeSolveSecondFactor,
- routeOperationDetails,
- routeWireTransfer,
- routeCashout,
- onOperationCreated,
- onClose,
- routeClose,
- onAuthorizationRequired,
-}: Props): State {
- const result = useAccountDetails(account);
-
- if (!result) {
- return {
- status: "loading",
- error: undefined,
- };
- }
-
- if (result instanceof TalerError) {
- return {
- status: "loading-error",
- error: result,
- };
- }
-
- if (result.type === "fail") {
- switch (result.case) {
- case HttpStatusCode.Unauthorized:
- return {
- status: "login",
- reason: "forbidden",
- };
- case HttpStatusCode.NotFound:
- return {
- status: "login",
- reason: "not-found",
- };
- default: {
- assertUnreachable(result);
- }
- }
- }
-
- const { body: data } = result;
-
- const balance = Amounts.parseOrThrow(data.balance.amount);
-
- 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")
- ) {
- return {
- status: "invalid-iban",
- error: data,
- };
- }
-
- const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
- const limit = balanceIsDebit
- ? Amounts.sub(debitThreshold, balance).amount
- : Amounts.add(balance, debitThreshold).amount;
-
- return {
- status: "ready",
- onOperationCreated,
- error: undefined,
- tab,
- routeCashout,
- routeOperationDetails,
- routeCreateWireTransfer,
- routePublicAccounts,
- routeSolveSecondFactor,
- onAuthorizationRequired,
- onClose,
- routeClose,
- routeChargeWallet,
- routeWireTransfer,
- account,
- limit,
- };
-}
diff --git a/packages/demobank-ui/src/pages/AccountPage/stories.tsx b/packages/demobank-ui/src/pages/AccountPage/stories.tsx
deleted file mode 100644
index fe09a4f89..000000000
--- a/packages/demobank-ui/src/pages/AccountPage/stories.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- 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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import { ReadyView } from "./views.js";
-
-export default {
- title: "account page",
-};
-
-export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/demobank-ui/src/pages/AccountPage/test.ts b/packages/demobank-ui/src/pages/AccountPage/test.ts
deleted file mode 100644
index 14c8be948..000000000
--- a/packages/demobank-ui/src/pages/AccountPage/test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- 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/>
- */
-
-/**
- *
- * @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";
-
-describe("Account states", () => {
- 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
deleted file mode 100644
index 7165c28b6..000000000
--- a/packages/demobank-ui/src/pages/AccountPage/views.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- 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 { TranslatedString } from "@gnu-taler/taler-util";
-import { Attention, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { Transactions } from "../../components/Transactions/index.js";
-import { useBankState } from "../../hooks/bank-state.js";
-import { usePreferences } from "../../hooks/preferences.js";
-import { PaymentOptions } from "../PaymentOptions.js";
-import { State } from "./index.js";
-import { RouteDefinition } from "../../route.js";
-
-export function InvalidIbanView({ error }: State.InvalidIban) {
- return (
- <div>Payto from server is not valid &quot;{error.payto_uri}&quot;</div>
- );
-}
-
-const IS_PUBLIC_ACCOUNT_ENABLED = false;
-
-function ShowDemoInfo({ routePublicAccounts }: {
- routePublicAccounts: RouteDefinition;
-}): 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 name="public account" href={routePublicAccounts.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({ routeSolveSecondFactor }: {
- routeSolveSecondFactor: RouteDefinition;
-}): VNode {
- const { i18n } = useTranslationContext();
- const [bankState, updateBankState] = useBankState();
- 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`;
- }
- })(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"
- name="complete operation"
- href={routeSolveSecondFactor.url({})}
- >
- <i18n.Translate>this page</i18n.Translate>
- </a>
- </Attention>
- );
-}
-
-export function ReadyView({
- tab,
- account,
- routeChargeWallet,
- routeWireTransfer,
- limit,
- routeCashout,
- routeCreateWireTransfer,
- routePublicAccounts,
- routeOperationDetails,
- routeSolveSecondFactor,
- onClose,
- routeClose,
- onOperationCreated,
- onAuthorizationRequired,
-}: State.Ready): VNode {
- return (
- <Fragment>
- <ShowPedingOperation routeSolveSecondFactor={routeSolveSecondFactor} />
- <ShowDemoInfo routePublicAccounts={routePublicAccounts} />
- <PaymentOptions
- tab={tab}
- routeOperationDetails={routeOperationDetails}
- routeCashout={routeCashout}
- routeChargeWallet={routeChargeWallet}
- routeWireTransfer={routeWireTransfer}
- limit={limit}
- routeClose={routeClose}
- onClose={onClose}
- onOperationCreated={onOperationCreated}
- onAuthorizationRequired={onAuthorizationRequired}
- />
- <Transactions account={account} routeCreateWireTransfer={routeCreateWireTransfer} />
- </Fragment>
- );
-}
diff --git a/packages/demobank-ui/src/pages/BankFrame.stories.tsx b/packages/demobank-ui/src/pages/BankFrame.stories.tsx
deleted file mode 100644
index c874ac4ca..000000000
--- a/packages/demobank-ui/src/pages/BankFrame.stories.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- 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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import { BankFrame } from "./BankFrame.js";
-
-export default {
- title: "bank frame",
-};
-
-export const Ready = tests.createExample(BankFrame, {});
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
deleted file mode 100644
index 427e9a156..000000000
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- 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, TalerError, TranslatedString } from "@gnu-taler/taler-util";
-import {
- Footer,
- Header,
- Loading,
- ToastBanner,
- notifyError,
- notifyException,
- useTranslationContext
-} from "@gnu-taler/web-util/browser";
-import { ComponentChildren, VNode, h } from "preact";
-import { useEffect, useErrorBoundary } from "preact/hooks";
-import { useBankCoreApiContext } from "../context/config.js";
-import { useSettingsContext } from "../context/settings.js";
-import { useAccountDetails } from "../hooks/account.js";
-import { useSessionState } from "../hooks/session.js";
-import { useBankState } from "../hooks/bank-state.js";
-import {
- getAllBooleanPreferences,
- getLabelForPreferences,
- usePreferences,
-} from "../hooks/preferences.js";
-import { RouteDefinition } from "../route.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,
- routeAccountDetails,
-}: {
- account?: string;
- routeAccountDetails?: RouteDefinition;
- children: ComponentChildren;
-}): VNode {
- const { i18n } = useTranslationContext();
- const session = useSessionState();
- const settings = useSettingsContext();
- const [preferences, updatePreferences] = usePreferences();
- const [, , resetBankState] = useBankState();
-
- const [error, resetError] = useErrorBoundary();
-
- useEffect(() => {
- if (error) {
- if (error instanceof 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,
- );
- }
- 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 ?? "#"}
- profileURL={routeAccountDetails?.url({})}
- onLogout={
- session.state.status !== "loggedIn"
- ? undefined
- : () => {
- session.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"
- name={`${set} switch`}
- 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>
-
- <div class="fixed z-20 w-full">
- <div class="mx-auto w-4/5">
- <ToastBanner />
- </div>
- </div>
-
- <main class="-mt-32 flex-1">
- {account && routeAccountDetails && (
- <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} routeAccountDetails={routeAccountDetails} />
- </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, routeAccountDetails }: {
- account: string,
- routeAccountDetails: RouteDefinition;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useAccountDetails(account);
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- return <div />;
- }
- if (result.type === "fail") {
- return (
- <a name="account details"
- href={routeAccountDetails.url({})}
- class="underline underline-offset-2"
- >
- <i18n.Translate>Welcome</i18n.Translate>
- </a>
- );
- }
- return (
- <a name="account details"
- href={routeAccountDetails.url({})}
- class="underline underline-offset-2"
- >
- <i18n.Translate>
- Welcome, <span class="whitespace-nowrap">{result.body.name}</span>
- </i18n.Translate>
- </a>
- );
-}
-
-function AccountBalance({ account }: { account: string }): VNode {
- const result = useAccountDetails(account);
- const { config } = useBankCoreApiContext();
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- 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}
- />
- );
-}
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
deleted file mode 100644
index bd20e79c8..000000000
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- 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
-} from "@gnu-taler/taler-util";
-import {
- Button,
- LocalNotificationBanner,
- ShowInputErrorLabel,
- useLocalNotificationHandler,
- 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 { useSessionState } from "../hooks/session.js";
-import { RouteDefinition } from "../route.js";
-import { undefinedIfEmpty } from "../utils.js";
-import { doAutoFocus } from "./PaytoWireTransferForm.js";
-
-/**
- * Collect and submit login data.
- */
-export function LoginForm({
- currentUser,
- fixedUser,
- routeRegister,
-}: {
- fixedUser?: boolean;
- currentUser?: string;
- routeRegister?: RouteDefinition;
-}): VNode {
- const session = useSessionState();
-
- const sessionUser =
- session.state.status !== "loggedOut" ? session.state.username : undefined;
- const [username, setUsername] = useState<string | undefined>(
- currentUser ?? sessionUser,
- );
- const [password, setPassword] = useState<string | undefined>();
- const { i18n } = useTranslationContext();
- const { authenticator } = useBankCoreApiContext();
- const [notification, withErrorHandler] = useLocalNotificationHandler();
- const { config } = useBankCoreApiContext();
-
- const ref = useRef<HTMLInputElement>(null);
- useEffect(function focusInput() {
- ref.current?.focus();
- }, []);
-
- 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,
- });
-
- async function doLogout() {
- session.logOut();
- }
-
- const loginHandler = !username || !password ? undefined : withErrorHandler(
- async () => authenticator(username)
- .createAccessToken(password, {
- // scope: "readwrite" as "write", // FIX: different than merchant
- scope: "readwrite",
- duration: { d_us: "forever" },
- refreshable: true,
- }),
- (result) => {
- session.logIn({ username, token: result.body.access_token })
- },
- (fail) => {
- switch (fail.case) {
- case HttpStatusCode.Unauthorized: return i18n.str`Wrong credentials for "${username}"`;
- case HttpStatusCode.NotFound: return i18n.str`Account not found`;
- }
- }
- )
-
- 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
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <div>
- <label
- for="username"
- class="block text-sm font-medium leading-6 text-gray-900"
- >
- <i18n.Translate>Username</i18n.Translate>
- </label>
- <div class="mt-2">
- <input
- ref={doAutoFocus}
- type="text"
- name="username"
- id="username"
- 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"
- value={username ?? ""}
- disabled={fixedUser}
- enterkeyhint="next"
- placeholder="identification"
- autocomplete="username"
- title={i18n.str`Username of the account`}
- required
- onInput={(e): void => {
- setUsername(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={username !== undefined}
- />
- </div>
- </div>
-
- <div>
- <div class="flex items-center justify-between">
- <label
- for="password"
- class="block text-sm font-medium leading-6 text-gray-900"
- >
- <i18n.Translate>Password</i18n.Translate>
- </label>
- </div>
- <div class="mt-2">
- <input
- type="password"
- name="password"
- id="password"
- autocomplete="current-password"
- 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"
- enterkeyhint="send"
- value={password ?? ""}
- placeholder="Password"
- title={i18n.str`Password of the account`}
- required
- onInput={(e): void => {
- setPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- </div>
- </div>
-
- {session.state.status !== "loggedOut" ? (
- <div class="flex justify-between">
- <button
- type="submit"
- name="cancel"
- 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"
- name="check"
- 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}
- handler={loginHandler}
- >
- <i18n.Translate>Check</i18n.Translate>
- </Button>
- </div>
- ) : (
- <div>
- <Button
- type="submit"
- name="login"
- 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}
- handler={loginHandler}
- >
- <i18n.Translate>Log in</i18n.Translate>
- </Button>
- </div>
- )}
- </form>
-
- {config.allow_registrations && routeRegister && (
- <a
- name="register"
- href={routeRegister.url({})}
- class="flex justify-center border-t 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"
- >
- <i18n.Translate>Register</i18n.Translate>
- </a>
- )}
- </div>
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/pages/OperationState/index.ts b/packages/demobank-ui/src/pages/OperationState/index.ts
deleted file mode 100644
index e4d9d45e3..000000000
--- a/packages/demobank-ui/src/pages/OperationState/index.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- 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,
- 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 { RouteDefinition } from "../../route.js";
-
-export interface Props {
- currency: string;
- onAuthorizationRequired: () => void;
- routeClose: RouteDefinition;
- onAbort: () => void;
- routeHere: RouteDefinition<{ wopid: string }>;
-}
-
-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 {
- status: "loading";
- error: undefined;
- }
-
- export interface Failed {
- status: "failed";
- error: TalerCoreBankErrorsByMethod<"createWithdrawal">;
- }
-
- export interface LoadingError {
- status: "loading-error";
- error: TalerError;
- }
-
- /**
- * Need to open the wallet
- */
- export interface Ready {
- status: "ready";
- error: undefined;
- uri: WithdrawUriResult;
- onAbort: () => Promise<
- TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
- >;
- routeClose: RouteDefinition;
- }
-
- export interface InvalidPayto {
- status: "invalid-payto";
- error: undefined;
- payto: string | undefined;
- }
- export interface InvalidWithdrawal {
- status: "invalid-withdrawal";
- error: undefined;
- uri: string;
- }
- export interface InvalidReserve {
- status: "invalid-reserve";
- error: undefined;
- reserve: string | undefined;
- }
- export interface NeedConfirmation {
- status: "need-confirmation";
- onAuthorizationRequired: () => void;
- account: string;
- routeHere: RouteDefinition<{ wopid: string }>;
- onAbort:
- | undefined
- | (() => Promise<
- TalerCoreBankErrorsByMethod<"abortWithdrawalById"> | undefined
- >);
- onConfirm:
- | undefined
- | (() => Promise<
- TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
- >);
- error: undefined;
- id: string;
- }
- export interface Aborted {
- status: "aborted";
- error: undefined;
- routeClose: RouteDefinition;
- }
- export interface Confirmed {
- status: "confirmed";
- error: undefined;
- routeClose: RouteDefinition;
- }
-}
-
-export interface Transaction {
- negative: boolean;
- counterpart: string;
- when: AbsoluteTime;
- amount: AmountJson | undefined;
- subject: string;
-}
-
-const viewMapping: utils.StateViewMap<State> = {
- loading: Loading,
- failed: FailedView,
- "invalid-payto": InvalidPaytoView,
- "invalid-withdrawal": InvalidWithdrawalView,
- "invalid-reserve": InvalidReserveView,
- "need-confirmation": NeedConfirmationView,
- aborted: AbortedView,
- confirmed: ConfirmedView,
- "loading-error": ErrorLoadingWithDebug,
- ready: ReadyView,
-};
-
-export const OperationState = utils.compose(
- (p: Props) => useComponentState(p),
- viewMapping,
-);
diff --git a/packages/demobank-ui/src/pages/OperationState/state.ts b/packages/demobank-ui/src/pages/OperationState/state.ts
deleted file mode 100644
index 5baf2d51c..000000000
--- a/packages/demobank-ui/src/pages/OperationState/state.ts
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- 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,
- 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";
-import { useBankCoreApiContext } from "../../context/config.js";
-import { useWithdrawalDetails } from "../../hooks/account.js";
-import { useSessionState } from "../../hooks/session.js";
-import { useBankState } from "../../hooks/bank-state.js";
-import { usePreferences } from "../../hooks/preferences.js";
-import { Props, State } from "./index.js";
-
-export function useComponentState({
- currency,
- routeClose,
- onAbort,
- routeHere,
- onAuthorizationRequired,
-}: Props): utils.RecursiveState<State> {
- const [settings] = usePreferences();
- const [bankState, updateBankState] = useBankState();
- const { state: credentials } = useSessionState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials;
- const { bank } = useBankCoreApiContext();
-
- 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}`);
- if (!creds) return;
- const resp = await bank.createWithdrawal(creds, {
- amount: Amounts.stringify(parsedAmount),
- });
- if (resp.type === "fail") {
- setFailure(resp);
- return;
- }
- updateBankState("currentWithdrawalOperationId", resp.body.withdrawal_id);
- }
-
- const withdrawalOperationId = bankState.currentWithdrawalOperationId;
- useEffect(() => {
- if (withdrawalOperationId === undefined) {
- doSilentStart();
- }
- }, [settings.fastWithdrawal, amount]);
-
- if (failure) {
- return {
- status: "failed",
- error: failure,
- };
- }
-
- if (!withdrawalOperationId) {
- return {
- status: "loading",
- error: undefined,
- };
- }
-
- const wid = withdrawalOperationId;
-
- async function doAbort() {
- if (!creds) return;
- const resp = await bank.abortWithdrawalById(creds, wid);
- if (resp.type === "ok") {
- // updateBankState("currentWithdrawalOperationId", undefined)
- onAbort();
- } else {
- return resp;
- }
- }
-
- async function doConfirm(): Promise<
- TalerCoreBankErrorsByMethod<"confirmWithdrawalById"> | undefined
- > {
- if (!creds) return;
- const resp = await bank.confirmWithdrawalById(creds, wid);
- if (resp.type === "ok") {
- mutate(() => true); //clean withdrawal state
- } else {
- return resp;
- }
- }
-
- const uri = stringifyWithdrawUri({
- bankIntegrationApiBaseUrl: bank.getIntegrationAPI(),
- withdrawalOperationId,
- });
- const parsedUri = parseWithdrawUri(uri);
- if (!parsedUri) {
- return {
- status: "invalid-withdrawal",
- error: undefined,
- uri,
- };
- }
-
- return (): utils.RecursiveState<State> => {
- const result = useWithdrawalDetails(withdrawalOperationId);
- const shouldCreateNewOperation = result && !(result instanceof TalerError);
-
- useEffect(() => {
- if (shouldCreateNewOperation) {
- doSilentStart();
- }
- }, []);
- if (!result) {
- return {
- status: "loading",
- error: undefined,
- };
- }
- if (result instanceof TalerError) {
- return {
- status: "loading-error",
- error: result,
- };
- }
-
- if (result.type === "fail") {
- switch (result.case) {
- case HttpStatusCode.BadRequest:
- case HttpStatusCode.NotFound: {
- return {
- status: "aborted",
- error: undefined,
- routeClose,
- };
- }
- default:
- assertUnreachable(result);
- }
- }
-
- const { body: data } = result;
- if (data.status === "aborted") {
- return {
- status: "aborted",
- error: undefined,
- routeClose,
- };
- }
-
- if (data.status === "confirmed") {
- if (!settings.showWithdrawalSuccess) {
- updateBankState("currentWithdrawalOperationId", undefined);
- // onClose()
- }
- return {
- status: "confirmed",
- error: undefined,
- routeClose,
- };
- }
-
- if (data.status === "pending") {
- return {
- status: "ready",
- error: undefined,
- uri: parsedUri,
- routeClose,
- onAbort: !creds
- ? async () => {
- onAbort();
- return undefined;
- }
- : doAbort,
- };
- }
-
- if (!data.selected_reserve_pub) {
- return {
- status: "invalid-reserve",
- error: undefined,
- reserve: data.selected_reserve_pub,
- };
- }
-
- 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,
- };
- }
-
- return {
- status: "need-confirmation",
- error: undefined,
- routeHere,
- onAuthorizationRequired,
- account: data.username,
- id: withdrawalOperationId,
- onAbort: !creds ? undefined : doAbort,
- onConfirm: !creds ? undefined : doConfirm,
- };
- };
-}
diff --git a/packages/demobank-ui/src/pages/OperationState/stories.tsx b/packages/demobank-ui/src/pages/OperationState/stories.tsx
deleted file mode 100644
index 82253b82c..000000000
--- a/packages/demobank-ui/src/pages/OperationState/stories.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- 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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import { ReadyView } from "./views.js";
-
-export default {
- title: "operation status page",
-};
-
-export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/demobank-ui/src/pages/OperationState/test.ts b/packages/demobank-ui/src/pages/OperationState/test.ts
deleted file mode 100644
index d47cb64a2..000000000
--- a/packages/demobank-ui/src/pages/OperationState/test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- 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/>
- */
-
-/**
- *
- * @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";
-
-describe("Withdrawal operation states", () => {
- 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
deleted file mode 100644
index 6eee6daa9..000000000
--- a/packages/demobank-ui/src/pages/OperationState/views.tsx
+++ /dev/null
@@ -1,440 +0,0 @@
-/*
- 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,
- 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 { State } from "./index.js";
-import { useTalerWalletIntegrationAPI } from "../../context/wallet-integration.js";
-
-export function InvalidPaytoView({ payto }: State.InvalidPayto) {
- return <div>Payto from server is not valid &quot;{payto}&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 }: State.InvalidReserve) {
- return <div>Reserve from server is not valid &quot;{reserve}&quot;</div>;
-}
-
-export function NeedConfirmationView({
- onAbort: doAbort,
- onConfirm: doConfirm,
- routeHere,
- 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();
- 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);
- }
- });
- }
-
- async function onConfirm() {
- errorHandler(async () => {
- if (!doConfirm) return;
- const resp = await doConfirm();
- if (!resp) {
- if (!settings.showWithdrawalSuccess) {
- notifyInfo(i18n.str`Wire transfer completed!`);
- }
- 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 HttpStatusCode.Accepted: {
- updateBankState("currentChallenge", {
- operation: "confirm-withdrawal",
- id: String(resp.body.challenge_id),
- sent: AbsoluteTime.never(),
- location: routeHere.url({ wopid: id }),
- request: id,
-
- });
- return onAuthorizationRequired();
- }
- default:
- assertUnreachable(resp);
- }
- });
- }
-
- return (
- <div class="bg-white shadow sm:rounded-lg">
- <LocalNotificationBanner notification={notification} />
- <div class="px-4 py-5 sm:p-6">
- <h3 class="text-base font-semibold text-gray-900">
- <i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
- </h3>
- <div class="mt-3 text-sm leading-6">
- <ShouldBeSameUser username={account}>
- <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();
- }}
- >
- <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"
- name="cancel"
- class="text-sm font-semibold leading-6 text-gray-900"
- onClick={(e) => {
- e.preventDefault();
- onCancel();
- }}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- <button
- type="submit"
- name="transfer"
- 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();
- }}
- >
- <i18n.Translate>Transfer</i18n.Translate>
- </button>
- </div>
- </form>
- </ShouldBeSameUser>
- </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);
- }
-}
-
-export function AbortedView() {
- return <div>aborted</div>;
-}
-
-export function ConfirmedView({ routeClose }: State.Confirmed) {
- const { i18n } = useTranslationContext();
- 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>
- </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-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"
- >
- <i18n.Translate>Do not show this again</i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- name="toggle withdrawal"
- 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>
- </button>
- </div>
- </div>
- <div class="mt-5 sm:mt-6">
- <a
- href={routeClose.url({})}
- type="button"
- name="close"
- 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>
- );
-}
-
-export function ReadyView({
- uri,
- onAbort: doAbort,
-}: State.Ready): VNode {
- const { i18n } = useTranslationContext();
- const walletInegrationApi = useTalerWalletIntegrationAPI();
- const [notification, notify, errorHandler] = useLocalNotification();
-
- const talerWithdrawUri = stringifyWithdrawUri(uri);
- useEffect(() => {
- walletInegrationApi.publishTalerAction(uri);
- }, []);
-
- async function onAbort() {
- errorHandler(async () => {
- 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);
- }
- });
- }
-
- return (
- <Fragment>
- <LocalNotificationBanner notification={notification} />
-
- <div class="flex justify-end mt-4">
- <button
- type="button"
- name="cancel"
- 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 can also
- </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}
- name="start"
- 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 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>
- </Fragment>
- );
-}
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.stories.tsx b/packages/demobank-ui/src/pages/PaymentOptions.stories.tsx
deleted file mode 100644
index 78af886a8..000000000
--- a/packages/demobank-ui/src/pages/PaymentOptions.stories.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- 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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import { PaymentOptions } from "./PaymentOptions.js";
-
-export default {
- title: "PaymentOptions",
-};
-
-export const USD = tests.createExample(PaymentOptions, {
- limit: {
- currency: "USD",
- fraction: 0,
- value: 1,
- },
-});
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
deleted file mode 100644
index 48ecc7525..000000000
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- 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 { AmountJson, TalerError } from "@gnu-taler/taler-util";
-import { Fragment, VNode, h } from "preact";
-import { useBankState } from "../hooks/bank-state.js";
-import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
-import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
-import { EmptyObject, RouteDefinition } from "../route.js";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { useWithdrawalDetails } from "../hooks/account.js";
-import { useEffect } from "preact/hooks";
-import { useSessionState } from "../hooks/session.js";
-
-function ShowOperationPendingTag({
- woid,
- onOperationAlreadyCompleted,
-}: {
- woid: string;
- onOperationAlreadyCompleted?: () => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { state: credentials } = useSessionState();
- const result = useWithdrawalDetails(woid);
- const loading = !result
- const error =
- !loading && (result instanceof TalerError || result.type === "fail");
- const pending =
- !loading && !error &&
- (result.body.status === "pending" || result.body.status === "selected")
- && credentials.status === "loggedIn"
- && credentials.username === result.body.username;
- useEffect(() => {
- if (!loading && !pending && onOperationAlreadyCompleted) {
- onOperationAlreadyCompleted();
- }
- }, [pending]);
-
- if (error || !pending) {
- return <Fragment />;
- }
-
- return (
- <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>
- );
-}
-
-/**
- * Let the user choose a payment option,
- * then specify the details trigger the action.
- */
-export function PaymentOptions({
- routeClose,
- routeCashout,
- routeChargeWallet,
- routeWireTransfer,
- tab,
- limit,
- onOperationCreated,
- onClose,
- routeOperationDetails,
- onAuthorizationRequired,
-}: {
- limit: AmountJson;
- tab: "charge-wallet" | "wire-transfer" | undefined;
- onAuthorizationRequired: () => void;
- onOperationCreated: (wopid: string) => void;
- onClose: () => void;
-
- routeOperationDetails: RouteDefinition<{ wopid: string }>;
- routeClose: RouteDefinition;
- routeCashout: RouteDefinition;
- routeChargeWallet: RouteDefinition;
- routeWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
- }>;
-}): VNode {
- const { i18n } = useTranslationContext();
- const [bankState, updateBankState] = useBankState();
-
- 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>
- </legend>
-
- <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" --> */}
- <a name="charge wallet" 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 Taler 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 && (
- <ShowOperationPendingTag
- woid={bankState.currentWithdrawalOperationId}
- onOperationAlreadyCompleted={() => {
- updateBankState(
- "currentWithdrawalOperationId",
- undefined,
- );
- }}
- />
- )}
- </div>
- </label>
- </a>
-
- <a name="wire transfer" 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>
- <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>
- </label>
- </a>
- </div>
- {tab === "charge-wallet" && (
- <WalletWithdrawForm
- routeOperationDetails={routeOperationDetails}
- focus
- limit={limit}
- onAuthorizationRequired={onAuthorizationRequired}
- onOperationCreated={onOperationCreated}
- onOperationAborted={onClose}
- routeCancel={routeClose}
- />
- )}
- {tab === "wire-transfer" && (
- <PaytoWireTransferForm
- focus
- title={i18n.str`Transfer details`}
- routeHere={routeWireTransfer}
- limit={limit}
- onAuthorizationRequired={onAuthorizationRequired}
- onSuccess={onClose}
- routeCashout={routeCashout}
- routeCancel={routeClose}
- />
- )}
- </fieldset>
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.stories.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.stories.tsx
deleted file mode 100644
index 61cfb5629..000000000
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.stories.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- 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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
-
-export default {
- title: "PaytoWireTransferForm",
-};
-
-export const USD = tests.createExample(PaytoWireTransferForm, {
- limit: {
- currency: "USD",
- fraction: 0,
- value: 1,
- },
-});
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
deleted file mode 100644
index 791a3b440..000000000
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ /dev/null
@@ -1,792 +0,0 @@
-/*
- 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,
- AmountJson,
- AmountString,
- Amounts,
- CurrencySpecification,
- FRAC_SEPARATOR,
- HttpStatusCode,
- PaytoString,
- PaytoUri,
- TalerErrorCode,
- TranslatedString,
- assertUnreachable,
- buildPayto,
- parsePaytoUri,
- stringifyPaytoUri
-} from "@gnu-taler/taler-util";
-import {
- InternationalizationAPI,
- LocalNotificationBanner,
- ShowInputErrorLabel,
- notifyInfo,
- useLocalNotification,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, Ref, VNode, h } from "preact";
-import { useState } from "preact/hooks";
-import { mutate } from "swr";
-import { useBankCoreApiContext } from "../context/config.js";
-import { useSessionState } from "../hooks/session.js";
-import { useBankState } from "../hooks/bank-state.js";
-import { EmptyObject, RouteDefinition } from "../route.js";
-import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js";
-
-interface Props {
- title: TranslatedString;
- focus?: boolean;
- withAccount?: string;
- withSubject?: string;
- withAmount?: string;
- onSuccess: () => void;
- onAuthorizationRequired: () => void;
- routeCancel?: RouteDefinition;
- routeCashout?: RouteDefinition;
- routeHere: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
- }>;
- limit: AmountJson;
-}
-
-export function PaytoWireTransferForm({
- focus,
- title,
- withAccount,
- withSubject,
- withAmount,
- onSuccess,
- routeCancel,
- routeCashout,
- routeHere,
- onAuthorizationRequired,
- limit,
-}: Props): VNode {
- const [isRawPayto, setIsRawPayto] = useState(false);
- const { state: credentials } = useSessionState();
- const { bank: api, config, url } = useBankCoreApiContext();
-
- const sendingToFixedAccount = withAccount !== undefined;
-
- const [account, setAccount] = useState<string | undefined>(withAccount);
- const [subject, setSubject] = useState<string | undefined>(withSubject);
- const [amount, setAmount] = useState<string | undefined>(withAmount);
- const [, updateBankState] = useBankState();
-
- const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
- undefined,
- );
- const { i18n } = useTranslationContext();
-
- const trimmedAmountStr = amount?.trim();
- const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`);
- const [notification, notify, handleError] = useLocalNotification();
-
- const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as const : "iban" as const;
-
- const errorsWire = undefinedIfEmpty({
- account: !account
- ? i18n.str`Required`
- : paytoType === "iban" ? validateIBAN(account, i18n) :
- paytoType === "x-taler-bank" ? validateTalerBank(account, i18n) :
- undefined,
- subject: !subject ? i18n.str`Required` : validateSubject(subject, i18n),
- amount: !trimmedAmountStr
- ? i18n.str`Required`
- : !parsedAmount
- ? i18n.str`Not valid`
- : validateAmount(parsedAmount, limit, i18n),
- });
-
- const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
-
-
- const errorsPayto = undefinedIfEmpty({
- rawPaytoInput: !rawPaytoInput
- ? i18n.str`Required`
- : !parsed ? i18n.str`Does not follow the pattern`
- : validateRawPayto(parsed, limit, url.host, i18n, paytoType),
- });
-
- async function doSend() {
- let payto_uri: PaytoString | undefined;
- let sendingAmount: AmountString | undefined;
-
- if (credentials.status !== "loggedIn") return;
- if (isRawPayto) {
- const p = parsePaytoUri(rawPaytoInput!);
- if (!p) return;
- sendingAmount = p.params.amount as AmountString;
- delete p.params.amount;
- // if this payto is valid then it already have message
- payto_uri = stringifyPaytoUri(p);
- } else {
- if (!account || !subject) return;
- let payto;
- switch (paytoType) {
- case "x-taler-bank": {
- payto = buildPayto("x-taler-bank", url.host, account);
- break;
- }
- case "iban": {
- payto = buildPayto("iban", account, undefined);
- break;
- }
- default: assertUnreachable(paytoType)
- }
-
- payto.params.message = encodeURIComponent(subject);
- payto_uri = stringifyPaytoUri(payto);
- sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString;
- }
- const puri = payto_uri;
- const sAmount = sendingAmount;
-
- await handleError(async () => {
- const request = {
- payto_uri: puri,
- amount: sAmount,
- };
- const resp = await api.createTransaction(credentials, request);
- 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.Accepted: {
- updateBankState("currentChallenge", {
- operation: "create-transaction",
- id: String(resp.body.challenge_id),
- location: routeHere.url({ account: account ?? "", amount, subject }),
- sent: AbsoluteTime.never(),
- request,
- });
- return onAuthorizationRequired();
- }
- default:
- assertUnreachable(resp);
- }
- }
- notifyInfo(i18n.str`Wire transfer created!`);
- onSuccess();
- setAmount(undefined);
- setAccount(undefined);
- setSubject(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 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) {
- switch (parsed.targetType) {
- case "iban": {
- setAccount(parsed.iban);
- break;
- }
- case "x-taler-bank": {
- setAccount(parsed.account);
- break;
- }
- case "bitcoin": {
- break;
- }
- default: {
- assertUnreachable(parsed)
- }
- }
- const amountStr = !parsed.params ? undefined : parsed.params["amount"];
- if (amountStr) {
- const amount = Amounts.parse(amountStr);
- if (amount) {
- setAmount(Amounts.stringifyValue(amount));
- }
- }
- const subject = parsed.params["message"];
- if (subject) {
- setSubject(subject);
- }
- }
- 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 (account) {
- let payto;
- switch (paytoType) {
- case "x-taler-bank": {
- payto = buildPayto("x-taler-bank", url.host, account);
- if (parsedAmount) {
- payto.params["amount"] =
- Amounts.stringify(parsedAmount);
- }
- if (subject) {
- payto.params["message"] = subject;
- }
- break;
- }
- case "iban": {
- payto = buildPayto("iban", account, undefined);
- if (parsedAmount) {
- payto.params["amount"] =
- Amounts.stringify(parsedAmount);
- }
- if (subject) {
- payto.params["message"] = subject;
- }
- break;
- }
- default: assertUnreachable(paytoType)
- }
- rawPaytoInputSetter(stringifyPaytoUri(payto));
- }
- setIsRawPayto(true);
- }}
- />
- <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>
- )}
- {routeCashout ? (
- <a
- name="do cashout"
- href={routeCashout.url({})}
- class="bg-white p-4 rounded-lg text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cashout</i18n.Translate>
- </a>
- ) : (
- undefined
- )}
- </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">
- {(() => {
- switch (paytoType) {
- case "x-taler-bank": {
- return <TextField
- id="x-taler-bank"
- label={i18n.str`Recipient`}
- help={i18n.str`Id of the recipient's account`}
- error={errorsWire?.account}
- onChange={setAccount}
- value={account}
- placeholder={i18n.str`username`}
- focus={focus}
- disabled={sendingToFixedAccount}
- />
- }
- case "iban": {
- return <TextField
- id="iban"
- label={i18n.str`Recipient`}
- help={i18n.str`IBAN of the recipient's account`}
- placeholder={"CC0123456789" as TranslatedString}
- error={errorsWire?.account}
- onChange={(v) => setAccount(v.toUpperCase())}
- value={account}
- focus={focus}
- disabled={sendingToFixedAccount}
- />
- }
- default: assertUnreachable(paytoType)
- }
- })()}
-
- <div class="sm:col-span-5">
- <label
- 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 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>
- ) : (
- <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={((): TranslatedString => {
- switch (paytoType) {
- case "x-taler-bank": return i18n.str`payto://x-taler-bank/[bank-host]/[receiver-account]?message=[subject]&amount=[${limit.currency}:X.Y]`
- case "iban": return i18n.str`payto://iban/[receiver-iban]?message=[subject]&amount=[${limit.currency}:X.Y]`
- }
- })()}
- onInput={(e): void => {
- rawPaytoInputSetter(e.currentTarget.value);
- }}
- />
- <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
- name="cancel"
- href={routeCancel.url({})}
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- ) : (
- <div />
- )}
- <button
- type="submit"
- name="send"
- 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>
- );
-}
-
-/**
- * Show the element when the load ended
- * @param element
- */
-export function doAutoFocus(element: HTMLElement | null) {
- if (element) {
- setTimeout(() => {
- element.focus({ preventScroll: true });
- element.scrollIntoView({
- behavior: "smooth",
- block: "center",
- inline: "center",
- });
- }, 100);
- }
-}
-
-export function InputAmount(
- {
- currency,
- name,
- value,
- error,
- left,
- onChange,
- }: {
- error?: string;
- currency: string;
- name: string;
- left?: boolean | undefined;
- value: string | undefined;
- onChange?: (s: string) => void;
- },
- ref: Ref<HTMLInputElement>,
-): VNode {
- 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">
- <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"
- ref={ref}
- name={name}
- id={name}
- autocomplete="off"
- value={value ?? ""}
- 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,
- );
- }
- onChange(e.currentTarget.value);
- }}
- />
- </div>
- <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
- </div>
- );
-}
-
-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,
- );
-
- 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>
- );
-}
-
-
-function validateRawPayto(parsed: PaytoUri, limit: AmountJson, host: string, i18n: InternationalizationAPI, type: "iban" | "x-taler-bank"): TranslatedString | undefined {
- if (!parsed.isKnown) {
- return i18n.str`The target type is unknown, use "${type}"`
- }
- let result: TranslatedString | undefined;
- switch (type) {
- case "x-taler-bank": {
- if (parsed.targetType !== "x-taler-bank") {
- return i18n.str`Only "x-taler-bank" target are supported`
- }
-
- if (parsed.host !== host) {
- return i18n.str`Only this host is allowed. Use "${host}"`
- }
-
- if (!parsed.account) {
- return i18n.str`Missing account name`
- }
- const result = validateTalerBank(parsed.account, i18n)
- if (result) return result
- break;
- }
- case "iban": {
- if (parsed.targetType !== "iban") {
- return i18n.str`Only "IBAN" target are supported`
- }
- const result = validateIBAN(parsed.iban, i18n)
- if (result) return result
- break;
- }
- default: assertUnreachable(type)
- }
- if (!parsed.params.amount) {
- return i18n.str`Missing "amount" parameter to specify the amount to be transferred`
- }
- const amount = Amounts.parse(parsed.params.amount)
- if (!amount) {
- return i18n.str`The "amount" parameter is not valid`
- }
- result = validateAmount(amount, limit, i18n)
- if (result) return result;
-
- if (!parsed.params.message) {
- return i18n.str`Missing the "message" parameter to specify a reference text for the transfer`
- }
- const subject = parsed.params.message
- result = validateSubject(subject, i18n)
- if (result) return result;
-
- return undefined
-}
-
-function validateAmount(amount: AmountJson, limit: AmountJson, i18n: InternationalizationAPI): TranslatedString | undefined {
- if (amount.currency !== limit.currency) {
- return i18n.str`The only currency allowed is "${limit.currency}"`
- }
- if (Amounts.isZero(amount)) {
- return i18n.str`Can't transfer zero amount`
- }
- if (Amounts.cmp(limit, amount) === -1) {
- return i18n.str`Balance is not enough`
- }
- return undefined
-}
-
-function validateSubject(text: string, i18n: InternationalizationAPI): TranslatedString | undefined {
- if (text.length < 2) {
- return i18n.str`Use a longer subject`
- }
- return undefined
-}
-
-interface PaytoFieldProps {
- id: string,
- label: TranslatedString;
- help?: TranslatedString;
- placeholder?: TranslatedString;
- error: string | undefined;
- value: string | undefined;
- rightIcons?: VNode;
- onChange: (p: string) => void;
- focus?: boolean;
- disabled?: boolean;
-}
-
-function Wrapper({ withIcon, children }: { withIcon: boolean, children: ComponentChildren }): VNode {
- if (withIcon) {
- return <div class="flex justify-between">
- {children}
- </div>
- }
- return <Fragment>{children}</Fragment>
-}
-
-export function TextField({
- id,
- label,
- help,
- focus,
- disabled,
- onChange,
- placeholder,
- rightIcons,
- value,
- error,
-}: PaytoFieldProps): VNode {
- return <div class="sm:col-span-5">
- <label
- for={id}
- class="block text-sm font-medium leading-6 text-gray-900"
- >{label}</label>
- <div class="mt-2">
- <Wrapper withIcon={rightIcons !== undefined}>
- <input
- ref={focus ? doAutoFocus : undefined}
- type="text"
- class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name={id}
- id={id}
- disabled={disabled}
- value={value ?? ""}
- placeholder={placeholder}
- autocomplete="off"
- required
- onInput={(e): void => {
- onChange(e.currentTarget.value);
- }}
- />
- {rightIcons}
- </Wrapper>
- <ShowInputErrorLabel
- message={error}
- isDirty={value !== undefined}
- />
- </div>
- {help &&
- <p class="mt-2 text-sm text-gray-500">
- {help}
- </p>
- }
- </div>
-}
diff --git a/packages/demobank-ui/src/pages/ProfileNavigation.tsx b/packages/demobank-ui/src/pages/ProfileNavigation.tsx
deleted file mode 100644
index 10497f015..000000000
--- a/packages/demobank-ui/src/pages/ProfileNavigation.tsx
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- 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 { assertUnreachable } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { useBankCoreApiContext } from "../context/config.js";
-import { useNavigationContext } from "../context/navigation.js";
-import { useSessionState } from "../hooks/session.js";
-import { RouteDefinition } from "../route.js";
-
-export function ProfileNavigation({
- current,
- routeMyAccountCashout,
- routeMyAccountDelete,
- routeMyAccountDetails,
- routeMyAccountPassword,
- routeConversionConfig
-}: {
- current: "details" | "delete" | "credentials" | "cashouts" | "conversion",
- routeMyAccountDetails: RouteDefinition;
- routeMyAccountDelete: RouteDefinition;
- routeMyAccountPassword: RouteDefinition;
- routeMyAccountCashout: RouteDefinition;
- routeConversionConfig: RouteDefinition;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { config } = useBankCoreApiContext();
- const { state: credentials } = useSessionState();
- const isAdminUser =
- credentials.status !== "loggedIn"
- ? false
- : credentials.isUserAdministrator;
- const nonAdminUser = !isAdminUser;
-
- const { navigateTo } = useNavigationContext();
- 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": {
- navigateTo(routeMyAccountDetails.url({}));
- return;
- }
- case "delete": {
- navigateTo(routeMyAccountDelete.url({}));
- return;
- }
- case "credentials": {
- navigateTo(routeMyAccountPassword.url({}));
- return;
- }
- case "cashouts": {
- navigateTo(routeMyAccountCashout.url({}));
- return;
- }
- case "conversion": {
- navigateTo(routeConversionConfig.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 ? (
- <Fragment>
- <option value="cashouts" selected={current == "cashouts"}>
- <i18n.Translate>Cashouts</i18n.Translate>
- </option>
- <option value="conversion" selected={current == "cashouts"}>
- <i18n.Translate>Conversion</i18n.Translate>
- </option>
- </Fragment>
- ) : undefined}
- </select>
- </div>
- <div class="hidden sm:block">
- <nav
- class="isolate flex divide-x divide-gray-200 rounded-lg shadow"
- aria-label="Tabs"
- >
- <a
- name="my account details"
- href={routeMyAccountDetails.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>
- {!config.allow_deletions ? undefined : (
- <a
- name="my account delete"
- href={routeMyAccountDelete.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
- name="my account password"
- href={routeMyAccountPassword.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>
- {config.allow_conversion && nonAdminUser ? (
- <a
- name="my account cashout"
- href={routeMyAccountCashout.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}
- {config.allow_conversion && isAdminUser ? (
- <a
- name="conversion config"
- href={routeConversionConfig.url({})}
- data-selected={current == "conversion"}
- 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>Conversion</i18n.Translate>
- </span>
- <span
- aria-hidden="true"
- data-selected={current == "conversion"}
- class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"
- ></span>
- </a>
- ) : undefined}
- </nav>
- </div>
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
deleted file mode 100644
index 84d703cbe..000000000
--- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- 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 { 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 { Transactions } from "../components/Transactions/index.js";
-import { usePublicAccounts } from "../hooks/account.js";
-
-/**
- * Show histories of public accounts.
- */
-export function PublicHistoriesPage(): VNode {
- const { i18n } = useTranslationContext();
-
- // 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 [showAccount, setShowAccount] = useState(firstAccount);
-
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- return <Loading />;
- }
-
- const { data } = result;
-
- const txs: Record<string, h.JSX.Element> = {};
- const accountsBar = [];
-
- // Ask story of all the public accounts.
- for (const account of data.public_accounts) {
- const isSelected = account.username == showAccount;
- accountsBar.push(
- <li
- class={
- isSelected
- ? "pure-menu-selected pure-menu-item"
- : "pure-menu-item pure-menu"
- }
- >
- <a
- href="#"
- name={`show account ${account.username}`}
- class="pure-menu-link"
- onClick={() => setShowAccount(account.username)}
- >
- {account.username}
- </a>
- </li>,
- );
- txs[account.username] = <Transactions account={account.username} routeCreateWireTransfer={undefined} />;
- }
-
- return (
- <Fragment>
- <h1 class="nav">{i18n.str`History of public accounts`}</h1>
- <section id="main">
- <article>
- <div class="pure-menu pure-menu-horizontal" name="accountMenu">
- <ul class="pure-menu-list">{accountsBar}</ul>
- {typeof showAccount !== "undefined" ? (
- txs[showAccount]
- ) : (
- <p>No public transactions found.</p>
- )}
- <br />
- </div>
- </article>
- </section>
- </Fragment>
- );
-}
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.stories.tsx b/packages/demobank-ui/src/pages/QrCodeSection.stories.tsx
deleted file mode 100644
index d53d2e7b4..000000000
--- a/packages/demobank-ui/src/pages/QrCodeSection.stories.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- 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/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import { QrCodeSection } from "./QrCodeSection.js";
-import { parseWithdrawUri } from "@gnu-taler/taler-util";
-
-export default {
- title: "Qr Code Selection",
-};
-
-export const SimpleExample = tests.createExample(QrCodeSection, {
- withdrawUri: parseWithdrawUri("taler://withdraw/bank.com/operationId"),
-});
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx
deleted file mode 100644
index da11e631d..000000000
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- 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,
- stringifyWithdrawUri,
- WithdrawUriResult
-} from "@gnu-taler/taler-util";
-import {
- Button,
- LocalNotificationBanner,
- useLocalNotificationHandler,
- 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 { useTalerWalletIntegrationAPI } from "../context/wallet-integration.js";
-import { useSessionState } from "../hooks/session.js";
-
-export function QrCodeSection({
- withdrawUri,
- onAborted,
-}: {
- withdrawUri: WithdrawUriResult;
- onAborted: () => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const walletInegrationApi = useTalerWalletIntegrationAPI();
- const talerWithdrawUri = stringifyWithdrawUri(withdrawUri);
- const { state: credentials } = useSessionState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials;
-
- useEffect(() => {
- walletInegrationApi.publishTalerAction(withdrawUri);
- }, []);
-
- const [notification, handleError] = useLocalNotificationHandler();
-
- const { bank: api } = useBankCoreApiContext();
-
- const onAbortHandler = handleError(
- async () => {
- if (!creds) return undefined;
- return api.abortWithdrawalById(
- creds,
- withdrawUri.withdrawalOperationId,
- )
- },
- onAborted,
- (fail) => {
- switch (fail.case) {
- case HttpStatusCode.BadRequest: return i18n.str`The operation id is invalid.`;
- case HttpStatusCode.NotFound: return i18n.str`The operation was not found.`;
- case HttpStatusCode.Conflict: return i18n.str`The reserve operation has been confirmed previously and can't be aborted`;
- }
- }
- )
-
- return (
- <Fragment>
- <LocalNotificationBanner notification={notification} />
- <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>
- </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"
- name="wallet page"
- 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"
- name="cancel"
- class="text-sm font-semibold leading-6 text-gray-900"
- handler={onAbortHandler}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </Button>
- <a
- href={talerWithdrawUri}
- name="withdraw"
- 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>
- </a>
- </div>
- </div>
- </div>
-
- <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 Taler 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>
- </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"
- // 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"
- handler={onAbortHandler}
- >
- <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
deleted file mode 100644
index e9f7e602f..000000000
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ /dev/null
@@ -1,415 +0,0 @@
-/*
- 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 {
- AccessToken,
- HttpStatusCode,
- OperationFail,
- TalerErrorCode,
- TranslatedString,
- assertUnreachable,
-} from "@gnu-taler/taler-util";
-import {
- LocalNotificationBanner,
- ShowInputErrorLabel,
- useLocalNotification,
- 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 { useSettingsContext } from "../context/settings.js";
-import { RouteDefinition } from "../route.js";
-import { undefinedIfEmpty } from "../utils.js";
-import { getRandomPassword, getRandomUsername } from "./rnd.js";
-
-export function RegistrationPage({
- onRegistrationSuccesful,
- routeCancel,
-}: {
- onRegistrationSuccesful: (user: string, password: string) => void;
- routeCancel: RouteDefinition;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { config } = useBankCoreApiContext();
- if (!config.allow_registrations) {
- return (
- <p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
- );
- }
- 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}$/;
-
-/**
- * Collect and submit registration data.
- */
-function RegistrationForm({
- onRegistrationSuccesful,
- routeCancel,
-}: {
- onRegistrationSuccesful: (user: string, password: string) => void;
- routeCancel: RouteDefinition;
-}): 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 [repeatPassword, setRepeatPassword] = useState<string | undefined>();
- const [notification, _, handleError] = useLocalNotification();
- const settings = useSettingsContext();
-
- const { bank: api } = useBankCoreApiContext();
- // const { register } = useTestingAPI();
- const { i18n } = useTranslationContext();
-
- const errors = undefinedIfEmpty({
- 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,
- password: !password ? i18n.str`Missing password` : undefined,
- repeatPassword: !repeatPassword
- ? i18n.str`Missing password`
- : repeatPassword !== password
- ? i18n.str`Passwords don't match`
- : undefined,
- });
-
- async function doRegistrationAndLogin(
- name: string,
- username: string,
- password: string,
- onComplete: () => void,
- ) {
- await handleError(async (onError) => {
- const resp = await api.createAccount("" as AccessToken, {
- name,
- username,
- password,
- });
- if (resp.type === "ok") {
- onComplete();
- } else {
- onError(resp, (_case) => {
- switch(_case) {
- case HttpStatusCode.BadRequest: return i18n.str`Server replied with invalid phone or email.`;
- case HttpStatusCode.Unauthorized: return i18n.str`No enough permission to create that account.`;
- case TalerErrorCode.BANK_UNALLOWED_DEBIT: return i18n.str`Registration is disabled because the bank ran out of bonus credit.`;
- case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT: return i18n.str`That username can't be used because is reserved.`;
- case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE: return i18n.str`That username is already taken.`;
- case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE: return i18n.str`That account id is already taken.`;
- case TalerErrorCode.BANK_MISSING_TAN_INFO: return i18n.str`No information for the selected authentication channel.`;
- case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: return i18n.str`Authentication channel is not supported.`;
- case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return i18n.str`Only admin is allow to set debt limit.`;
- case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL: return i18n.str`Only admin can create accounts with second factor authentication.`;
- }
- })
- }
- });
- }
-
- async function doRegistrationStep() {
- if (!username || !password || !name) return;
- await doRegistrationAndLogin(name, username, password, () => {
- setUsername(undefined);
- setPassword(undefined);
- setRepeatPassword(undefined);
- onRegistrationSuccesful(username, password);
- });
- }
-
- async function doRandomRegistration() {
- const user = getRandomUsername();
-
- const password = settings.simplePasswordForRandomAccounts
- ? "123"
- : getRandomPassword();
- const username = `_${user.first}-${user.second}_`;
- const name = `${capitalizeFirstLetter(user.first)} ${capitalizeFirstLetter(
- user.second,
- )}`;
- await doRegistrationAndLogin(name, username, password, () => {
- onRegistrationSuccesful(username, password);
- });
- }
-
- return (
- <Fragment>
- <LocalNotificationBanner notification={notification} />
-
- <div class="flex min-h-full flex-col justify-center">
- <div class="sm:mx-auto sm:w-full sm:max-w-sm">
- <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Account registration`}</h2>
- </div>
-
- <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
- <form
- class="space-y-6"
- noValidate
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <div>
- <label
- for="username"
- class="block text-sm font-medium leading-6 text-gray-900"
- >
- <i18n.Translate>Login username</i18n.Translate>
- <b style={{ color: "red" }}> *</b>
- </label>
- <div class="mt-2">
- <input
- autoFocus
- type="text"
- name="username"
- id="username"
- 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={username ?? ""}
- enterkeyhint="next"
- placeholder="account identification to login"
- autocomplete="username"
- required
- onInput={(e): void => {
- setUsername(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={username !== undefined}
- />
- </div>
- </div>
-
- <div>
- <div class="flex items-center justify-between">
- <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>
- </div>
- <div class="mt-2">
- <input
- type="password"
- name="password"
- id="password"
- autocomplete="current-password"
- 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"
- enterkeyhint="send"
- value={password ?? ""}
- placeholder="Password"
- required
- onInput={(e): void => {
- setPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- </div>
- </div>
-
- <div>
- <div class="flex items-center justify-between">
- <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>
- </div>
- <div class="mt-2">
- <input
- type="password"
- name="register-repeat"
- id="register-repeat"
- autocomplete="current-password"
- 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"
- enterkeyhint="send"
- value={repeatPassword ?? ""}
- placeholder="Same password"
- required
- onInput={(e): void => {
- setRepeatPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.repeatPassword}
- isDirty={repeatPassword !== undefined}
- />
- </div>
- </div>
-
- <div>
- <div class="flex items-center justify-between">
- <label
- for="name"
- class="block text-sm font-medium leading-6 text-gray-900"
- >
- <i18n.Translate>Full name</i18n.Translate>
- <b style={{ color: "red" }}> *</b>
- </label>
- </div>
- <div class="mt-2">
- <input
- autoFocus
- type="text"
- name="name"
- id="name"
- 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={name ?? ""}
- enterkeyhint="next"
- placeholder="John Doe"
- autocomplete="name"
- required
- onInput={(e): void => {
- setName(e.currentTarget.value);
- }}
- />
- {/* <ShowInputErrorLabel
- message={errors?.name}
- isDirty={name !== undefined}
- /> */}
- </div>
- </div>
-
- {/* <div>
- <label for="phone" class="block text-sm font-medium leading-6 text-gray-900">
- <i18n.Translate>Phone</i18n.Translate>
- </label>
- <div class="mt-2">
- <input
- autoFocus
- type="text"
- name="phone"
- id="phone"
- 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={phone ?? ""}
- enterkeyhint="next"
- placeholder="your phone"
- autocomplete="none"
- onInput={(e): void => {
- setPhone(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.phone}
- isDirty={phone !== undefined}
- />
- </div>
- </div>
- <div>
- <label for="email" class="block text-sm font-medium leading-6 text-gray-900">
- <i18n.Translate>Email</i18n.Translate>
- </label>
- <div class="mt-2">
- <input
- autoFocus
- type="text"
- name="email"
- id="email"
- 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={email ?? ""}
- enterkeyhint="next"
- placeholder="your email"
- autocomplete="email"
- onInput={(e): void => {
- setEmail(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.email}
- isDirty={email !== undefined}
- />
- </div>
- </div> */}
-
- <div class="flex w-full justify-between">
- <a
- name="cancel"
- 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"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- name="register"
- 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();
-
- doRegistrationStep();
- }}
- >
- <i18n.Translate>Register</i18n.Translate>
- </button>
- </div>
- </form>
-
- {settings.allowRandomAccountCreation && (
- <p class="mt-10 text-center text-sm text-gray-500 border-t">
- <button
- type="submit"
- name="create random"
- 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();
- }}
- >
- <i18n.Translate>Create a random temporary user</i18n.Translate>
- </button>
- </p>
- )}
- </div>
- </div>
- </Fragment>
- );
-}
-
-function capitalizeFirstLetter(str: string) {
- return str.charAt(0).toUpperCase() + str.slice(1);
-}
diff --git a/packages/demobank-ui/src/pages/SolveChallengePage.tsx b/packages/demobank-ui/src/pages/SolveChallengePage.tsx
deleted file mode 100644
index b2e053b3c..000000000
--- a/packages/demobank-ui/src/pages/SolveChallengePage.tsx
+++ /dev/null
@@ -1,716 +0,0 @@
-/*
- 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,
- Duration,
- HttpStatusCode,
- TalerCorebankApi,
- TalerError,
- TalerErrorCode,
- TranslatedString,
- assertUnreachable,
- parsePaytoUri,
-} from "@gnu-taler/taler-util";
-import {
- Attention,
- Loading,
- LocalNotificationBanner,
- ShowInputErrorLabel,
- useLocalNotification,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
-import { Fragment, VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js";
-import { useBankCoreApiContext } from "../context/config.js";
-import { useWithdrawalDetails } from "../hooks/account.js";
-import { useSessionState } from "../hooks/session.js";
-import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js";
-import { useConversionInfo } from "../hooks/regional.js";
-import { RouteDefinition } from "../route.js";
-import { undefinedIfEmpty } from "../utils.js";
-import { RenderAmount } from "./PaytoWireTransferForm.js";
-import { OperationNotFound } from "./WithdrawalQRCode.js";
-import { useNavigationContext } from "../context/navigation.js";
-import { Time } from "../components/Time.js";
-
-export function SolveChallengePage({
- onChallengeCompleted,
- routeClose,
-}: {
- onChallengeCompleted: () => void;
- routeClose: RouteDefinition;
-}): VNode {
- const { bank: api } = useBankCoreApiContext();
- const { i18n } = useTranslationContext();
- const [bankState, updateBankState] = useBankState();
- const [code, setCode] = useState<string | undefined>(undefined);
- const [notification, notify, handleError] = useLocalNotification();
- const { state } = useSessionState();
- const creds = state.status !== "loggedIn" ? undefined : state;
- const { navigateTo } = useNavigationContext();
-
- if (!bankState.currentChallenge) {
- return (
- <div>
- <span>no challenge to solve </span>
- <a
- href={routeClose.url({})}
- name="close"
- 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 errors = undefinedIfEmpty({
- code: !code ? i18n.str`Required` : undefined,
- });
-
- async function startChallenge() {
- if (!creds) return;
- 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);
- } else {
- const newCh = structuredClone(ch);
- newCh.sent = AbsoluteTime.now();
- newCh.info = undefined;
- updateBankState("currentChallenge", newCh);
- 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);
- }
- }
- });
- }
-
- async function completeChallenge() {
- if (!creds || !code) return;
- await handleError(async () => {
- {
- const resp = await api.confirmChallenge(creds, ch.id, {
- tan: code,
- });
- if (resp.type === "fail") {
- 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 attempts, 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);
- }
- })(ch);
-
- if (resp.type === "fail") {
- if (resp.case !== HttpStatusCode.Accepted) {
- return notify({
- type: "error",
- title: i18n.str`The operation failed.`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- }
- // 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),
- location: ch.location,
- sent: AbsoluteTime.never(),
- request: ch.request,
- });
- return notify({
- type: "info",
- title: i18n.str`The operation needs another confirmation to complete.`,
- });
- }
- 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`;
- }
- })(ch.operation);
-
- return (
- <Fragment>
- <LocalNotificationBanner notification={notification} />
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- <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>
- </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}
- onCancel={() => {
- updateBankState("currentChallenge", undefined);
- navigateTo(ch.location)
- }}
- />
- {ch.info && (
- <div class="mt-2">
- <form
- class="bg-white shadow-sm ring-1 ring-gray-900/5"
- autoCapitalize="none"
- autoCorrect="off"
- onSubmit={(e) => {
- e.preventDefault();
- }}
- >
- <div class="px-4 py-4">
- <label for="withdraw-amount">
- <i18n.Translate>Enter the confirmation code</i18n.Translate>
- </label>
- <div class="mt-2">
- <div class="relative rounded-md shadow-sm">
- <input
- type="text"
- // class="block w-full rounded-md border-0 py-1.5 pl-16 text-gray-900 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"
- aria-describedby="answer"
- autoFocus
- 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);
- }}
- />
- </div>
- <ShowInputErrorLabel
- message={errors?.code}
- isDirty={code !== undefined}
- />
- </div>
- </div>
- <div class="flex items-center justify-between border-gray-900/10 px-4 py-4 ">
- <div />
- <button
- type="submit"
- name="confirm"
- 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();
- e.preventDefault();
- }}
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </button>
- </div>
- </form>
- </div>
- )}
- </div>
- </div>
- </Fragment>
- );
-}
-
-function ChallengeDetails({
- challenge,
- onStart,
- onCancel,
-}: {
- challenge: ChallengeInProgess;
- onStart: () => void;
- onCancel: () => void;
-}): VNode {
- const { i18n, dateLocale } = useTranslationContext();
- const { config } = useBankCoreApiContext();
-
- const firstTime = AbsoluteTime.isNever(challenge.sent)
- useEffect(() => {
- if (firstTime) {
- onStart()
- }
- }, [])
- return (
- <div class="px-4 mt-4 ">
- <div class="w-full">
- <div class="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">
- <i18n.Translate>Account</i18n.Translate>
- </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">
- <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={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">
- <i18n.Translate>To account</i18n.Translate>
- </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">
- <i18n.Translate>Cashout account</i18n.Translate>
- </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">
- <i18n.Translate>Email</i18n.Translate>
- </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">
- <i18n.Translate>Phone</i18n.Translate>
- </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">
- <i18n.Translate>Debit threshold</i18n.Translate>
- </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">
- <i18n.Translate>
- Is this account public?
- </i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.request.is_public
- ? i18n.str`Enable`
- : i18n.str`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">
- <i18n.Translate>Name</i18n.Translate>
- </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">
- <i18n.Translate>
- Authentication channel
- </i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {challenge.request.tan_channel ?? i18n.str`Remove`}
- </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">
- <i18n.Translate>New password</i18n.Translate>
- </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">
- <Time format="dd/MM/yyyy HH:mm:ss"
- timestamp={challenge.sent}
- relative={Duration.fromSpec({ days: 1 })} />
- </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 class="mt-6 mb-4 flex justify-between">
- <button
- type="button"
- name="cancel"
- class="text-sm font-semibold leading-6 text-gray-900"
- onClick={onCancel}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- {challenge.info ? (
- <button
- type="submit"
- name="send again"
- 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>
- ) : (
- <div> sending code ...</div>
- )}
- </div>
- </div>
- </div>
- );
-}
-
-function ShowWithdrawalDetails({ id }: { id: string }): VNode {
- const details = useWithdrawalDetails(id);
- const { i18n } = useTranslationContext();
- const { config } = useBankCoreApiContext();
- if (!details) {
- return <Loading />;
- }
- if (details instanceof TalerError) {
- return <ErrorLoadingWithDebug error={details} />;
- }
- if (details.type === "fail") {
- switch (details.case) {
- case HttpStatusCode.BadRequest:
- 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">
- <i18n.Translate>Withdraw id</i18n.Translate>
- </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">
- <i18n.Translate>To account</i18n.Translate>
- </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 {
- const { i18n } = useTranslationContext();
- const info = useConversionInfo();
- if (!info) {
- return <Loading />;
- }
-
- if (info instanceof TalerError) {
- return <ErrorLoadingWithDebug error={info} />;
- }
- if (info.type === "fail") {
- switch (info.case) {
- case HttpStatusCode.NotImplemented: {
- return (
- <Attention
- type="danger"
- title={i18n.str`Cashout are disabled`}
- >
- <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
- </Attention>
- );
- }
- default:
- assertUnreachable(info.case);
- }
- }
-
- 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">
- <i18n.Translate>Subject</i18n.Translate>
- </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">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>
- );
-}
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
deleted file mode 100644
index 001d90fa1..000000000
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ /dev/null
@@ -1,366 +0,0 @@
-/*
- 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 {
- AmountJson,
- Amounts,
- HttpStatusCode,
- TranslatedString,
- assertUnreachable,
- parseWithdrawUri
-} from "@gnu-taler/taler-util";
-import {
- Attention,
- LocalNotificationBanner,
- notifyError,
- useLocalNotification,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
-import { forwardRef } from "preact/compat";
-import { useState } from "preact/hooks";
-import { useBankCoreApiContext } from "../context/config.js";
-import { useSessionState } from "../hooks/session.js";
-import { useBankState } from "../hooks/bank-state.js";
-import { usePreferences } from "../hooks/preferences.js";
-import { RouteDefinition } from "../route.js";
-import { undefinedIfEmpty } from "../utils.js";
-import { OperationState } from "./OperationState/index.js";
-import { InputAmount, doAutoFocus } from "./PaytoWireTransferForm.js";
-
-const RefAmount = forwardRef(InputAmount);
-
-function OldWithdrawalForm({
- onOperationCreated,
- limit,
- routeCancel,
- focus,
- routeOperationDetails,
-}: {
- limit: AmountJson;
- focus?: boolean;
- routeOperationDetails: RouteDefinition<{ wopid: string }>,
- onOperationCreated: (wopid: string) => void;
- routeCancel: RouteDefinition;
-}): VNode {
- const { i18n } = useTranslationContext();
- const [settings] = usePreferences();
-
- // const walletInegrationApi = useTalerWalletIntegrationAPI()
- // const { navigateTo } = useNavigationContext();
-
- const [bankState, updateBankState] = useBankState();
- const { bank: api } = useBankCoreApiContext();
-
- const { state: credentials } = useSessionState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials;
-
- const [amountStr, setAmountStr] = useState<string | undefined>(
- `${settings.maxWithdrawalAmount}`,
- );
- const [notification, notify, handleError] = useLocalNotification();
-
- if (bankState.currentWithdrawalOperationId) {
- // FIXME: doing the preventDefault is not optimal
-
- // const suri = stringifyWithdrawUri({
- // bankIntegrationApiBaseUrl: api.getIntegrationAPI().baseUrl,
- // withdrawalOperationId: bankState.currentWithdrawalOperationId,
- // });
- // const uri = parseWithdrawUri(suri)!
- const url = routeOperationDetails.url({
- wopid: bankState.currentWithdrawalOperationId,
- });
- return (
- <Attention type="warning" title={i18n.str`There is an operation already`} onClose={() => {
- updateBankState("currentWithdrawalOperationId", undefined);
- }}>
- <span ref={focus ? doAutoFocus : undefined} />
- <i18n.Translate>
- Complete the operation in
- </i18n.Translate>{" "}
- <a
- class="font-semibold text-yellow-700 hover:text-yellow-600"
- name="complete operation"
- href={url}
- // onClick={(e) => {
- // e.preventDefault()
- // walletInegrationApi.publishTalerAction(uri, () => {
- // navigateTo(url)
- // })
- // }}
- >
- <i18n.Translate>this page</i18n.Translate>
- </a>
- </Attention>
- );
- }
-
- const trimmedAmountStr = amountStr?.trim();
-
- const parsedAmount = trimmedAmountStr
- ? Amounts.parse(`${limit.currency}:${trimmedAmountStr}`)
- : undefined;
-
- const errors = undefinedIfEmpty({
- amount:
- trimmedAmountStr == null
- ? i18n.str`Required`
- : !parsedAmount
- ? i18n.str`Invalid`
- : Amounts.cmp(limit, parsedAmount) === -1
- ? i18n.str`Balance is not enough`
- : undefined,
- });
-
- async function doStart() {
- if (!parsedAmount || !creds) return;
- await handleError(async () => {
- const resp = await api.createWithdrawal(creds, {
- amount: Amounts.stringify(parsedAmount),
- });
- if (resp.type === "ok") {
- const uri = parseWithdrawUri(resp.body.taler_withdraw_uri);
- if (!uri) {
- return notifyError(
- i18n.str`Server responded with an invalid withdraw URI`,
- i18n.str`Withdraw URI: ${resp.body.taler_withdraw_uri}`,
- );
- } else {
- updateBankState(
- "currentWithdrawalOperationId",
- uri.withdrawalOperationId,
- );
- onOperationCreated(uri.withdrawalOperationId);
- }
- } else {
- switch (resp.case) {
- case HttpStatusCode.Conflict: {
- notify({
- type: "error",
- title: i18n.str`The operation was rejected due to insufficient funds`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- break;
- }
- case HttpStatusCode.Unauthorized: {
- notify({
- type: "error",
- title: i18n.str`The operation was rejected due to insufficient funds`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- break;
- }
- case HttpStatusCode.NotFound: {
- notify({
- type: "error",
- title: i18n.str`Account not found`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- break;
- }
- 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} />
-
- <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 class="mt-4">
- <div class="sm:inline">
- <button
- type="button"
- name="set 50"
- 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"
- name="set 25"
- 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"
- name="set 10"
- 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"
- name="set 5"
- 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 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({})}
- name="cancel"
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- name="continue"
- 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,
- routeCancel,
- onAuthorizationRequired,
- onOperationCreated,
- onOperationAborted,
- routeOperationDetails,
-}: {
- limit: AmountJson;
- focus?: boolean;
- routeOperationDetails: RouteDefinition<{ wopid: string }>,
- onAuthorizationRequired: () => void;
- onOperationCreated: (wopid: string) => void;
- onOperationAborted: () => void;
- routeCancel: RouteDefinition;
-}): VNode {
- const { i18n } = useTranslationContext();
- 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 Taler 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 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"
- name="wallet page"
- 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}
- routeOperationDetails={routeOperationDetails}
- limit={limit}
- routeCancel={routeCancel}
- onOperationCreated={onOperationCreated}
- />
- ) : (
- <OperationState
- currency={limit.currency}
- onAuthorizationRequired={onAuthorizationRequired}
- routeClose={routeCancel}
- routeHere={routeOperationDetails}
- onAbort={onOperationAborted}
- // route={routeCancel}
- />
- )}
- </div>
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/pages/WireTransfer.tsx b/packages/demobank-ui/src/pages/WireTransfer.tsx
deleted file mode 100644
index 33f067e63..000000000
--- a/packages/demobank-ui/src/pages/WireTransfer.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- 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/account.js";
-import { useSessionState } from "../hooks/session.js";
-import { LoginForm } from "./LoginForm.js";
-import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
-import { RouteDefinition } from "../route.js";
-
-export function WireTransfer({
- toAccount,
- withSubject,
- withAmount,
- onAuthorizationRequired,
- routeCancel,
- routeHere,
- onSuccess,
-}: {
- onSuccess?: () => void;
- routeHere: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
- }>;
- toAccount?: string;
- withSubject?: string,
- withAmount?: string,
- routeCancel?: RouteDefinition;
- onAuthorizationRequired: () => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const r = useSessionState();
- const account = r.state.status !== "loggedOut" ? r.state.username : "admin";
- const result = useAccountDetails(account);
-
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- 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);
- }
- }
- const { body: data } = result;
-
- const balance = Amounts.parseOrThrow(data.balance.amount);
- const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
-
- const debitThreshold = Amounts.parseOrThrow(data.debit_threshold);
- const limit = balanceIsDebit
- ? Amounts.sub(debitThreshold, balance).amount
- : Amounts.add(balance, debitThreshold).amount;
- if (!balance) return <Fragment />;
- return (
- <PaytoWireTransferForm
- title={i18n.str`Make a wire transfer`}
- withAccount={toAccount}
- withAmount={withAmount}
- withSubject={withSubject}
- routeHere={routeHere}
- limit={limit}
- onAuthorizationRequired={onAuthorizationRequired}
- onSuccess={() => {
- notifyInfo(i18n.str`Wire transfer created!`);
- if (onSuccess) onSuccess();
- }}
- routeCancel={routeCancel}
- />
- );
-}
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
deleted file mode 100644
index 5925719c3..000000000
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ /dev/null
@@ -1,355 +0,0 @@
-/*
- 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,
- AmountJson,
- HttpStatusCode,
- PaytoUri,
- PaytoUriIBAN,
- PaytoUriTalerBank,
- TalerErrorCode,
- TranslatedString,
- WithdrawUriResult,
- assertUnreachable,
-} from "@gnu-taler/taler-util";
-import {
- Attention,
- LocalNotificationBanner,
- notifyInfo,
- useLocalNotification,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { ComponentChildren, Fragment, VNode, h } from "preact";
-import { mutate } from "swr";
-import { useBankCoreApiContext } from "../context/config.js";
-import { useBankState } from "../hooks/bank-state.js";
-import { usePreferences } from "../hooks/preferences.js";
-import { useSessionState } from "../hooks/session.js";
-import { RouteDefinition } from "../route.js";
-import { LoginForm } from "./LoginForm.js";
-import { RenderAmount } from "./PaytoWireTransferForm.js";
-
-interface Props {
- onAborted: () => void;
- withdrawUri: WithdrawUriResult;
- routeHere: RouteDefinition<{ wopid: string }>;
- details: {
- account: PaytoUri;
- reserve: string;
- username: string;
- amount: AmountJson;
- };
- onAuthorizationRequired: () => void;
-}
-/**
- * Additional authentication required to complete the operation.
- * Not providing a back button, only abort.
- */
-export function WithdrawalConfirmationQuestion({
- onAborted,
- details,
- onAuthorizationRequired,
- routeHere,
- withdrawUri,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
- const [settings] = usePreferences();
- const { state: credentials } = useSessionState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials;
- const [, updateBankState] = useBankState();
-
- const [notification, notify, handleError] = useLocalNotification();
-
- const { config, bank: api } = useBankCoreApiContext();
-
- async function doTransfer() {
- await handleError(async () => {
- if (!creds) return;
- const resp = await api.confirmWithdrawalById(
- creds,
- withdrawUri.withdrawalOperationId,
- );
- if (resp.type === "ok") {
- mutate(() => true); // clean any info that we have
- if (!settings.showWithdrawalSuccess) {
- 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 HttpStatusCode.Accepted: {
- updateBankState("currentChallenge", {
- operation: "confirm-withdrawal",
- id: String(resp.body.challenge_id),
- location: routeHere.url({ wopid: withdrawUri.withdrawalOperationId }),
- sent: AbsoluteTime.never(),
- request: withdrawUri.withdrawalOperationId,
- });
- return onAuthorizationRequired();
- }
- default:
- assertUnreachable(resp);
- }
- }
- });
- }
-
- async function doCancel() {
- await handleError(async () => {
- if (!creds) return;
- 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,
- });
- default: {
- assertUnreachable(resp);
- }
- }
- }
- });
- }
-
- return (
- <Fragment>
- <LocalNotificationBanner notification={notification} />
-
- <div class="bg-white shadow sm:rounded-lg">
- <div class="px-4 py-5 sm:p-6">
- <h3 class="text-base font-semibold text-gray-900">
- <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();
- }}
- >
- <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>
- </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>Payment provider's account number</i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {p.iban}
- </dd>
- </div>
- {name && (
- <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">
- <i18n.Translate>Payment provider's name</i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {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>Payment provider's account id</i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {p.account}
- </dd>
- </div>
- {name && (
- <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
- <dt class="text-sm font-medium leading-6 text-gray-900">
- <i18n.Translate>Payment provider's name</i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {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>Payment provider's account</i18n.Translate>
- </dt>
- <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
- {details.account.targetPath}
- </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">
- <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}
- />
- </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"
- name="cancel"
- class="text-sm font-semibold leading-6 text-gray-900"
- onClick={doCancel}
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- <button
- type="submit"
- name="transfer"
- 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();
- }}
- >
- <i18n.Translate>Transfer</i18n.Translate>
- </button>
- </div>
- </form>
- </div>
- </ShouldBeSameUser>
- </div>
- </div>
- </div>
- </Fragment>
- );
-}
-
-export function ShouldBeSameUser({
- username,
- children,
-}: {
- username: string;
- children: ComponentChildren;
-}): VNode {
- const { state: credentials } = useSessionState();
- const { i18n } = useTranslationContext();
- if (credentials.status === "loggedOut") {
- 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>{children}</Fragment>;
-}
diff --git a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
deleted file mode 100644
index 973a23011..000000000
--- a/packages/demobank-ui/src/pages/WithdrawalOperationPage.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- 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 { 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";
-
-export function WithdrawalOperationPage({
- operationId,
- onAuthorizationRequired,
- onOperationAborted,
- routeClose,
- routeWithdrawalDetails,
-}: {
- onAuthorizationRequired: () => void;
- operationId: string;
- purpose: "after-creation" | "after-confirmation",
- onOperationAborted: () => void;
- routeClose: RouteDefinition;
- routeWithdrawalDetails: RouteDefinition<{ wopid: string }>;
-}): VNode {
- const { bank: api } = useBankCoreApiContext();
- const uri = stringifyWithdrawUri({
- bankIntegrationApiBaseUrl: api.getIntegrationAPI(),
- withdrawalOperationId: operationId,
- });
- const parsedUri = parseWithdrawUri(uri);
- const { i18n } = useTranslationContext();
- const [, updateBankState] = useBankState();
-
- if (!parsedUri) {
- return (
- <Attention
- type="danger"
- title={i18n.str`The Withdrawal URI is not valid`}
- >
- {uri}
- </Attention>
- );
- }
-
- return (
- <WithdrawalQRCode
- withdrawUri={parsedUri}
- routeWithdrawalDetails={routeWithdrawalDetails}
- onAuthorizationRequired={onAuthorizationRequired}
- onOperationAborted={() => {
- updateBankState("currentWithdrawalOperationId", undefined);
- onOperationAborted();
- }}
- routeClose={routeClose}
- />
- );
-}
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
deleted file mode 100644
index 9765147d1..000000000
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ /dev/null
@@ -1,310 +0,0 @@
-/*
- 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,
- WithdrawUriResult,
- assertUnreachable,
- parsePaytoUri,
-} from "@gnu-taler/taler-util";
-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/account.js";
-import { RouteDefinition } from "../route.js";
-import { QrCodeSection } from "./QrCodeSection.js";
-import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
-
-interface Props {
- withdrawUri: WithdrawUriResult;
- onOperationAborted: () => void;
- routeClose: RouteDefinition;
- routeWithdrawalDetails: RouteDefinition<{ wopid: string }>;
- onAuthorizationRequired: () => void;
-}
-/**
- * Offer the QR code (and a clickable taler://-link) to
- * permit the passing of exchange and reserve details to
- * the bank. Poll the backend until such operation is done.
- */
-export function WithdrawalQRCode({
- withdrawUri,
- onOperationAborted,
- routeClose,
- routeWithdrawalDetails,
- onAuthorizationRequired,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
- const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
-
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />;
- }
- if (result.type === "fail") {
- switch (result.case) {
- case HttpStatusCode.BadRequest:
- case HttpStatusCode.NotFound:
- return <OperationNotFound routeClose={routeClose} />;
- default:
- assertUnreachable(result);
- }
- }
-
- const { body: data } = result;
-
- if (data.status === "aborted") {
- 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-yellow-100">
- <svg
- class="h-5 w-5 text-yellow-400"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- fill-rule="evenodd"
- d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
- clip-rule="evenodd"
- />
- </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 aborted</i18n.Translate>
- </h3>
- <div class="mt-2">
- <p class="text-sm text-gray-500">
- <i18n.Translate>
- The wire transfer to the payment provider's account was
- aborted from somewhere else, your balance was not affected.
- </i18n.Translate>
- </p>
- </div>
- </div>
- </div>
- <div class="mt-5 sm:mt-6">
- <a
- href={routeClose.url({})}
- name="continue"
- 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>Continue</i18n.Translate>
- </a>
- </div>
- </div>
- );
- }
-
- 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>
- </div>
- </div>
- </div>
- <div class="mt-5 sm:mt-6">
- <a
- href={routeClose.url({})}
- name="done"
- 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>
- );
- }
-
- if (data.status === "pending") {
- return (
- <QrCodeSection
- withdrawUri={withdrawUri}
- onAborted={() => {
- notifyInfo(i18n.str`Operation canceled`);
- onOperationAborted();
- }}
- />
- );
- }
-
- 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>
- );
- }
-
- 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>
- );
- }
-
- 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 (
- <WithdrawalConfirmationQuestion
- withdrawUri={withdrawUri}
- routeHere={routeWithdrawalDetails}
- details={{
- username: data.username,
- account,
- reserve: data.selected_reserve_pub,
- amount: Amounts.parseOrThrow(data.amount),
- }}
- onAuthorizationRequired={onAuthorizationRequired}
- onAborted={() => {
- notifyInfo(i18n.str`Operation canceled`);
- onOperationAborted();
- }}
- />
- );
-}
-
-export function OperationNotFound({
- routeClose,
-}: {
- routeClose: RouteDefinition | 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>
-
- <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({})}
- name="continue to dashboard"
- 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>
- );
-}
diff --git a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
deleted file mode 100644
index 2216b96fc..000000000
--- a/packages/demobank-ui/src/pages/account/CashoutListForAccount.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- 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 { useSessionState } from "../../hooks/session.js";
-import { ProfileNavigation } from "../ProfileNavigation.js";
-import { CreateCashout } from "../regional/CreateCashout.js";
-import { RouteDefinition } from "../../route.js";
-
-interface Props {
- account: string;
- routeClose: RouteDefinition;
- onAuthorizationRequired: () => void;
- routeCashoutDetails: RouteDefinition<{ cid: string }>;
- routeMyAccountDetails: RouteDefinition;
- routeMyAccountDelete: RouteDefinition;
- routeMyAccountPassword: RouteDefinition;
- routeMyAccountCashout: RouteDefinition;
- routeCreateCashout: RouteDefinition;
- routeConversionConfig:RouteDefinition;
-}
-
-export function CashoutListForAccount({
- account,
- onAuthorizationRequired,
- routeCreateCashout,
- routeCashoutDetails,
- routeMyAccountCashout,
- routeMyAccountDelete,
- routeMyAccountDetails,
- routeConversionConfig,
- routeMyAccountPassword,
- routeClose,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
-
- const { state: credentials } = useSessionState();
-
- const accountIsTheCurrentUser =
- credentials.status === "loggedIn"
- ? credentials.username === account
- : false;
-
- return (
- <Fragment>
- {accountIsTheCurrentUser ? (
- <ProfileNavigation current="cashouts"
- routeMyAccountCashout={routeMyAccountCashout}
- routeMyAccountDelete={routeMyAccountDelete}
- routeMyAccountDetails={routeMyAccountDetails}
- routeMyAccountPassword={routeMyAccountPassword}
- routeConversionConfig={routeConversionConfig}
- />
- ) : (
- <h1 class="text-base font-semibold leading-6 text-gray-900">
- <i18n.Translate>Cashout for account {account}</i18n.Translate>
- </h1>
- )}
-
- <CreateCashout
- focus
- routeHere={routeCreateCashout}
- 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
deleted file mode 100644
index 62c8df7f8..000000000
--- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- 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/account.js";
-import { useSessionState } from "../../hooks/session.js";
-import { useBankState } from "../../hooks/bank-state.js";
-import { RouteDefinition } from "../../route.js";
-import { LoginForm } from "../LoginForm.js";
-import { ProfileNavigation } from "../ProfileNavigation.js";
-import { AccountForm } from "../admin/AccountForm.js";
-
-export function ShowAccountDetails({
- account,
- routeClose,
- onUpdateSuccess,
- onAuthorizationRequired,
- routeMyAccountCashout,
- routeMyAccountDelete,
- routeMyAccountDetails,
- routeHere,
- routeMyAccountPassword,
- routeConversionConfig,
-}: {
- routeClose: RouteDefinition;
- routeHere: RouteDefinition<{ account: string }>;
- routeMyAccountDetails: RouteDefinition;
- routeMyAccountDelete: RouteDefinition;
- routeMyAccountPassword: RouteDefinition;
- routeMyAccountCashout: RouteDefinition;
- routeConversionConfig: RouteDefinition;
- onUpdateSuccess: () => void;
- onAuthorizationRequired: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { state: credentials } = useSessionState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials;
- const { bank: api } = useBankCoreApiContext();
- const accountIsTheCurrentUser =
- credentials.status === "loggedIn"
- ? credentials.username === account
- : false;
-
- const [submitAccount, setSubmitAccount] = useState<
- TalerCorebankApi.AccountReconfiguration | undefined
- >();
- const [notification, notify, handleError] = useLocalNotification();
- const [, updateBankState] = useBankState();
-
- const result = useAccountDetails(account);
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />;
- }
- if (result.type === "fail") {
- switch (result.case) {
- case HttpStatusCode.Unauthorized:
- case HttpStatusCode.NotFound:
- return <LoginForm currentUser={account} />;
- default:
- assertUnreachable(result);
- }
- }
-
- async function doUpdate() {
- if (!submitAccount || !creds) return;
- await handleError(async () => {
- 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.Accepted: {
- updateBankState("currentChallenge", {
- operation: "update-account",
- id: String(resp.body.challenge_id),
- location: routeHere.url({ account }),
- sent: AbsoluteTime.never(),
- request: submitAccount,
- });
- return onAuthorizationRequired();
- }
- 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,
- });
- }
- default:
- assertUnreachable(resp);
- }
- }
- });
- }
-
- return (
- <Fragment>
- <LocalNotificationBanner notification={notification} showDebug={true} />
- {accountIsTheCurrentUser ? (
- <ProfileNavigation current="details"
- routeMyAccountCashout={routeMyAccountCashout}
- routeMyAccountDelete={routeMyAccountDelete}
- routeConversionConfig={routeConversionConfig}
- routeMyAccountDetails={routeMyAccountDetails}
- routeMyAccountPassword={routeMyAccountPassword}
- />
- ) : (
- <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"
- >
- <i18n.Translate>Change details</i18n.Translate>
- </span>
- </span>
- </div>
- </h2>
- </div>
-
- <AccountForm
- focus={true}
- username={account}
- template={result.body}
- purpose="update"
- 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">
- <a
- href={routeClose.url({})}
- name="cancel"
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- name="update"
- 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={doUpdate}
- >
- <i18n.Translate>Update</i18n.Translate>
- </button>
- </div>
- </AccountForm>
- </div>
- </Fragment>
- );
-}
diff --git a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
deleted file mode 100644
index e21ac2464..000000000
--- a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx
+++ /dev/null
@@ -1,301 +0,0 @@
-/*
- 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 { useBankCoreApiContext } from "../../context/config.js";
-import { useSessionState } from "../../hooks/session.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";
-
-export function UpdateAccountPassword({
- account: accountName,
- routeClose,
- onUpdateSuccess,
- onAuthorizationRequired,
- routeMyAccountCashout,
- routeMyAccountDelete,
- routeMyAccountDetails,
- routeMyAccountPassword,
- routeConversionConfig,
- focus,
- routeHere,
-}: {
- routeClose: RouteDefinition;
- routeHere: RouteDefinition<{ account: string }>;
- routeMyAccountDetails: RouteDefinition;
- routeMyAccountDelete: RouteDefinition;
- routeMyAccountPassword: RouteDefinition;
- routeMyAccountCashout: RouteDefinition;
- routeConversionConfig: RouteDefinition;
- focus?: boolean;
- onAuthorizationRequired: () => void;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { state: credentials } = useSessionState();
- const token =
- credentials.status !== "loggedIn" ? undefined : credentials.token;
- const { bank: api } = useBankCoreApiContext();
-
- const [current, setCurrent] = useState<string | undefined>();
- const [password, setPassword] = useState<string | undefined>();
- const [repeat, setRepeat] = useState<string | undefined>();
- const [, updateBankState] = useBankState();
-
- const accountIsTheCurrentUser =
- credentials.status === "loggedIn"
- ? credentials.username === accountName
- : false;
-
- const errors = undefinedIfEmpty({
- current: !accountIsTheCurrentUser
- ? undefined
- : !current
- ? i18n.str`Required`
- : undefined,
- password: !password ? i18n.str`Required` : undefined,
- repeat: !repeat
- ? i18n.str`Required`
- : password !== repeat
- ? i18n.str`Repeated password doesn't match`
- : undefined,
- });
- const [notification, notify, handleError] = useLocalNotification();
-
- async function doChangePassword() {
- if (!!errors || !password || !token) return;
- await handleError(async () => {
- const request = {
- old_password: current,
- new_password: password,
- };
- 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.Accepted: {
- updateBankState("currentChallenge", {
- operation: "update-password",
- id: String(resp.body.challenge_id),
- location: routeHere.url({ account: accountName }),
- sent: AbsoluteTime.never(),
- request,
- });
- return onAuthorizationRequired();
- }
- default:
- assertUnreachable(resp);
- }
- }
- });
- }
-
- return (
- <Fragment>
- <LocalNotificationBanner notification={notification} />
- {accountIsTheCurrentUser ? (
- <ProfileNavigation current="credentials"
- routeMyAccountCashout={routeMyAccountCashout}
- routeMyAccountDelete={routeMyAccountDelete}
- routeMyAccountDetails={routeMyAccountDetails}
- routeMyAccountPassword={routeMyAccountPassword}
- routeConversionConfig={routeConversionConfig}
- />
- ) : (
- <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">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- <i18n.Translate>Update password</i18n.Translate>
- </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();
- }}
- >
- <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">
- {accountIsTheCurrentUser ? (
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="password"
- >
- {i18n.str`Current password`}
- </label>
- <div class="mt-2">
- <input
- type="password"
- 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="current"
- id="current-password"
- data-error={!!errors?.current && current !== undefined}
- value={current ?? ""}
- onChange={(e) => {
- setCurrent(e.currentTarget.value);
- }}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.current}
- isDirty={current !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>
- Your current password, for security
- </i18n.Translate>
- </p>
- </div>
- ) : undefined}
-
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="password"
- >
- {i18n.str`New password`}
- </label>
- <div class="mt-2">
- <input
- ref={focus ? doAutoFocus : undefined}
- type="password"
- 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?.password && password !== undefined}
- value={password ?? ""}
- onChange={(e) => {
- setPassword(e.currentTarget.value);
- }}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- </div>
- </div>
-
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="repeat"
- >
- {i18n.str`Type it again`}
- </label>
- <div class="mt-2">
- <input
- type="password"
- 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="repeat"
- id="repeat"
- data-error={!!errors?.repeat && repeat !== undefined}
- value={repeat ?? ""}
- onChange={(e) => {
- setRepeat(e.currentTarget.value);
- }}
- // placeholder=""
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.repeat}
- isDirty={repeat !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Repeat the same password</i18n.Translate>
- </p>
- </div>
-
- </div>
- </div>
- <div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
- <a
- href={routeClose.url({})}
- name="cancel"
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- name="change"
- 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();
- }}
- >
- <i18n.Translate>Change</i18n.Translate>
- </button>
- </div>
- </form>
- </div>
- </Fragment>
- );
-}
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx
deleted file mode 100644
index bce7afe11..000000000
--- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx
+++ /dev/null
@@ -1,901 +0,0 @@
-/*
- 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 { useSessionState } from "../../hooks/session.js";
-import {
- ErrorMessageMappingFor,
- TanChannel,
- undefinedIfEmpty,
- validateIBAN,
- validateTalerBank,
-} from "../../utils.js";
-import { InputAmount, TextField, doAutoFocus } from "../PaytoWireTransferForm.js";
-import { getRandomPassword } from "../rnd.js";
-
-const EMAIL_REGEX =
- /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
-
-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";
-};
-
-type ChangeByPurposeType = {
- 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
- */
-export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({
- template,
- username,
- purpose,
- onChange,
- focus,
- children,
-}: {
- focus?: boolean;
- children: ComponentChildren;
- username?: string;
- template: TalerCorebankApi.AccountData | undefined;
- onChange: ChangeByPurposeType[PurposeType];
- purpose: PurposeType;
-}): VNode {
- const { config, hints, url } = useBankCoreApiContext();
- const { i18n } = useTranslationContext();
- const { state: credentials } = useSessionState();
- const [form, setForm] = useState<AccountFormData>({});
-
- const [errors, setErrors] = useState<
- ErrorMessageMappingFor<typeof defaultValue> | undefined
- >(undefined);
-
- const paytoType = config.wire_type === "X_TALER_BANK" ? "x-taler-bank" as const : "iban" as const;
- const cashoutPaytoType: typeof paytoType = "iban" as const;
-
- const defaultValue: AccountFormData = {
- debit_threshold: Amounts.stringifyValue(
- template?.debit_threshold ?? config.default_debit_threshold,
- ),
- isExchange: template?.is_taler_exchange,
- isPublic: template?.is_public,
- name: template?.name ?? "",
- cashout_payto_uri:
- getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ?? ("" as PaytoString),
- payto_uri: getAccountId(paytoType, template?.payto_uri) ?? ("" as PaytoString),
- 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 userIsAdmin =
- credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator;
-
- const editableUsername = purpose === "create";
- const editableName =
- purpose === "create" ||
- (purpose === "update" && (config.allow_edit_name || userIsAdmin));
-
- const isCashoutEnabled = config.allow_conversion;
- const editableCashout =
- (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 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
- : !newForm.cashout_payto_uri ? undefined
- : cashoutPaytoType === "iban" ? validateIBAN(newForm.cashout_payto_uri, i18n) :
- cashoutPaytoType === "x-taler-bank" ? validateTalerBank(newForm.cashout_payto_uri, i18n) :
- undefined,
-
- payto_uri: !newForm.payto_uri
- ? undefined
- : !editableAccount
- ? undefined
- : !newForm.payto_uri ? undefined
- : paytoType === "iban" ? validateIBAN(newForm.payto_uri, i18n) :
- paytoType === "x-taler-bank" ? validateTalerBank(newForm.payto_uri, i18n) :
- undefined,
-
- email: !newForm.email
- ? undefined
- : !EMAIL_REGEX.test(newForm.email)
- ? i18n.str`Doesn't 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)
- ? 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,
- });
- setErrors(errors);
-
- setForm(newForm);
- if (!onChange) return;
-
- if (errors) {
- onChange(undefined);
- } else {
- let cashout;
- if (newForm.cashout_payto_uri) switch (cashoutPaytoType) {
- case "x-taler-bank": {
- cashout = buildPayto("x-taler-bank", url.host, newForm.cashout_payto_uri);
- break;
- }
- case "iban": {
- cashout = buildPayto("iban", newForm.cashout_payto_uri, undefined);
- break;
- }
- default: assertUnreachable(cashoutPaytoType)
- }
- const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout);
- let internal;
- if (newForm.payto_uri) switch (paytoType) {
- case "x-taler-bank": {
- internal = buildPayto("x-taler-bank", url.host, newForm.payto_uri);
- break;
- }
- case "iban": {
- internal = buildPayto("iban", newForm.payto_uri, undefined);
- break;
- }
- default: assertUnreachable(paytoType)
- }
- const internalURI = !internal ? undefined : stringifyPaytoUri(internal);
-
- 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"];
- const result: TalerCorebankApi.RegisterAccountRequest = {
- name: newForm.name!,
- password: getRandomPassword(),
- username: newForm.username!,
- contact_data: undefinedIfEmpty({
- email: !newForm.email ? undefined : newForm.email,
- phone: !newForm.phone ? undefined :newForm.phone,
- }),
- debit_threshold: threshold ?? config.default_debit_threshold,
- cashout_payto_uri: cashoutURI,
- payto_uri: internalURI,
- is_public: newForm.isPublic,
- is_taler_exchange: newForm.isExchange,
- 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"];
-
- const result: TalerCorebankApi.AccountReconfiguration = {
- cashout_payto_uri: cashoutURI,
- contact_data: undefinedIfEmpty({
- email: !newForm.email ? undefined : newForm.email,
- phone: !newForm.phone ? undefined :newForm.phone,
- }),
- debit_threshold: threshold,
- is_public: newForm.isPublic,
- name: newForm.name,
- tan_channel:
- newForm.tan_channel === "remove" ? null : newForm.tan_channel,
- };
- callback(result);
- return;
- }
- case "show": {
- return;
- }
- default: {
- assertUnreachable(purpose);
- }
- }
- }
- }
- return (
- <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();
- }}
- >
- <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"
- for="username"
- >
- {i18n.str`Login username`}
- {editableUsername && <b style={{ color: "red" }}> *</b>}
- </label>
- <div class="mt-2">
- <input
- ref={focus && purpose === "create" ? doAutoFocus : undefined}
- type="text"
- class="block w-full 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="username"
- id="username"
- data-error={!!errors?.username && form.username !== undefined}
- disabled={!editableUsername}
- value={form.username ?? defaultValue.username}
- onChange={(e) => {
- form.username = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- // placeholder=""
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={form.username !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Account id for authentication</i18n.Translate>
- </p>
- </div>
-
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="name"
- >
- {i18n.str`Full name`}
- {editableName && <b style={{ color: "red" }}> *</b>}
- </label>
- <div class="mt-2">
- <input
- type="text"
- class="block w-full 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"
- data-error={!!errors?.name && form.name !== undefined}
- id="name"
- disabled={!editableName}
- value={form.name ?? defaultValue.name}
- onChange={(e) => {
- form.name = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- // placeholder=""
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.name}
- isDirty={form.name !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Name of the account holder</i18n.Translate>
- </p>
- </div>
-
- {purpose === "create" ? undefined :
- <TextField
- id="internal-account"
- label={i18n.str`Internal account`}
- help={
- purpose === "create"
- ? i18n.str`If empty a random account id will be assigned`
- : i18n.str`Share this id to receive bank transfers`
- }
-
- error={errors?.payto_uri}
- onChange={(e) => {
- form.payto_uri = e as PaytoString;
- updateForm(structuredClone(form));
- }}
- rightIcons={<CopyButton
- class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
- getContent={() => form.payto_uri ?? defaultValue.payto_uri ?? ""}
- />}
- value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString}
- disabled={!editableAccount}
- />
- }
-
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="email"
- >
- {i18n.str`Email`}
- </label>
- <div class="mt-2">
- <input
- type="email"
- class="block w-full 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="email"
- id="email"
- data-error={!!errors?.email && form.email !== undefined}
- disabled={purpose === "show"}
- value={form.email ?? defaultValue.email}
- onChange={(e) => {
- form.email = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.email}
- isDirty={form.email !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>To be used when second factor authentication is enabled</i18n.Translate>
- </p>
- </div>
-
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="phone"
- >
- {i18n.str`Phone`}
- </label>
- <div class="mt-2">
- <input
- type="text"
- class="block w-full 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="phone"
- id="phone"
- disabled={purpose === "show"}
- value={form.phone ?? defaultValue.phone}
- data-error={!!errors?.phone && form.phone !== undefined}
- onChange={(e) => {
- form.phone = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.phone}
- isDirty={form.phone !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>To be used when second factor authentication is enabled</i18n.Translate>
- </p>
- </div>
-
- {isCashoutEnabled && (
- <TextField
- id="cashout-account"
- label={i18n.str`Cashout account`}
- help={i18n.str`External account number where the money is going to be sent when doing cashouts`}
- error={errors?.cashout_payto_uri}
- onChange={(e) => {
- form.cashout_payto_uri = e as PaytoString;
- updateForm(structuredClone(form));
- }}
- value={(form.cashout_payto_uri ?? defaultValue.cashout_payto_uri) as PaytoString}
- disabled={!editableCashout}
- />
- )}
-
- {/* channel, not shown if old cashout api */}
- {OLD_CASHOUT_API ||
- config.supported_tan_channels.length === 0 ? undefined : (
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="channel"
- >
- {i18n.str`Enable second factor authentication`}
- </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;
- }
- 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"
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span
- id="project-type-0-label"
- class="block text-sm font-medium text-gray-900 "
- >
- <i18n.Translate>Using email</i18n.Translate>
- </span>
- {purpose !== "show" &&
- !hasEmail &&
- i18n.str`Add an 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>
- </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;
- }
- 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"
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span
- id="project-type-1-label"
- class="block text-sm font-medium text-gray-900"
- >
- <i18n.Translate>Using SMS</i18n.Translate>
- </span>
- {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>
- </label>
- )}
- </div>
- </div>
- </div>
- )}
-
- <div class="sm:col-span-5">
- <label
- for="debit"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Max debt`}</label>
- <InputAmount
- name="debit"
- left
- currency={config.currency}
- value={form.debit_threshold ?? defaultValue.debit_threshold}
- onChange={
- !editableThreshold
- ? undefined
- : (e) => {
- form.debit_threshold = e as AmountString;
- updateForm(structuredClone(form));
- }
- }
- />
- <ShowInputErrorLabel
- message={
- errors?.debit_threshold
- ? String(errors?.debit_threshold)
- : undefined
- }
- isDirty={form.debit_threshold !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>How much the balance can go below zero.</i18n.Translate>
- </p>
- </div>
-
- <div class="sm:col-span-5">
- <div class="flex items-center justify-between">
- <span class="flex flex-grow flex-col">
- <span
- 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"
- name="is public"
- 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 ?? defaultValue.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 accessible</i18n.Translate>
- </p>
- </div>
-
- {purpose !== "create" || !userIsAdmin ? undefined : (
- <div class="sm:col-span-5">
- <div class="flex items-center justify-between">
- <span class="flex flex-grow flex-col">
- <span
- class="text-sm text-black font-medium leading-6 "
- id="availability-label"
- >
- <i18n.Translate>Is this account a payment provider?</i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- name="is exchange"
- data-enabled={
- form.isExchange ?? defaultValue.isExchange
- ? "true"
- : "false"
- }
- class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
- role="switch"
- aria-checked="false"
- aria-labelledby="availability-label"
- aria-describedby="availability-description"
- onClick={() => {
- form.isExchange = !form.isExchange;
- updateForm(structuredClone(form));
- }}
- >
- <span
- aria-hidden="true"
- data-enabled={
- form.isExchange ?? defaultValue.isExchange
- ? "true"
- : "false"
- }
- class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
- ></span>
- </button>
- </div>
- </div>
- )}
- </div>
- </div>
- {children}
- </form>
- );
-}
-
-function getAccountId(type: "iban" | "x-taler-bank", s: PaytoString | undefined): string | undefined {
- if (s === undefined) return undefined;
- const p = parsePaytoUri(s);
- if (p === undefined) return undefined;
- if (!p.isKnown) return "<unknown>";
- if (type === "iban" && p.targetType === "iban") return p.iban;
- if (type === "x-taler-bank" && p.targetType === "x-taler-bank") return p.account;
- return "<unsupported>";
-}
-
-{
- /* <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="cashout"
- >
- {}
- </label>
- <div class="mt-2">
- <input
- type="text"
- ref={focus && purpose === "update" ? doAutoFocus : undefined}
- data-error={!!errors?.cashout_payto_uri && form.cashout_payto_uri !== undefined}
- class="block w-full 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="cashout"
- id="cashout"
- disabled={purpose === "show"}
- value={form.cashout_payto_uri ?? defaultValue.cashout_payto_uri}
- onChange={(e) => {
- form.cashout_payto_uri = e.currentTarget.value as PaytoString;
- if (!form.cashout_payto_uri) {
- form.cashout_payto_uri = undefined
- }
- updateForm(structuredClone(form));
- }}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.cashout_payto_uri}
- isDirty={form.cashout_payto_uri !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500" >
- <i18n.Translate></i18n.Translate>
- </p>
- </div> */
-}
-
-// function PaytoField({
-// name,
-// label,
-// help,
-// type,
-// value,
-// disabled,
-// onChange,
-// error,
-// }: {
-// error: TranslatedString | undefined;
-// name: string;
-// label: TranslatedString;
-// help: TranslatedString;
-// onChange: (s: string) => void;
-// type: "iban" | "x-taler-bank" | "bitcoin";
-// disabled?: boolean;
-// value: string | undefined;
-// }): VNode {
-// if (type === "iban") {
-// return (
-// <div class="sm:col-span-5">
-// <label
-// class="block text-sm font-medium leading-6 text-gray-900"
-// for={name}
-// >
-// {label}
-// </label>
-// <div class="mt-2">
-// <div class="flex justify-between">
-// <input
-// type="text"
-// class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
-// name={name}
-// id={name}
-// disabled={disabled}
-// value={value ?? ""}
-// onChange={(e) => {
-// onChange(e.currentTarget.value);
-// }}
-// />
-// <CopyButton
-// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
-// getContent={() => value ?? ""}
-// />
-// </div>
-// <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
-// </div>
-// <p class="mt-2 text-sm text-gray-500">{help}</p>
-// </div>
-// );
-// }
-// if (type === "x-taler-bank") {
-// return (
-// <div class="sm:col-span-5">
-// <label
-// class="block text-sm font-medium leading-6 text-gray-900"
-// for={name}
-// >
-// {label}
-// </label>
-// <div class="mt-2">
-// <div class="flex justify-between">
-// <input
-// type="text"
-// class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
-// name={name}
-// id={name}
-// disabled={disabled}
-// value={value ?? ""}
-// onChange={(e) => {
-// onChange(e.currentTarget.value);
-// }}
-// />
-// <CopyButton
-// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
-// getContent={() => value ?? ""}
-// />
-// </div>
-// <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
-// </div>
-// <p class="mt-2 text-sm text-gray-500">
-// {help}
-// </p>
-// </div>
-// );
-// }
-// if (type === "bitcoin") {
-// return (
-// <div class="sm:col-span-5">
-// <label
-// class="block text-sm font-medium leading-6 text-gray-900"
-// for={name}
-// >
-// {label}
-// </label>
-// <div class="mt-2">
-// <div class="flex justify-between">
-// <input
-// type="text"
-// class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
-// name={name}
-// id={name}
-// disabled={disabled}
-// value={value ?? ""}
-// />
-// <CopyButton
-// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
-// getContent={() => value ?? ""}
-// />
-// <ShowInputErrorLabel
-// message={error}
-// isDirty={value !== undefined}
-// />
-// </div>
-// </div>
-// <p class="mt-2 text-sm text-gray-500">
-// {/* <i18n.Translate>bitcoin address</i18n.Translate> */}
-// {help}
-// </p>
-// </div>
-// );
-// }
-// assertUnreachable(type);
-// }
diff --git a/packages/demobank-ui/src/pages/admin/AccountList.tsx b/packages/demobank-ui/src/pages/admin/AccountList.tsx
deleted file mode 100644
index 4e465d4b5..000000000
--- a/packages/demobank-ui/src/pages/admin/AccountList.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- 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/regional.js";
-import { RenderAmount } from "../PaytoWireTransferForm.js";
-import { RouteDefinition } from "../../route.js";
-
-interface Props {
- routeCreate: RouteDefinition;
-
- routeShowAccount: RouteDefinition<{ account: string }>;
- routeRemoveAccount: RouteDefinition<{ account: string }>;
- routeUpdatePasswordAccount: RouteDefinition<{ account: string }>;
- routeShowCashoutsAccount: RouteDefinition<{ account: string }>;
-}
-
-export function AccountList({
- routeCreate,
- routeRemoveAccount,
- routeShowAccount,
- routeShowCashoutsAccount,
- routeUpdatePasswordAccount,
-}: Props): VNode {
- const result = useBusinessAccounts();
- const { i18n } = useTranslationContext();
- const { config } = useBankCoreApiContext();
-
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- return <ErrorLoadingWithDebug error={result} />;
- }
- if (result.data.type === "fail") {
- switch (result.data.case) {
- case HttpStatusCode.Unauthorized:
- return <Fragment />;
- default:
- assertUnreachable(result.data.case);
- }
- }
-
- const onGoStart = result.isFirstPage ? undefined : result.loadFirst
- const onGoNext = result.isLastPage ? undefined : result.loadNext
-
- const accounts = result.result;
- 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 bank accounts.
- </i18n.Translate>
- </p>
- </div>
- <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
- <a
- href={routeCreate.url({})}
- name="create account"
- 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 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>
- {/* FIXME: ADD empty list */}
- </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
- name={`show account ${item.username}`}
- 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
- name={`update password ${item.username}`}
- href={routeUpdatePasswordAccount.url({
- account: item.username,
- })}
- class="text-indigo-600 hover:text-indigo-900"
- >
- <i18n.Translate>Change password</i18n.Translate>
- </a>
- <br />
- {/* {config.allow_conversion ?
- <Fragment>
-
- <a
- name={`show cashout ${item.username}`}
- href={routeShowCashoutsAccount.url({
- account: item.username,
- })}
- class="text-indigo-600 hover:text-indigo-900"
- >
- <i18n.Translate>Cashouts</i18n.Translate>
- </a>
- <br />
- </Fragment>
- : undefined} */}
- {noBalance ? (
- <a
- name={`remove account ${item.username}`}
- 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>
- <nav
- class="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 rounded-lg"
- aria-label="Pagination"
- >
- <div class="flex flex-1 justify-between sm:justify-end">
- <button
- name="first page"
- class="relative disabled:bg-gray-100 disabled:text-gray-500 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
- disabled={!onGoStart}
- onClick={onGoStart}
- >
- <i18n.Translate>First page</i18n.Translate>
- </button>
- <button
- name="next page"
- class="relative disabled:bg-gray-100 disabled:text-gray-500 ml-3 inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:outline-offset-0"
- disabled={!onGoNext}
- onClick={onGoNext}
- >
- <i18n.Translate>Next</i18n.Translate>
- </button>
- </div>
- </nav>
-
- </div>
- </div>
- </div>
- </Fragment>
- );
-}
diff --git a/packages/demobank-ui/src/pages/admin/AdminHome.tsx b/packages/demobank-ui/src/pages/admin/AdminHome.tsx
deleted file mode 100644
index 752d86aa6..000000000
--- a/packages/demobank-ui/src/pages/admin/AdminHome.tsx
+++ /dev/null
@@ -1,541 +0,0 @@
-/*
- 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 { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
-import { Transactions } from "../../components/Transactions/index.js";
-import { useBankCoreApiContext } from "../../context/config.js";
-import { useConversionInfo, useLastMonitorInfo } from "../../hooks/regional.js";
-import { RouteDefinition } from "../../route.js";
-import { RenderAmount } from "../PaytoWireTransferForm.js";
-import { WireTransfer } from "../WireTransfer.js";
-import { AccountList } from "./AccountList.js";
-
-/**
- * Query account information and show QR code if there is pending withdrawal
- */
-interface Props {
- routeCreate: RouteDefinition;
- routeDownloadStats: RouteDefinition;
- routeCreateWireTransfer: RouteDefinition<{
- account?: string,
- subject?: string,
- amount?: string,
- }>;
-
- routeShowAccount: RouteDefinition<{ account: string }>;
- routeRemoveAccount: RouteDefinition<{ account: string }>;
- routeUpdatePasswordAccount: RouteDefinition<{ account: string }>;
- routeShowCashoutsAccount: RouteDefinition<{ account: string }>;
- onAuthorizationRequired: () => void;
-}
-export function AdminHome({
- routeCreate,
- routeRemoveAccount,
- routeShowAccount,
- routeShowCashoutsAccount,
- routeUpdatePasswordAccount,
- routeDownloadStats,
- routeCreateWireTransfer,
- onAuthorizationRequired,
-}: Props): VNode {
- return (
- <Fragment>
- <Metrics routeDownloadStats={routeDownloadStats} />
- <WireTransfer routeHere={routeCreateWireTransfer} onAuthorizationRequired={onAuthorizationRequired} />
-
- <Transactions
- account="admin"
- routeCreateWireTransfer={routeCreateWireTransfer}
- />
- <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();
- 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 });
- }
- assertUnreachable(timeframe);
-}
-
-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);
- }
-}
-
-function Metrics({
- routeDownloadStats,
-}: {
- routeDownloadStats: RouteDefinition;
-}): VNode {
- 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 resp = useLastMonitorInfo(params.current, params.previous, metricType);
- if (!resp) return <Fragment />;
- if (resp instanceof TalerError) {
- return <ErrorLoadingWithDebug error={resp} />;
- }
- if (!respInfo) return <Fragment />;
- if (respInfo instanceof TalerError) {
- return <ErrorLoadingWithDebug error={respInfo} />;
- }
- if (respInfo.type === "fail") {
- switch (respInfo.case) {
- case HttpStatusCode.NotImplemented: {
- return (
- <Attention type="danger" title={i18n.str`Cashout are disabled`}>
- <i18n.Translate>
- Cashout should be enable by configuration and the conversion rate
- should be initialized with fee, ratio and rounding mode.
- </i18n.Translate>
- </Attention>
- );
- }
- default:
- assertUnreachable(respInfo.case);
- }
- }
-
- if (resp.current.type !== "ok" || resp.previous.type !== "ok") {
- 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 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>Previous 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">
- {/* FIXME: This should be LINKS */}
- <nav
- class="isolate flex divide-x divide-gray-200 rounded-lg shadow"
- aria-label="Tabs"
- >
- <button
- type="button"
- name="set last hour"
- 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"
- name="set previous day"
- 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>Previous 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"
- name="set last month"
- 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"
- name="set last year"
- 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>
-
- <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>
- <div class="text-xs text-gray-500">
- <i18n.Translate>Transferred from an external account to an account in this bank.</i18n.Translate>
- </div>
- </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>
- <div class="text-xs text-gray-500">
- <i18n.Translate>Transferred from an account in this bank to an external account.</i18n.Translate>
- </div>
- <MetricValue
- current={resp.current.body.cashoutFiatVolume}
- previous={resp.previous.body.cashoutFiatVolume}
- 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>
- <div class="text-xs text-gray-500">
- <i18n.Translate>Transferred from an account to a Taler exchange.</i18n.Translate>
- </div>
- </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>
- <div class="text-xs text-gray-500">
- <i18n.Translate>Transferred from a Taler exchange to another account.</i18n.Translate>
- </div>
- </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={routeDownloadStats.url({})}
- name="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>
- );
-}
-
-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 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">
- <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>
- )}
-
- {negative ? (
- <span class="sr-only">
- <i18n.Translate>Decreased 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
deleted file mode 100644
index 38119735e..000000000
--- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- 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 { useBankCoreApiContext } from "../../context/config.js";
-import { useSessionState } from "../../hooks/session.js";
-import { RouteDefinition } from "../../route.js";
-import { AccountForm } from "./AccountForm.js";
-
-export function CreateNewAccount({
- routeCancel,
- onCreateSuccess,
-}: {
- routeCancel: RouteDefinition;
- onCreateSuccess: () => void;
-}): VNode {
- const { i18n } = useTranslationContext();
- const { state: credentials } = useSessionState();
- const token =
- credentials.status !== "loggedIn" ? undefined : credentials.token;
- const { bank: api } = useBankCoreApiContext();
-
- const [submitAccount, setSubmitAccount] = useState<
- TalerCorebankApi.RegisterAccountRequest | undefined
- >();
- const [notification, notify, handleError] = useLocalNotification();
-
- async function doCreate() {
- if (!submitAccount || !token) return;
- await handleError(async () => {
- const resp = await api.createAccount(token, submitAccount);
- if (resp.type === "ok") {
- notifyInfo(
- i18n.str`Account created with password "${submitAccount.password}".`,
- );
- 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);
- }
- }
- });
- }
-
- if (!(credentials.status === "loggedIn" && credentials.isUserAdministrator)) {
- 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({})}
- name="close"
- 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 (
- <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} />
-
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- <i18n.Translate>New bank account</i18n.Translate>
- </h2>
- </div>
- <AccountForm
- template={undefined}
- purpose="create"
- 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">
- <a
- href={routeCancel.url({})}
- name="cancel"
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- name="create"
- 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();
- }}
- >
- <i18n.Translate>Create</i18n.Translate>
- </button>
- </div>
- </AccountForm>
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/pages/admin/DownloadStats.tsx b/packages/demobank-ui/src/pages/admin/DownloadStats.tsx
deleted file mode 100644
index fba366676..000000000
--- a/packages/demobank-ui/src/pages/admin/DownloadStats.tsx
+++ /dev/null
@@ -1,585 +0,0 @@
-/*
- 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 {
- 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 { useSessionState } from "../../hooks/session.js";
-import { EmptyObject, RouteDefinition } from "../../route.js";
-import { getTimeframesForDate } from "./AdminHome.js";
-
-interface Props {
- routeCancel: RouteDefinition;
-}
-
-type Options = {
- dayMetric: boolean;
- hourMetric: boolean;
- monthMetric: boolean;
- yearMetric: boolean;
- compareWithPrevious: boolean;
- endOnFirstFail: boolean;
- includeHeader: boolean;
-};
-
-/**
- * Show histories of public accounts.
- */
-export function DownloadStats({ routeCancel }: Props): VNode {
- const { i18n } = useTranslationContext();
-
- const { state: credentials } = useSessionState();
- const creds =
- credentials.status !== "loggedIn" || !credentials.isUserAdministrator
- ? undefined
- : credentials;
- const { bank: api } = useBankCoreApiContext();
-
- const [options, setOptions] = useState<Options>({
- compareWithPrevious: true,
- dayMetric: true,
- endOnFirstFail: false,
- hourMetric: true,
- 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, , handleError] = useLocalNotification();
-
- if (!creds) {
- 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} />
-
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- <i18n.Translate>Download bank stats</i18n.Translate>
- </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();
- }}
- >
- <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">
- <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>Include hour metric</i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- name={`hour switch`}
- 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"
- >
- <i18n.Translate>Include day metric</i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- name={`day switch`}
- 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"
- >
- <i18n.Translate>Include month metric</i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- name={`month switch`}
- 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"
- >
- <i18n.Translate>Include year metric</i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- name={`year switch`}
- 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"
- >
- <i18n.Translate>Include table header</i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- name={`header switch`}
- 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>
- </span>
- <button
- type="button"
- name={`compare switch`}
- 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"
- >
- <i18n.Translate>Fail on first error</i18n.Translate>
- </span>
- </span>
- <button
- type="button"
- name={`fail switch`}
- 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">
- <a name="cancel"
- href={routeCancel.url({})}
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- name="download"
- 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);
- await handleError(async () => {
- 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>
- </div>
- </div>
- </div>
- )}
- {!downloaded ? (
- <div class="h-5 mb-5" />
- ) : (
- <a
- href={
- "data:text/plain;charset=utf-8," + encodeURIComponent(downloaded)
- }
- name="save file"
- download={"bank-stats.csv"}
- >
- <Attention title={i18n.str`Download completed`}>
- <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[],
- progress: (current: number, total: number) => void,
-): Promise<string> {
- const allMetrics: TalerCorebankApi.MonitorTimeframeParam[] = [];
- if (options.hourMetric) {
- allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.hour);
- }
- if (options.dayMetric) {
- allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.day);
- }
- if (options.monthMetric) {
- allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.month);
- }
- if (options.yearMetric) {
- allMetrics.push(TalerCorebankApi.MonitorTimeframeParam.year);
- }
-
- /**
- * convert request into frames
- */
- 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;
- progress(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,
- });
-
- 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>),
- );
- progress(total, total);
-
- /**
- * convert into table format
- *
- */
- const table: Array<string[]> = [];
- if (options.includeHeader) {
- table.push([
- "date",
- "metric",
- "reference",
- "talerInCount",
- "talerInVolume",
- "talerOutCount",
- "talerOutVolume",
- "cashinCount",
- "cashinFiatVolume",
- "cashinRegionalVolume",
- "cashoutCount",
- "cashoutFiatVolume",
- "cashoutRegionalVolume",
- ]);
- }
- Object.entries(allInfo).forEach(([name, data]) => {
- if (data.current) {
- const row: TableRow = {
- date: data.reference.getTime(),
- metric: name,
- reference: "current",
- ...dataToRow(data.current),
- };
- table.push(Object.values(row) as string[]);
- }
-
- if (data.previous) {
- const row: TableRow = {
- date: data.reference.getTime(),
- metric: name,
- reference: "previous",
- ...dataToRow(data.previous),
- };
- table.push(Object.values(row) as string[]);
- }
- });
-
- const csv = table.reduce((acc, row) => {
- return acc + row.join(",") + "\n";
- }, "");
-
- return csv;
-}
-
-type JustData = Omit<Omit<Omit<TableRow, "metric">, "date">, "reference">;
-function dataToRow(info: TalerCorebankApi.MonitorResponse): JustData {
- return {
- talerInCount: info.talerInCount,
- talerInVolume: info.talerInVolume,
- 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,
- };
-}
-
-type Data = {
- reference: Date;
- previous: TalerCorebankApi.MonitorResponse | undefined;
- current: TalerCorebankApi.MonitorResponse | undefined;
-};
-type TableRow = {
- date: number;
- metric: string;
- reference: "current" | "previous";
- cashinCount?: number;
- cashinRegionalVolume?: AmountString;
- cashinFiatVolume?: AmountString;
- cashoutCount?: number;
- cashoutRegionalVolume?: AmountString;
- cashoutFiatVolume?: AmountString;
- talerInCount: number;
- talerInVolume: AmountString;
- talerOutCount: number;
- talerOutVolume: AmountString;
-};
diff --git a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx b/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
deleted file mode 100644
index 61def9a95..000000000
--- a/packages/demobank-ui/src/pages/admin/RemoveAccount.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- 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";
-import { useBankCoreApiContext } from "../../context/config.js";
-import { useAccountDetails } from "../../hooks/account.js";
-import { useSessionState } from "../../hooks/session.js";
-import { undefinedIfEmpty } from "../../utils.js";
-import { LoginForm } from "../LoginForm.js";
-import { doAutoFocus } from "../PaytoWireTransferForm.js";
-import { useBankState } from "../../hooks/bank-state.js";
-import { RouteDefinition } from "../../route.js";
-
-export function RemoveAccount({
- account,
- routeCancel,
- onUpdateSuccess,
- onAuthorizationRequired,
- focus,
- routeHere,
-}: {
- focus?: boolean;
- routeHere: RouteDefinition<{ account: string }>;
- onAuthorizationRequired: () => void;
- routeCancel: RouteDefinition;
- onUpdateSuccess: () => void;
- account: string;
-}): VNode {
- const { i18n } = useTranslationContext();
- const result = useAccountDetails(account);
- const [accountName, setAccountName] = useState<string | undefined>();
-
- const { state } = useSessionState();
- const token = state.status !== "loggedIn" ? undefined : state.token;
- const { bank: api } = useBankCoreApiContext();
- const [notification, notify, handleError] = useLocalNotification();
- const [, updateBankState] = useBankState();
-
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- 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);
- }
- }
-
- const balance = Amounts.parse(result.body.balance.amount);
- if (!balance) {
- return <div>there was an error reading the balance</div>;
- }
- const isBalanceEmpty = Amounts.isZero(balance);
- if (!isBalanceEmpty) {
- 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({})}
- name="close"
- 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() {
- if (!token) return;
- await handleError(async () => {
- const resp = await api.deleteAccount({ username: account, token });
- if (resp.type === "ok") {
- notifyInfo(i18n.str`Account removed`);
- 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.Accepted: {
- updateBankState("currentChallenge", {
- operation: "delete-account",
- id: String(resp.body.challenge_id),
- sent: AbsoluteTime.never(),
- location: routeHere.url({ account }),
- request: account,
- });
- return onAuthorizationRequired();
- }
- default: {
- assertUnreachable(resp);
- }
- }
- }
- });
- }
-
- const errors = undefinedIfEmpty({
- accountName: !accountName
- ? i18n.str`Required`
- : account !== accountName
- ? i18n.str`Name doesn't match`
- : undefined,
- });
-
- return (
- <div>
- <LocalNotificationBanner notification={notification} />
-
- <Attention
- type="warning"
- title={i18n.str`You are going to remove the account`}
- >
- <i18n.Translate>This step can't be undone.</i18n.Translate>
- </Attention>
-
- <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>Deleting account "{account}"</i18n.Translate>
- </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();
- }}
- >
- <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"
- for="password"
- >
- {i18n.str`Verification`}
- </label>
- <div class="mt-2">
- <input
- ref={focus ? doAutoFocus : undefined}
- 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 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
- }
- value={accountName ?? ""}
- onChange={(e) => {
- setAccountName(e.currentTarget.value);
- }}
- placeholder={account}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.accountName}
- 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>
- </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">
- <a
- href={routeCancel.url({})}
- name="cancel"
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- name="delete"
- 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();
- }}
- >
- <i18n.Translate>Delete</i18n.Translate>
- </button>
- </div>
- </form>
- </div>
- </div>
- );
-}
diff --git a/packages/demobank-ui/src/pages/index.stories.tsx b/packages/demobank-ui/src/pages/index.stories.tsx
deleted file mode 100644
index 823def5d7..000000000
--- a/packages/demobank-ui/src/pages/index.stories.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- 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/>
- */
-
-export * as qr from "./QrCodeSection.stories.js";
-export * as po from "./PaymentOptions.stories.js";
-export * as ptf from "./PaytoWireTransferForm.stories.js";
-export * as frame from "./BankFrame.stories.js";
diff --git a/packages/demobank-ui/src/pages/regional/ConversionConfig.tsx b/packages/demobank-ui/src/pages/regional/ConversionConfig.tsx
deleted file mode 100644
index 8845ec9a0..000000000
--- a/packages/demobank-ui/src/pages/regional/ConversionConfig.tsx
+++ /dev/null
@@ -1,978 +0,0 @@
-/*
- 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 {
- AmountJson,
- Amounts,
- HttpStatusCode,
- TalerBankConversionApi,
- TalerError,
- TranslatedString,
- assertUnreachable
-} from "@gnu-taler/taler-util";
-import {
- Attention,
- InternationalizationAPI,
- LocalNotificationBanner,
- ShowInputErrorLabel,
- useLocalNotification,
- useTranslationContext,
- utils
-} from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
-import { useEffect, useState } from "preact/hooks";
-import { useBankCoreApiContext } from "../../context/config.js";
-import { useSessionState } from "../../hooks/session.js";
-import { TransferCalculation, useCashinEstimator, useCashoutEstimator, useConversionInfo } from "../../hooks/regional.js";
-import { RouteDefinition } from "../../route.js";
-import { undefinedIfEmpty } from "../../utils.js";
-import { InputAmount, RenderAmount } from "../PaytoWireTransferForm.js";
-import { ProfileNavigation } from "../ProfileNavigation.js";
-import { FormErrors, FormStatus, FormValues, RecursivePartial, UIField, useFormState } from "../../hooks/form.js";
-
-interface Props {
- routeMyAccountDetails: RouteDefinition;
- routeMyAccountDelete: RouteDefinition;
- routeMyAccountPassword: RouteDefinition;
- routeMyAccountCashout: RouteDefinition;
- routeConversionConfig: RouteDefinition;
- routeCancel: RouteDefinition;
- onUpdateSuccess: () => void;
-}
-
-type FormType = { amount: AmountJson, conv: TalerBankConversionApi.ConversionRate }
-
-
-function useComponentState({
- onUpdateSuccess,
- routeCancel,
- routeConversionConfig,
- routeMyAccountCashout,
- routeMyAccountDelete,
- routeMyAccountDetails,
- routeMyAccountPassword,
-}: Props): utils.RecursiveState<VNode> {
- const { i18n } = useTranslationContext();
-
- const result = useConversionInfo()
- const info = result && !(result instanceof TalerError) && result.type === "ok" ?
- result.body : undefined;
-
- const { state: credentials } = useSessionState();
- const creds =
- credentials.status !== "loggedIn" || !credentials.isUserAdministrator
- ? undefined
- : credentials;
-
- if (!info) {
- return <i18n.Translate>loading...</i18n.Translate>
- }
-
- if (!creds) {
- return <i18n.Translate>only admin can setup conversion</i18n.Translate>
- }
-
- return () => {
- const { i18n } = useTranslationContext();
-
- const { bank, conversion, config } = useBankCoreApiContext();
-
- const [notification, notify, handleError] = useLocalNotification();
-
- const initalState: FormValues<FormType> = {
- amount: "100",
- conv: {
- cashin_min_amount: info.conversion_rate.cashin_min_amount.split(":")[1],
- cashin_tiny_amount: info.conversion_rate.cashin_tiny_amount.split(":")[1],
- cashin_fee: info.conversion_rate.cashin_fee.split(":")[1],
- cashin_ratio: info.conversion_rate.cashin_ratio,
- cashin_rounding_mode: info.conversion_rate.cashin_rounding_mode,
- cashout_min_amount: info.conversion_rate.cashout_min_amount.split(":")[1],
- cashout_tiny_amount: info.conversion_rate.cashout_tiny_amount.split(":")[1],
- cashout_fee: info.conversion_rate.cashout_fee.split(":")[1],
- cashout_ratio: info.conversion_rate.cashout_ratio,
- cashout_rounding_mode: info.conversion_rate.cashout_rounding_mode,
- }
- }
-
- const [form, status] = useFormState<FormType>(
- initalState,
- createFormValidator(i18n, info.regional_currency, info.fiat_currency)
- )
-
- const {
- estimateByDebit: calculateCashoutFromDebit,
- } = useCashoutEstimator();
-
- const {
- estimateByDebit: calculateCashinFromDebit,
- } = useCashinEstimator();
-
- const [calculationResult, setCalc] = useState<{ cashin: TransferCalculation, cashout: TransferCalculation }>()
-
- useEffect(() => {
- async function doAsync() {
- await handleError(async () => {
- if (!info) return;
- if (!form.amount?.value || form.amount.error) return;
- const in_amount = Amounts.parseOrThrow(`${info.fiat_currency}:${form.amount.value}`)
- const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee)
- const cashin = await calculateCashinFromDebit(in_amount, in_fee);
-
- if (cashin === "amount-is-too-small") {
- setCalc(undefined)
- return;
- }
- // const out_amount = Amounts.parseOrThrow(`${info.regional_currency}:${form.amount.value}`)
- const out_fee = Amounts.parseOrThrow(info.conversion_rate.cashout_fee)
- const cashout = await calculateCashoutFromDebit(cashin.credit, out_fee);
-
- setCalc({ cashin, cashout });
- });
- }
- doAsync();
- }, [form.amount?.value, form.conv?.cashin_fee?.value, form.conv?.cashout_fee?.value]);
-
- const [section, setSection] = useState<"detail" | "cashout" | "cashin">("detail")
- const cashinCalc = calculationResult?.cashin === "amount-is-too-small" ? undefined : calculationResult?.cashin
- const cashoutCalc = calculationResult?.cashout === "amount-is-too-small" ? undefined : calculationResult?.cashout
- async function doUpdate() {
- if (!creds) return
- await handleError(async () => {
- if (status.status === "fail") return;
- const resp = await conversion.updateConversionRate(creds.token, status.result.conv)
- if (resp.type === "ok") {
- setSection("detail")
- } else {
- switch (resp.case) {
- case HttpStatusCode.Unauthorized: {
- return notify({
- type: "error",
- title: i18n.str`Wrong credentials`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- }
- case HttpStatusCode.NotImplemented: {
- return notify({
- type: "error",
- title: i18n.str`Conversion is disabled`,
- description: resp.detail.hint as TranslatedString,
- debug: resp.detail,
- });
- }
- default:
- assertUnreachable(resp);
- }
- }
- });
- }
-
- const in_ratio = Number.parseFloat(info.conversion_rate.cashin_ratio)
- const out_ratio = Number.parseFloat(info.conversion_rate.cashout_ratio)
-
- const both_high = in_ratio > 1 && out_ratio > 1;
- const both_low = in_ratio < 1 && out_ratio < 1;
-
-
- return (
- <div>
- <ProfileNavigation current="conversion"
- routeMyAccountCashout={routeMyAccountCashout}
- routeMyAccountDelete={routeMyAccountDelete}
- routeMyAccountDetails={routeMyAccountDetails}
- routeMyAccountPassword={routeMyAccountPassword}
- routeConversionConfig={routeConversionConfig}
- />
-
- <LocalNotificationBanner notification={notification} />
- <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-10 md:grid-cols-3 bg-gray-100 my-4 px-4 pb-4 rounded-lg">
-
- <div class="px-4 sm:px-0">
- <h2 class="text-base font-semibold leading-7 text-gray-900">
- <i18n.Translate>Conversion</i18n.Translate>
- </h2>
- <div class="px-2 mt-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
- <label
- data-enabled={section === "detail"}
- class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 data-[enabled=true]:ring-indigo-600"
- >
- <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={() => {
- setSection("detail")
- }}
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span class="block text-sm font-medium text-gray-900">
- <i18n.Translate>Details</i18n.Translate>
- </span>
- </span>
- </span>
- </label>
-
- <label
- data-enabled={section === "cashout"}
- class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 -- data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 data-[enabled=true]:ring-indigo-600"
- >
- <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={() => {
- setSection("cashout")
- }}
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span class="block text-sm font-medium text-gray-900">
- <i18n.Translate>Config cashout</i18n.Translate>
- </span>
- </span>
- </span>
- </label>
- <label
- data-enabled={section === "cashin"}
- class="relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none border-gray-300 -- data-[enabled=true]:border-indigo-600 data-[enabled=true]:ring-2 data-[enabled=true]:ring-indigo-600"
- >
- <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={() => {
- setSection("cashin")
- }}
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span class="block text-sm font-medium text-gray-900">
- <i18n.Translate>Config cashin</i18n.Translate>
- </span>
- </span>
- </span>
- </label>
- </div>
-
- </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();
- }}
- >
- {section == "cashin" &&
- <ConversionForm id="cashin"
- inputCurrency={info.fiat_currency}
- outputCurrency={info.regional_currency}
- fee={form?.conv?.cashin_fee}
- minimum={form?.conv?.cashin_min_amount}
- ratio={form?.conv?.cashin_ratio}
- rounding={form?.conv?.cashin_rounding_mode}
- tiny={form?.conv?.cashin_tiny_amount}
- />}
-
- {section == "cashout" && <Fragment>
- <ConversionForm id="cashout"
- inputCurrency={info.regional_currency}
- outputCurrency={info.fiat_currency}
- fee={form?.conv?.cashout_fee}
- minimum={form?.conv?.cashout_min_amount}
- ratio={form?.conv?.cashout_ratio}
- rounding={form?.conv?.cashout_rounding_mode}
- tiny={form?.conv?.cashout_tiny_amount}
- />
- </Fragment>}
-
- {section == "detail" && <Fragment>
- <div class="px-6 pt-6">
- <div class="justify-between items-center flex ">
- <dt class="text-sm text-gray-600">
- <i18n.Translate>Cashin ratio</i18n.Translate>
- </dt>
- <dd class="text-sm text-gray-900">
- {info.conversion_rate.cashin_ratio}
- </dd>
- </div>
- </div>
-
- <div class="px-6 pt-6">
- <div class="justify-between items-center flex ">
- <dt class="text-sm text-gray-600">
- <i18n.Translate>Cashout ratio</i18n.Translate>
- </dt>
- <dd class="text-sm text-gray-900">
- {info.conversion_rate.cashout_ratio}
- </dd>
- </div>
- </div>
-
- {both_low || both_high ? <div class="p-4">
- <Attention title={i18n.str`Bad ratios`} type="warning">
- <i18n.Translate>
- One of the ratios should be higher or equal than 1 an the other should be lower or equal than 1.
- </i18n.Translate>
- </Attention>
- </div> : undefined}
-
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- for="amount"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Initial amount`}</label>
- <InputAmount
- name="amount"
- left
- currency={info.fiat_currency}
- value={form.amount?.value ?? ""}
- onChange={form.amount?.onUpdate}
- />
- <ShowInputErrorLabel
- message={form.amount?.error}
- isDirty={form.amount?.value !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Use it to test how the conversion will affect the amount.</i18n.Translate>
- </p>
- </div>
- </div>
- </div>
-
- {!cashoutCalc || !cashinCalc ? undefined : (
- <div class="px-6 pt-6">
- <div class="sm:col-span-5">
- <dl class="mt-4 space-y-4">
- <div class="justify-between items-center flex ">
- <dt class="text-sm text-gray-600">
- <i18n.Translate>Sending to this bank</i18n.Translate>
- </dt>
- <dd class="text-sm text-gray-900">
- <RenderAmount
- value={cashinCalc.debit}
- negative
- withColor
- spec={info.regional_currency_specification}
- />
- </dd>
- </div>
-
- {Amounts.isZero(cashinCalc.beforeFee) ? undefined : (
- <div class="flex items-center justify-between afu ">
- <dt class="flex items-center text-sm text-gray-600">
- <span>
- <i18n.Translate>Converted</i18n.Translate>
- </span>
- </dt>
- <dd class="text-sm text-gray-900">
- <RenderAmount
- value={cashinCalc.beforeFee}
- spec={info.fiat_currency_specification}
- />
- </dd>
- </div>
- )}
- <div class="flex justify-between items-center border-t-2 afu pt-4">
- <dt class="text-lg text-gray-900 font-medium">
- <i18n.Translate>Cashin after fee</i18n.Translate>
- </dt>
- <dd class="text-lg text-gray-900 font-medium">
- <RenderAmount
- value={cashinCalc.credit}
- withColor
- spec={info.fiat_currency_specification}
- />
- </dd>
- </div>
- </dl>
- </div>
-
- <div class="sm:col-span-5">
- <dl class="mt-4 space-y-4">
- <div class="justify-between items-center flex ">
- <dt class="text-sm text-gray-600">
- <i18n.Translate>Sending from this bank</i18n.Translate>
- </dt>
- <dd class="text-sm text-gray-900">
- <RenderAmount
- value={cashoutCalc.debit}
- negative
- withColor
- spec={info.fiat_currency_specification}
- />
- </dd>
- </div>
-
- {Amounts.isZero(cashoutCalc.beforeFee) ? undefined : (
- <div class="flex items-center justify-between afu">
- <dt class="flex items-center text-sm text-gray-600">
- <span>
- <i18n.Translate>Converted</i18n.Translate>
- </span>
- </dt>
- <dd class="text-sm text-gray-900">
- <RenderAmount
- value={cashoutCalc.beforeFee}
- spec={info.regional_currency_specification}
- />
- </dd>
- </div>
- )}
- <div class="flex justify-between items-center border-t-2 afu pt-4">
- <dt class="text-lg text-gray-900 font-medium">
- <i18n.Translate>Cashout after fee</i18n.Translate>
- </dt>
- <dd class="text-lg text-gray-900 font-medium">
- <RenderAmount
- value={cashoutCalc.credit}
- withColor
- spec={info.regional_currency_specification}
- />
- </dd>
- </div>
- </dl>
- </div>
-
- {cashoutCalc && status.status === "ok" && Amounts.cmp(status.result.amount, cashoutCalc.credit) < 0 ? <div class="p-4">
- <Attention title={i18n.str`Bad configuration`} type="warning">
- <i18n.Translate>
- This configuration allows users to cash out more of what has been cashed in.
- </i18n.Translate>
- </Attention>
- </div> : undefined}
- </div>
- )}
- </Fragment>}
-
-
- <div class="flex items-center justify-between mt-4 gap-x-6 border-t border-gray-900/10 px-4 py-4">
- <a name="cancel"
- href={routeCancel.url({})}
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- {section == "cashin" || section == "cashout" ? <Fragment>
- <button
- type="submit"
- name="update conversion"
- class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
- onClick={async () => {
- doUpdate()
- }}
- >
- <i18n.Translate>Update</i18n.Translate>
- </button>
- </Fragment> : <div />}
- </div>
-
-
- </form>
- </div>
- </div>
- );
-
- }
-}
-
-export const ConversionConfig = utils.recursive(useComponentState);
-
-/**
- *
- * @param i18n
- * @param regional
- * @param fiat
- * @returns form validator
- */
-function createFormValidator(i18n: InternationalizationAPI, regional: string, fiat: string) {
- return function check(state: FormValues<FormType>): FormStatus<FormType> {
-
- const cashin_min_amount = Amounts.parse(`${fiat}:${state.conv.cashin_min_amount}`)
- const cashin_tiny_amount = Amounts.parse(`${regional}:${state.conv.cashin_tiny_amount}`)
- const cashin_fee = Amounts.parse(`${regional}:${state.conv.cashin_fee}`)
-
- const cashout_min_amount = Amounts.parse(`${regional}:${state.conv.cashout_min_amount}`)
- const cashout_tiny_amount = Amounts.parse(`${fiat}:${state.conv.cashout_tiny_amount}`)
- const cashout_fee = Amounts.parse(`${fiat}:${state.conv.cashout_fee}`)
-
- const am = Amounts.parse(`${fiat}:${state.amount}`)
-
- const cashin_ratio = Number.parseFloat(state.conv.cashin_ratio ?? "")
- const cashout_ratio = Number.parseFloat(state.conv.cashout_ratio ?? "")
-
- const errors = undefinedIfEmpty<FormErrors<FormType>>({
- conv: undefinedIfEmpty<FormErrors<FormType["conv"]>>({
- cashin_min_amount: !state.conv.cashin_min_amount ? i18n.str`required` :
- !cashin_min_amount ? i18n.str`invalid` :
- undefined,
- cashin_tiny_amount: !state.conv.cashin_tiny_amount ? i18n.str`required` :
- !cashin_tiny_amount ? i18n.str`invalid` :
- undefined,
- cashin_fee: !state.conv.cashin_fee ? i18n.str`required` :
- !cashin_fee ? i18n.str`invalid` :
- undefined,
-
- cashout_min_amount: !state.conv.cashout_min_amount ? i18n.str`required` :
- !cashout_min_amount ? i18n.str`invalid` :
- undefined,
- cashout_tiny_amount: !state.conv.cashin_tiny_amount ? i18n.str`required` :
- !cashout_tiny_amount ? i18n.str`invalid` :
- undefined,
- cashout_fee: !state.conv.cashin_fee ? i18n.str`required` :
- !cashout_fee ? i18n.str`invalid` :
- undefined,
-
- cashin_rounding_mode: !state.conv.cashin_rounding_mode ? i18n.str`required` : undefined,
- cashout_rounding_mode: !state.conv.cashout_rounding_mode ? i18n.str`required` : undefined,
-
- cashin_ratio: !state.conv.cashin_ratio ? i18n.str`required` : Number.isNaN(cashin_ratio) ? i18n.str`invalid` : undefined,
- cashout_ratio: !state.conv.cashout_ratio ? i18n.str`required` : Number.isNaN(cashout_ratio) ? i18n.str`invalid` : undefined,
- }),
-
- amount: !state.amount ? i18n.str`required` :
- !am ? i18n.str`invalid` :
- undefined,
- })
-
- const result: RecursivePartial<FormType> = {
- amount: am,
- conv: {
- cashin_fee: !errors?.conv?.cashin_fee ? Amounts.stringify(cashin_fee!) : undefined,
- cashin_min_amount: !errors?.conv?.cashin_min_amount ? Amounts.stringify(cashin_min_amount!) : undefined,
- cashin_ratio: !errors?.conv?.cashin_ratio ? String(cashin_ratio!) : undefined,
- cashin_rounding_mode: !errors?.conv?.cashin_rounding_mode ? (state.conv.cashin_rounding_mode!) : undefined,
- cashin_tiny_amount: !errors?.conv?.cashin_tiny_amount ? Amounts.stringify(cashin_tiny_amount!) : undefined,
- cashout_fee: !errors?.conv?.cashout_fee ? Amounts.stringify(cashout_fee!) : undefined,
- cashout_min_amount: !errors?.conv?.cashout_min_amount ? Amounts.stringify(cashout_min_amount!) : undefined,
- cashout_ratio: !errors?.conv?.cashout_ratio ? String(cashout_ratio!) : undefined,
- cashout_rounding_mode: !errors?.conv?.cashout_rounding_mode ? (state.conv.cashout_rounding_mode!) : undefined,
- cashout_tiny_amount: !errors?.conv?.cashout_tiny_amount ? Amounts.stringify(cashout_tiny_amount!) : undefined,
- }
-
- }
- return errors === undefined ?
- { status: "ok", result: result as FormType, errors } :
- { status: "fail", result, errors }
- }
-}
-
-
-function ConversionForm({ id, inputCurrency, outputCurrency, fee, minimum, ratio, rounding, tiny }: {
- inputCurrency: string,
- outputCurrency: string,
- minimum: UIField | undefined,
- tiny: UIField | undefined,
- fee: UIField | undefined,
- rounding: UIField | undefined,
- ratio: UIField | undefined,
- id: string,
-}): VNode {
- const { i18n } = useTranslationContext();
- return <Fragment>
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- for="cashin_min_amount"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Minimum amount`}</label>
- <InputAmount
- name="cashin_min_amount"
- left
- currency={inputCurrency}
- value={minimum?.value ?? ""}
- onChange={minimum?.onUpdate}
- />
- <ShowInputErrorLabel
- message={minimum?.error}
- isDirty={minimum?.value !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Only cashout operation above this threshold will be allowed</i18n.Translate>
- </p>
- </div>
- </div>
- </div>
-
- <div class="px-6 pt-6">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="password"
- >
- {i18n.str`Ratio`}
- </label>
- <div class="mt-2">
- <input
- type="number"
- class="block rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
- name="current"
- id="cashin_ratio"
- data-error={!!ratio?.error && ratio?.value !== undefined}
- value={ratio?.value ?? ""}
- onChange={(e) => {
- ratio?.onUpdate(e.currentTarget.value);
- }}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={ratio?.error}
- isDirty={ratio?.value !== undefined}
- />
- </div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>
- Conversion ratio between currencies
- </i18n.Translate>
- </p>
- </div>
-
- <div class="px-6 pt-4">
- <Attention title={i18n.str`Example conversion`}>
- <i18n.Translate>1 {inputCurrency} will be converted into {ratio?.value} {outputCurrency}</i18n.Translate>
- </Attention>
- </div>
-
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- for="cashin_tiny_amount"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Rounding value`}</label>
- <InputAmount
- name="cashin_tiny_amount"
- left
- currency={outputCurrency}
- value={tiny?.value ?? ""}
- onChange={tiny?.onUpdate}
- />
- <ShowInputErrorLabel
- message={tiny?.error}
- isDirty={tiny?.value !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Smallest difference between two amounts after the ratio is applied.</i18n.Translate>
- </p>
- </div>
- </div>
- </div>
-
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="channel"
- >
- {i18n.str`Rounding mode`}
- </label>
- <div class="mt-2 max-w-xl text-sm text-gray-500">
- <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
- <label
- onClick={(e) => {
- e.preventDefault();
- rounding?.onUpdate("zero")
- }}
- data-selected={rounding?.value === "zero"}
- class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"
- >
- <input
- type="radio"
- name="channel"
- value="Newsletter"
- class="sr-only"
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span
- id="project-type-0-label"
- class="block text-sm font-medium text-gray-900 "
- >
- <i18n.Translate>Zero</i18n.Translate>
- </span>
- <i18n.Translate>Amount will be round below to the largest possible value smaller than the input.</i18n.Translate>
- </span>
- </span>
- <svg
- data-selected={rounding?.value === "zero"}
- class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- fill-rule="evenodd"
- d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
- clip-rule="evenodd"
- />
- </svg>
- </label>
-
- <label
- onClick={(e) => {
- e.preventDefault();
- rounding?.onUpdate("up")
- }}
- data-selected={rounding?.value === "up"}
- class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"
- >
- <input
- type="radio"
- name="channel"
- value="Existing Customers"
- class="sr-only"
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span
- id="project-type-0-label"
- class="block text-sm font-medium text-gray-900 "
- >
- <i18n.Translate>Up</i18n.Translate>
- </span>
- <i18n.Translate>Amount will be round up to the smallest possible value larger than the input.</i18n.Translate>
- </span>
- </span>
- <svg
- data-selected={rounding?.value === "up"}
- class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- fill-rule="evenodd"
- d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
- clip-rule="evenodd"
- />
- </svg>
- </label>
- <label
- onClick={(e) => {
- e.preventDefault();
- rounding?.onUpdate("nearest")
- }}
- data-selected={rounding?.value === "nearest"}
- class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"
- >
- <input
- type="radio"
- name="channel"
- value="Existing Customers"
- class="sr-only"
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span
- id="project-type-0-label"
- class="block text-sm font-medium text-gray-900 "
- >
- <i18n.Translate>Nearest</i18n.Translate>
- </span>
- <i18n.Translate>Amount will be round to the closest possible value.</i18n.Translate>
- </span>
- </span>
- <svg
- data-selected={rounding?.value === "nearest"}
- class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
- viewBox="0 0 20 20"
- fill="currentColor"
- aria-hidden="true"
- >
- <path
- fill-rule="evenodd"
- d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
- clip-rule="evenodd"
- />
- </svg>
- </label>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <div class="px-6 pt-4">
- <Attention title={i18n.str`Examples`}>
- <section class="grid grid-cols-1 gap-y-3 text-gray-600">
- <details class="group text-sm">
- <summary class="flex cursor-pointer flex-row items-center justify-between ">
- <i18n.Translate>
- Rounding an amount of 1.24 with rounding value 0.1
- </i18n.Translate>
- <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
- </svg>
- </summary>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- Given the rounding value of 0.1 the possible values closest to 1.24 are: 1.1, 1.2, 1.3, 1.4.
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "zero" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "nearest" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 mt-4">
- <i18n.Translate>
- With the "up" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- </details>
- <details class="group ">
- <summary class="flex cursor-pointer flex-row items-center justify-between ">
- <i18n.Translate>
- Rounding an amount of 1.26 with rounding value 0.1
- </i18n.Translate>
- <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
- </svg>
- </summary>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- Given the rounding value of 0.1 the possible values closest to 1.24 are: 1.1, 1.2, 1.3, 1.4.
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "zero" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "nearest" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "up" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- </details>
- <details class="group ">
- <summary class="flex cursor-pointer flex-row items-center justify-between ">
- <i18n.Translate>
- Rounding an amount of 1.24 with rounding value 0.3
- </i18n.Translate>
- <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
- </svg>
- </summary>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- Given the rounding value of 0.3 the possible values closest to 1.24 are: 0.9, 1.2, 1.5, 1.8.
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "zero" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "nearest" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "up" mode the value will be rounded to 1.5
- </i18n.Translate>
- </p>
- </details>
- <details class="group ">
- <summary class="flex cursor-pointer flex-row items-center justify-between ">
- <i18n.Translate>
- Rounding an amount of 1.26 with rounding value 0.3
- </i18n.Translate>
- <svg class="h-6 w-6 rotate-0 transform group-open:rotate-180" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" aria-hidden="true">
- <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path>
- </svg>
- </summary>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- Given the rounding value of 0.3 the possible values closest to 1.24 are: 0.9, 1.2, 1.5, 1.8.
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "zero" mode the value will be rounded to 1.2
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "nearest" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- <p class="text-gray-900 my-4">
- <i18n.Translate>
- With the "up" mode the value will be rounded to 1.3
- </i18n.Translate>
- </p>
- </details>
- </section>
- </Attention>
- </div>
-
-
-
- <div class="px-6 pt-6">
- <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
- <div class="sm:col-span-5">
- <label
- for="cashin_fee"
- class="block text-sm font-medium leading-6 text-gray-900"
- >{i18n.str`Fee`}</label>
- <InputAmount
- name="cashin_fee"
- left
- currency={outputCurrency}
- value={fee?.value ?? ""}
- onChange={fee?.onUpdate}
- />
- <ShowInputErrorLabel
- message={fee?.error}
- isDirty={fee?.value !== undefined}
- />
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>Amount to be deducted before amount is credited.</i18n.Translate>
- </p>
- </div>
- </div>
- </div>
-
- </Fragment>
-}
diff --git a/packages/demobank-ui/src/pages/regional/CreateCashout.tsx b/packages/demobank-ui/src/pages/regional/CreateCashout.tsx
deleted file mode 100644
index 2f15d16b4..000000000
--- a/packages/demobank-ui/src/pages/regional/CreateCashout.tsx
+++ /dev/null
@@ -1,809 +0,0 @@
-/*
- 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,
- encodeCrock,
- getRandomBytes,
- parsePaytoUri,
-} 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 { useEffect, useState } from "preact/hooks";
-import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
-import { VersionHint, useBankCoreApiContext } from "../../context/config.js";
-import { useAccountDetails } from "../../hooks/account.js";
-import { useSessionState } from "../../hooks/session.js";
-import { useBankState } from "../../hooks/bank-state.js";
-import { TransferCalculation, useCashoutEstimator, useConversionInfo, useEstimator } from "../../hooks/regional.js";
-import { 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;
- routeClose: RouteDefinition;
- routeHere: RouteDefinition;
-}
-
-type FormType = {
- isDebit: boolean;
- amount: string;
- subject: string;
- channel: TanChannel;
-};
-type ErrorFrom<T> = {
- [P in keyof T]+?: string;
-};
-
-export function CreateCashout({
- account: accountName,
- onAuthorizationRequired,
- focus,
- routeHere,
- routeClose,
-}: Props): VNode {
- const { i18n } = useTranslationContext();
- const resultAccount = useAccountDetails(accountName);
- const {
- estimateByCredit: calculateFromCredit,
- estimateByDebit: calculateFromDebit,
- } = useCashoutEstimator();
- const { state: credentials } = useSessionState();
- const creds = credentials.status !== "loggedIn" ? undefined : credentials;
- const [, updateBankState] = useBankState();
-
- const { bank: 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 (
- <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({})}
- name="close"
- 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;
-
- if (!resultAccount) {
- return <Loading />;
- }
- if (resultAccount instanceof TalerError) {
- 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);
- }
- }
- if (!info) {
- return <Loading />;
- }
-
- if (info instanceof TalerError) {
- return <ErrorLoadingWithDebug error={info} />;
- }
- if (info.type === "fail") {
- switch (info.case) {
- case HttpStatusCode.NotImplemented: {
- return (
- <Attention
- type="danger"
- title={i18n.str`Cashout are disabled`}
- >
- <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
- </Attention>
- );
- }
- default:
- assertUnreachable(info.case);
- }
- }
-
- const conversionInfo = info.body.conversion_rate;
- if (!conversionInfo) {
- 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),
- };
-
- 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 [calculationResult, setCalculation] = useState<TransferCalculation>(zeroCalc);
- const sellFee = Amounts.parseOrThrow(conversionInfo.cashout_fee);
- 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
- }`,
- );
-
- useEffect(() => {
- async function doAsync() {
- await handleError(async () => {
- const higerThanMin = form.isDebit ?
- Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) === 1 : true;
- const notZero = Amounts.isNonZero(inputAmount)
- if (notZero && higerThanMin) {
- const resp = await (form.isDebit
- ? calculateFromDebit(inputAmount, sellFee)
- : calculateFromCredit(inputAmount, sellFee));
- setCalculation(resp);
- } else {
- setCalculation(zeroCalc)
- }
- });
- }
- doAsync();
- }, [form.amount, form.isDebit]);
-
- const calc = calculationResult === "amount-is-too-small" ? zeroCalc : calculationResult
-
- const balanceAfter = Amounts.sub(account.balance, calc.debit).amount;
-
- function updateForm(newForm: typeof form): void {
- setForm(newForm);
- }
- const errors = undefinedIfEmpty<ErrorFrom<typeof form>>({
- subject: !form.subject ? i18n.str`Required` : undefined,
- amount: !form.amount
- ? i18n.str`Required`
- : !inputAmount
- ? i18n.str`Invalid`
- : Amounts.cmp(limit, calc.debit) === -1
- ? i18n.str`Balance is not enough`
- : form.isDebit && Amounts.cmp(inputAmount, conversionInfo.cashout_min_amount) < 1
- ? i18n.str`Needs to be higher than ${Amounts.stringifyValueWithSpec(Amounts.parseOrThrow(conversionInfo.cashout_min_amount), regional_currency_specification).normal}`
- : calculationResult === "amount-is-too-small"
- ? i18n.str`Amount needs to be higher`
- : Amounts.isZero(calc.credit)
- ? i18n.str`The total transfer at destination will be zero`
- : undefined,
- channel: OLD_CASHOUT_API && !form.channel ? i18n.str`Required` : undefined,
- });
- const trimmedAmountStr = form.amount?.trim();
-
- async function createCashout() {
- 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;
-
- if (!creds || !form.subject || !validChannel) return;
- const request = {
- request_uid,
- amount_credit: Amounts.stringify(calc.credit),
- amount_debit: Amounts.stringify(calc.debit),
- subject: form.subject,
- tan_channel: form.channel,
- };
- const resp = await api.createCashout(creds, request);
- if (resp.type === "ok") {
- notifyInfo(i18n.str`Cashout created`);
- } else {
- switch (resp.case) {
- case HttpStatusCode.Accepted: {
- updateBankState("currentChallenge", {
- operation: "create-cashout",
- id: String(resp.body.challenge_id),
- sent: AbsoluteTime.never(),
- location: routeHere.url({}),
- request,
- });
- 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 succeeded 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`Cashout are disabled`,
- 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);
- }
- });
- }
- const cashoutDisabled =
- config.supported_tan_channels.length < 1 ||
- !resultAccount.body.cashout_payto_uri;
-
- const cashoutAccount = !resultAccount.body.cashout_payto_uri
- ? undefined
- : parsePaytoUri(resultAccount.body.cashout_payto_uri);
- const cashoutAccountName = !cashoutAccount
- ? undefined
- : cashoutAccount.targetPath;
-
- const cashoutLegalName = !cashoutAccount
- ? undefined
- : cashoutAccount.params["receiver-name"];
-
- 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>
-
- <dl class="mt-4 space-y-4">
- <div class="justify-between items-center flex">
- <dt class="text-sm text-gray-600">
- <i18n.Translate>Conversion 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>
- </dt>
- <dd class="text-sm text-gray-900">
- <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>
- </dt>
- <dd class="text-sm text-gray-900">
- <RenderAmount
- value={sellFee}
- spec={fiat_currency_specification}
- />
- </dd>
- </div>
- {cashoutAccountName && cashoutLegalName ? (
- <Fragment>
- <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>
- </dt>
- <dd class="text-sm text-gray-900">{cashoutAccountName}</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>Legal name</i18n.Translate>
- </span>
- </dt>
- <dd class="text-sm text-gray-900">{cashoutLegalName}</dd>
- </div>
- <p class="mt-2 text-sm text-gray-500">
- <i18n.Translate>If this name doesn't match the account holder's name your transaction may fail.</i18n.Translate>
- </p>
- </Fragment>
- ) : (
- <div class="flex items-center justify-between border-t-2 afu pt-4">
- <Attention type="warning" title={i18n.str`No cashout account`}>
- <i18n.Translate>
- Before doing a cashout you need to complete your profile
- </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();
- }}
- >
- <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">
- {/* subject */}
-
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="subject"
- >
- {i18n.str`Transfer subject`}
- <b style={{ color: "red" }}> *</b>
- </label>
- <div class="mt-2">
- <input
- ref={focus ? doAutoFocus : undefined}
- type="text"
- class="block w-full rounded-md disabled:bg-gray-200 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="subject"
- id="subject"
- disabled={cashoutDisabled}
- data-error={!!errors?.subject && form.subject !== undefined}
- value={form.subject ?? ""}
- onChange={(e) => {
- form.subject = e.currentTarget.value;
- updateForm(structuredClone(form));
- }}
- autocomplete="off"
- />
- <ShowInputErrorLabel
- message={errors?.subject}
- isDirty={form.subject !== undefined}
- />
- </div>
- </div>
-
- <div class="sm:col-span-5">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="subject"
- >
- {i18n.str`Currency`}
- </label>
-
- <div class="mt-2">
- <button
- type="button"
- name="set 50"
- class=" inline-flex p-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();
- form.isDebit = true;
- updateForm(structuredClone(form));
- }}
- >
- {form.isDebit ?
- <svg
- class="self-center flex-none h-5 w-5 text-indigo-600"
- 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>
-
- :
- <svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
- <path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
- </svg>
- }
-
- <i18n.Translate>Send {regional_currency}</i18n.Translate>
- </button>
- <button
- type="button"
- name="set 25"
- class=" -ml-px -mr-px inline-flex p-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();
- form.isDebit = false;
- updateForm(structuredClone(form));
- }}
- >
- {!form.isDebit ?
- <svg
- class="self-center flex-none h-5 w-5 text-indigo-600"
- 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>
-
- :
- <svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
- <path d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
- </svg>
- }
-
- <i18n.Translate>Receive {fiat_currency}</i18n.Translate>
- </button>
- </div>
- </div>
-
- {/* amount */}
- <div class="sm:col-span-5">
- <div class="flex justify-between">
- <label
- class="block text-sm font-medium leading-6 text-gray-900"
- for="amount"
- >
- {i18n.str`Amount`}
- <b style={{ color: "red" }}> *</b>
- </label>
- {/* <button
- type="button"
- data-enabled={form.isDebit}
- class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
- role="switch"
- aria-checked="false"
- aria-labelledby="availability-label"
- aria-describedby="availability-description"
- onClick={() => {
- form.isDebit = !form.isDebit;
- updateForm(structuredClone(form));
- }}
- >
- <span
- aria-hidden="true"
- data-enabled={form.isDebit}
- class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
- ></span>
- </button> */}
- </div>
- <div class="mt-2">
- <InputAmount
- name="amount"
- left
- currency={form.isDebit ? regional_currency : fiat_currency}
- value={trimmedAmountStr}
- 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>
- <dd class="text-sm text-gray-900">
- <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>
- </dt>
- <dd class="text-sm text-gray-900">
- <RenderAmount
- value={balanceAfter}
- spec={regional_currency_specification}
- />
- </dd>
- </div>
- {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>
- </dt>
- <dd class="text-sm text-gray-900">
- <RenderAmount
- value={calc.beforeFee}
- spec={fiat_currency_specification}
- />
- </dd>
- </div>
- )}
- <div class="flex justify-between items-center border-t-2 afu pt-4">
- <dt class="text-lg text-gray-900 font-medium">
- <i18n.Translate>Total cashout transfer</i18n.Translate>
- </dt>
- <dd class="text-lg text-gray-900 font-medium">
- <RenderAmount
- value={calc.credit}
- withColor
- spec={fiat_currency_specification}
- />
- </dd>
- </div>
- </dl>
- </div>
- )}
-
- {/* channel, not shown if new cashout api */}
- {!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`}
- >
- <i18n.Translate>
- 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"
- for="channel"
- >
- {i18n.str`Second factor authentication`}
- </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"
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span
- id="project-type-0-label"
- class="block text-sm font-medium text-gray-900 "
- >
- <i18n.Translate>Email</i18n.Translate>
- </span>
- {!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>
- </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"
- />
- <span class="flex flex-1">
- <span class="flex flex-col">
- <span
- id="project-type-1-label"
- class="block text-sm font-medium text-gray-900"
- >
- <i18n.Translate>SMS</i18n.Translate>
- </span>
- {!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>
- </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">
- <a
- href={routeClose.url({})}
- name="cancel"
- type="button"
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <i18n.Translate>Cancel</i18n.Translate>
- </a>
- <button
- type="submit"
- name="cashout"
- 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();
- }}
- >
- <i18n.Translate>Cashout</i18n.Translate>
- </button>
- </div>
- </form>
- </div >
- </div >
- );
-}
diff --git a/packages/demobank-ui/src/pages/regional/ShowCashoutDetails.tsx b/packages/demobank-ui/src/pages/regional/ShowCashoutDetails.tsx
deleted file mode 100644
index 415f88868..000000000
--- a/packages/demobank-ui/src/pages/regional/ShowCashoutDetails.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- 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,
- Duration,
- HttpStatusCode,
- TalerError,
- assertUnreachable,
-} from "@gnu-taler/taler-util";
-import {
- Attention,
- Loading,
- useTranslationContext,
-} from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
-import { VNode, h } from "preact";
-import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
-import { useCashoutDetails, useConversionInfo } from "../../hooks/regional.js";
-import { RouteDefinition } from "../../route.js";
-import { RenderAmount } from "../PaytoWireTransferForm.js";
-import { Time } from "../../components/Time.js";
-
-interface Props {
- id: string;
- routeClose: RouteDefinition;
-}
-export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
- const { i18n, dateLocale } = useTranslationContext();
- const 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`}
- />
- );
- }
- if (!result) {
- return <Loading />;
- }
- if (result instanceof TalerError) {
- 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`Cashout are disabled`}
- >
- <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
- </Attention>
- );
- default:
- assertUnreachable(result);
- }
- }
- if (!info) {
- return <Loading />;
- }
-
- if (info instanceof TalerError) {
- return <ErrorLoadingWithDebug error={info} />;
- }
- if (info.type === "fail") {
- switch (info.case) {
- case HttpStatusCode.NotImplemented: {
- return (
- <Attention type="danger"
- title={i18n.str`Cashout are disabled`}
- >
- <i18n.Translate>Cashout should be enable by configuration and the conversion rate should be initialized with fee, ratio and rounding mode.</i18n.Translate>
- </Attention>
- );
- }
- default:
- assertUnreachable(info.case);
- }
- }
-
- 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>
- <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>
- <dd class="text-sm ">{result.body.subject}</dd>
- </div>
- </dl>
- </section>
- <div class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2">
- <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">
- <dl class="space-y-4">
- {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>
- <dd class="text-sm ">
- <Time format="dd/MM/yyyy HH:mm:ss"
- timestamp={AbsoluteTime.fromProtocolTimestamp(result.body.creation_time)}
- // relative={Duration.fromSpec({ days: 1 })}
- />
- </dd>
- </div>
- ) : 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>
- <dd class=" font-medium">
- <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>
- </dt>
- <dd class="text-sm ">
- <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" }}>
- <a
- href={routeClose.url({})}
- name="close"
- class="text-sm font-semibold leading-6 text-gray-900"
- >
- <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
deleted file mode 100644
index d66fb005b..000000000
--- a/packages/demobank-ui/src/pages/rnd.ts
+++ /dev/null
@@ -1,2907 +0,0 @@
-/*
- 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",
- "history",
- "way",
- "art",
- "world",
- "information",
- "map",
- "two",
- "family",
- "government",
- "health",
- "system",
- "computer",
- "meat",
- "year",
- "thanks",
- "music",
- "person",
- "reading",
- "method",
- "data",
- "food",
- "understanding",
- "theory",
- "law",
- "bird",
- "literature",
- "problem",
- "software",
- "control",
- "knowledge",
- "power",
- "ability",
- "economics",
- "love",
- "internet",
- "television",
- "science",
- "library",
- "nature",
- "fact",
- "product",
- "idea",
- "temperature",
- "investment",
- "area",
- "society",
- "activity",
- "story",
- "industry",
- "media",
- "thing",
- "oven",
- "community",
- "definition",
- "safety",
- "quality",
- "development",
- "language",
- "management",
- "player",
- "variety",
- "video",
- "week",
- "security",
- "country",
- "exam",
- "movie",
- "organization",
- "equipment",
- "physics",
- "analysis",
- "policy",
- "series",
- "thought",
- "basis",
- "boyfriend",
- "direction",
- "strategy",
- "technology",
- "army",
- "camera",
- "freedom",
- "paper",
- "environment",
- "child",
- "instance",
- "month",
- "truth",
- "marketing",
- "university",
- "writing",
- "article",
- "department",
- "difference",
- "goal",
- "news",
- "audience",
- "fishing",
- "growth",
- "income",
- "marriage",
- "user",
- "combination",
- "failure",
- "meaning",
- "medicine",
- "philosophy",
- "teacher",
- "communication",
- "night",
- "chemistry",
- "disease",
- "disk",
- "energy",
- "nation",
- "road",
- "role",
- "soup",
- "advertising",
- "location",
- "success",
- "addition",
- "apartment",
- "education",
- "math",
- "moment",
- "painting",
- "politics",
- "attention",
- "decision",
- "event",
- "property",
- "shopping",
- "student",
- "wood",
- "competition",
- "distribution",
- "entertainment",
- "office",
- "population",
- "president",
- "unit",
- "category",
- "cigarette",
- "context",
- "introduction",
- "opportunity",
- "performance",
- "driver",
- "flight",
- "length",
- "magazine",
- "newspaper",
- "relationship",
- "teaching",
- "cell",
- "dealer",
- "finding",
- "lake",
- "member",
- "message",
- "phone",
- "scene",
- "appearance",
- "association",
- "concept",
- "customer",
- "death",
- "discussion",
- "housing",
- "inflation",
- "insurance",
- "mood",
- "woman",
- "advice",
- "blood",
- "effort",
- "expression",
- "importance",
- "opinion",
- "payment",
- "reality",
- "responsibility",
- "situation",
- "skill",
- "statement",
- "wealth",
- "application",
- "city",
- "county",
- "depth",
- "estate",
- "foundation",
- "grandmother",
- "heart",
- "perspective",
- "photo",
- "recipe",
- "studio",
- "topic",
- "collection",
- "depression",
- "imagination",
- "passion",
- "percentage",
- "resource",
- "setting",
- "ad",
- "agency",
- "college",
- "connection",
- "criticism",
- "debt",
- "description",
- "memory",
- "patience",
- "secretary",
- "solution",
- "administration",
- "aspect",
- "attitude",
- "director",
- "personality",
- "psychology",
- "recommendation",
- "response",
- "selection",
- "storage",
- "version",
- "alcohol",
- "argument",
- "complaint",
- "contract",
- "emphasis",
- "highway",
- "loss",
- "membership",
- "possession",
- "preparation",
- "steak",
- "union",
- "agreement",
- "cancer",
- "currency",
- "employment",
- "engineering",
- "entry",
- "interaction",
- "mixture",
- "preference",
- "region",
- "republic",
- "tradition",
- "virus",
- "actor",
- "classroom",
- "delivery",
- "device",
- "difficulty",
- "drama",
- "election",
- "engine",
- "football",
- "guidance",
- "hotel",
- "owner",
- "priority",
- "protection",
- "suggestion",
- "tension",
- "variation",
- "anxiety",
- "atmosphere",
- "awareness",
- "bath",
- "bread",
- "candidate",
- "climate",
- "comparison",
- "confusion",
- "construction",
- "elevator",
- "emotion",
- "employee",
- "employer",
- "guest",
- "height",
- "leadership",
- "mall",
- "manager",
- "operation",
- "recording",
- "sample",
- "transportation",
- "charity",
- "cousin",
- "disaster",
- "editor",
- "efficiency",
- "excitement",
- "extent",
- "feedback",
- "guitar",
- "homework",
- "leader",
- "mom",
- "outcome",
- "permission",
- "presentation",
- "promotion",
- "reflection",
- "refrigerator",
- "resolution",
- "revenue",
- "session",
- "singer",
- "tennis",
- "basket",
- "bonus",
- "cabinet",
- "childhood",
- "church",
- "clothes",
- "coffee",
- "dinner",
- "drawing",
- "hair",
- "hearing",
- "initiative",
- "judgment",
- "lab",
- "measurement",
- "mode",
- "mud",
- "orange",
- "poetry",
- "police",
- "possibility",
- "procedure",
- "queen",
- "ratio",
- "relation",
- "restaurant",
- "satisfaction",
- "sector",
- "signature",
- "significance",
- "song",
- "tooth",
- "town",
- "vehicle",
- "volume",
- "wife",
- "accident",
- "airport",
- "appointment",
- "arrival",
- "assumption",
- "baseball",
- "chapter",
- "committee",
- "conversation",
- "database",
- "enthusiasm",
- "error",
- "explanation",
- "farmer",
- "gate",
- "girl",
- "hall",
- "historian",
- "hospital",
- "injury",
- "instruction",
- "maintenance",
- "manufacturer",
- "meal",
- "perception",
- "pie",
- "poem",
- "presence",
- "proposal",
- "reception",
- "replacement",
- "revolution",
- "river",
- "son",
- "speech",
- "tea",
- "village",
- "warning",
- "winner",
- "worker",
- "writer",
- "assistance",
- "breath",
- "buyer",
- "chest",
- "chocolate",
- "conclusion",
- "contribution",
- "cookie",
- "courage",
- "dad",
- "desk",
- "drawer",
- "establishment",
- "examination",
- "garbage",
- "grocery",
- "honey",
- "impression",
- "improvement",
- "independence",
- "insect",
- "inspection",
- "inspector",
- "king",
- "ladder",
- "menu",
- "penalty",
- "piano",
- "potato",
- "profession",
- "professor",
- "quantity",
- "reaction",
- "requirement",
- "salad",
- "sister",
- "supermarket",
- "tongue",
- "weakness",
- "wedding",
- "affair",
- "ambition",
- "analyst",
- "apple",
- "assignment",
- "assistant",
- "bathroom",
- "bedroom",
- "beer",
- "birthday",
- "celebration",
- "championship",
- "cheek",
- "client",
- "consequence",
- "departure",
- "diamond",
- "dirt",
- "ear",
- "fortune",
- "friendship",
- "funeral",
- "gene",
- "girlfriend",
- "hat",
- "indication",
- "intention",
- "lady",
- "midnight",
- "negotiation",
- "obligation",
- "passenger",
- "pizza",
- "platform",
- "poet",
- "pollution",
- "recognition",
- "reputation",
- "shirt",
- "sir",
- "speaker",
- "stranger",
- "surgery",
- "sympathy",
- "tale",
- "throat",
- "trainer",
- "uncle",
- "youth",
- "time",
- "work",
- "film",
- "water",
- "money",
- "example",
- "while",
- "business",
- "study",
- "game",
- "life",
- "form",
- "air",
- "day",
- "place",
- "number",
- "part",
- "field",
- "fish",
- "back",
- "process",
- "heat",
- "hand",
- "experience",
- "job",
- "book",
- "end",
- "point",
- "type",
- "home",
- "economy",
- "value",
- "body",
- "market",
- "guide",
- "interest",
- "state",
- "radio",
- "course",
- "company",
- "price",
- "size",
- "card",
- "list",
- "mind",
- "trade",
- "line",
- "care",
- "group",
- "risk",
- "word",
- "fat",
- "force",
- "key",
- "light",
- "training",
- "name",
- "school",
- "top",
- "amount",
- "level",
- "order",
- "practice",
- "research",
- "sense",
- "service",
- "piece",
- "web",
- "boss",
- "sport",
- "fun",
- "house",
- "page",
- "term",
- "test",
- "answer",
- "sound",
- "focus",
- "matter",
- "kind",
- "soil",
- "board",
- "oil",
- "picture",
- "access",
- "garden",
- "range",
- "rate",
- "reason",
- "future",
- "site",
- "demand",
- "exercise",
- "image",
- "case",
- "cause",
- "coast",
- "action",
- "age",
- "bad",
- "boat",
- "record",
- "result",
- "section",
- "building",
- "mouse",
- "cash",
- "class",
- "nothing",
- "period",
- "plan",
- "store",
- "tax",
- "side",
- "subject",
- "space",
- "rule",
- "stock",
- "weather",
- "chance",
- "figure",
- "man",
- "model",
- "source",
- "beginning",
- "earth",
- "program",
- "chicken",
- "design",
- "feature",
- "head",
- "material",
- "purpose",
- "question",
- "rock",
- "salt",
- "act",
- "birth",
- "car",
- "dog",
- "object",
- "scale",
- "sun",
- "note",
- "profit",
- "rent",
- "speed",
- "style",
- "war",
- "bank",
- "craft",
- "half",
- "inside",
- "outside",
- "standard",
- "bus",
- "exchange",
- "eye",
- "fire",
- "position",
- "pressure",
- "stress",
- "advantage",
- "benefit",
- "box",
- "frame",
- "issue",
- "step",
- "cycle",
- "face",
- "item",
- "metal",
- "paint",
- "review",
- "room",
- "screen",
- "structure",
- "view",
- "account",
- "ball",
- "discipline",
- "medium",
- "share",
- "balance",
- "bit",
- "black",
- "bottom",
- "choice",
- "gift",
- "impact",
- "machine",
- "shape",
- "tool",
- "wind",
- "address",
- "average",
- "career",
- "culture",
- "morning",
- "pot",
- "sign",
- "table",
- "task",
- "condition",
- "contact",
- "credit",
- "egg",
- "hope",
- "ice",
- "network",
- "north",
- "square",
- "attempt",
- "date",
- "effect",
- "link",
- "post",
- "star",
- "voice",
- "capital",
- "challenge",
- "friend",
- "self",
- "shot",
- "brush",
- "couple",
- "debate",
- "exit",
- "front",
- "function",
- "lack",
- "living",
- "plant",
- "plastic",
- "spot",
- "summer",
- "taste",
- "theme",
- "track",
- "wing",
- "brain",
- "button",
- "click",
- "desire",
- "foot",
- "gas",
- "influence",
- "notice",
- "rain",
- "wall",
- "base",
- "damage",
- "distance",
- "feeling",
- "pair",
- "savings",
- "staff",
- "sugar",
- "target",
- "text",
- "animal",
- "author",
- "budget",
- "discount",
- "file",
- "ground",
- "lesson",
- "minute",
- "officer",
- "phase",
- "reference",
- "register",
- "sky",
- "stage",
- "stick",
- "title",
- "trouble",
- "bowl",
- "bridge",
- "campaign",
- "character",
- "club",
- "edge",
- "evidence",
- "fan",
- "letter",
- "lock",
- "maximum",
- "novel",
- "option",
- "pack",
- "park",
- "plenty",
- "quarter",
- "skin",
- "sort",
- "weight",
- "baby",
- "background",
- "carry",
- "dish",
- "factor",
- "fruit",
- "glass",
- "joint",
- "master",
- "muscle",
- "red",
- "strength",
- "traffic",
- "trip",
- "vegetable",
- "appeal",
- "chart",
- "gear",
- "ideal",
- "kitchen",
- "land",
- "log",
- "mother",
- "net",
- "party",
- "principle",
- "relative",
- "sale",
- "season",
- "signal",
- "spirit",
- "street",
- "tree",
- "wave",
- "belt",
- "bench",
- "commission",
- "copy",
- "drop",
- "minimum",
- "path",
- "progress",
- "project",
- "sea",
- "south",
- "status",
- "stuff",
- "ticket",
- "tour",
- "angle",
- "blue",
- "breakfast",
- "confidence",
- "daughter",
- "degree",
- "doctor",
- "dot",
- "dream",
- "duty",
- "essay",
- "father",
- "fee",
- "finance",
- "hour",
- "juice",
- "limit",
- "luck",
- "milk",
- "mouth",
- "peace",
- "pipe",
- "seat",
- "stable",
- "storm",
- "substance",
- "team",
- "trick",
- "afternoon",
- "bat",
- "beach",
- "blank",
- "catch",
- "chain",
- "consideration",
- "cream",
- "crew",
- "detail",
- "gold",
- "interview",
- "kid",
- "mark",
- "match",
- "mission",
- "pain",
- "pleasure",
- "score",
- "screw",
- "sex",
- "shop",
- "shower",
- "suit",
- "tone",
- "window",
- "agent",
- "band",
- "block",
- "bone",
- "calendar",
- "cap",
- "coat",
- "contest",
- "corner",
- "court",
- "cup",
- "district",
- "door",
- "east",
- "finger",
- "garage",
- "guarantee",
- "hole",
- "hook",
- "implement",
- "layer",
- "lecture",
- "lie",
- "manner",
- "meeting",
- "nose",
- "parking",
- "partner",
- "profile",
- "respect",
- "rice",
- "routine",
- "schedule",
- "swimming",
- "telephone",
- "tip",
- "winter",
- "airline",
- "bag",
- "battle",
- "bed",
- "bill",
- "bother",
- "cake",
- "code",
- "curve",
- "designer",
- "dimension",
- "dress",
- "ease",
- "emergency",
- "evening",
- "extension",
- "farm",
- "fight",
- "gap",
- "grade",
- "holiday",
- "horror",
- "horse",
- "host",
- "husband",
- "loan",
- "mistake",
- "mountain",
- "nail",
- "noise",
- "occasion",
- "package",
- "patient",
- "pause",
- "phrase",
- "proof",
- "race",
- "relief",
- "sand",
- "sentence",
- "shoulder",
- "smoke",
- "stomach",
- "string",
- "tourist",
- "towel",
- "vacation",
- "west",
- "wheel",
- "wine",
- "arm",
- "aside",
- "associate",
- "bet",
- "blow",
- "border",
- "branch",
- "breast",
- "brother",
- "buddy",
- "bunch",
- "chip",
- "coach",
- "cross",
- "document",
- "draft",
- "dust",
- "expert",
- "floor",
- "god",
- "golf",
- "habit",
- "iron",
- "judge",
- "knife",
- "landscape",
- "league",
- "mail",
- "mess",
- "native",
- "opening",
- "parent",
- "pattern",
- "pin",
- "pool",
- "pound",
- "request",
- "salary",
- "shame",
- "shelter",
- "shoe",
- "silver",
- "tackle",
- "tank",
- "trust",
- "assist",
- "bake",
- "bar",
- "bell",
- "bike",
- "blame",
- "boy",
- "brick",
- "chair",
- "closet",
- "clue",
- "collar",
- "comment",
- "conference",
- "devil",
- "diet",
- "fear",
- "fuel",
- "glove",
- "jacket",
- "lunch",
- "monitor",
- "mortgage",
- "nurse",
- "pace",
- "panic",
- "peak",
- "plane",
- "reward",
- "row",
- "sandwich",
- "shock",
- "spite",
- "spray",
- "surprise",
- "till",
- "transition",
- "weekend",
- "welcome",
- "yard",
- "alarm",
- "bend",
- "bicycle",
- "bite",
- "blind",
- "bottle",
- "cable",
- "candle",
- "clerk",
- "cloud",
- "concert",
- "counter",
- "flower",
- "grandfather",
- "harm",
- "knee",
- "lawyer",
- "leather",
- "load",
- "mirror",
- "neck",
- "pension",
- "plate",
- "purple",
- "ruin",
- "ship",
- "skirt",
- "slice",
- "snow",
- "specialist",
- "stroke",
- "switch",
- "trash",
- "tune",
- "zone",
- "anger",
- "award",
- "bid",
- "bitter",
- "boot",
- "bug",
- "camp",
- "candy",
- "carpet",
- "cat",
- "champion",
- "channel",
- "clock",
- "comfort",
- "cow",
- "crack",
- "engineer",
- "entrance",
- "fault",
- "grass",
- "guy",
- "hell",
- "highlight",
- "incident",
- "island",
- "joke",
- "jury",
- "leg",
- "lip",
- "mate",
- "motor",
- "nerve",
- "passage",
- "pen",
- "pride",
- "priest",
- "prize",
- "promise",
- "resident",
- "resort",
- "ring",
- "roof",
- "rope",
- "sail",
- "scheme",
- "script",
- "sock",
- "station",
- "toe",
- "tower",
- "truck",
- "witness",
- "a",
- "you",
- "it",
- "can",
- "will",
- "if",
- "one",
- "many",
- "most",
- "other",
- "use",
- "make",
- "good",
- "look",
- "help",
- "go",
- "great",
- "being",
- "few",
- "might",
- "still",
- "public",
- "read",
- "keep",
- "start",
- "give",
- "human",
- "local",
- "general",
- "she",
- "specific",
- "long",
- "play",
- "feel",
- "high",
- "tonight",
- "put",
- "common",
- "set",
- "change",
- "simple",
- "past",
- "big",
- "possible",
- "particular",
- "today",
- "major",
- "personal",
- "current",
- "national",
- "cut",
- "natural",
- "physical",
- "show",
- "try",
- "check",
- "second",
- "call",
- "move",
- "pay",
- "let",
- "increase",
- "single",
- "individual",
- "turn",
- "ask",
- "buy",
- "guard",
- "hold",
- "main",
- "offer",
- "potential",
- "professional",
- "international",
- "travel",
- "cook",
- "alternative",
- "following",
- "special",
- "working",
- "whole",
- "dance",
- "excuse",
- "cold",
- "commercial",
- "low",
- "purchase",
- "deal",
- "primary",
- "worth",
- "fall",
- "necessary",
- "positive",
- "produce",
- "search",
- "present",
- "spend",
- "talk",
- "creative",
- "tell",
- "cost",
- "drive",
- "green",
- "support",
- "glad",
- "remove",
- "return",
- "run",
- "complex",
- "due",
- "effective",
- "middle",
- "regular",
- "reserve",
- "independent",
- "leave",
- "original",
- "reach",
- "rest",
- "serve",
- "watch",
- "beautiful",
- "charge",
- "active",
- "break",
- "negative",
- "safe",
- "stay",
- "visit",
- "visual",
- "affect",
- "cover",
- "report",
- "rise",
- "walk",
- "white",
- "beyond",
- "junior",
- "pick",
- "unique",
- "anything",
- "classic",
- "final",
- "lift",
- "mix",
- "private",
- "stop",
- "teach",
- "western",
- "concern",
- "familiar",
- "fly",
- "official",
- "broad",
- "comfortable",
- "gain",
- "maybe",
- "rich",
- "save",
- "stand",
- "young",
- "fail",
- "heavy",
- "hello",
- "lead",
- "listen",
- "valuable",
- "worry",
- "handle",
- "leading",
- "meet",
- "release",
- "sell",
- "finish",
- "normal",
- "press",
- "ride",
- "secret",
- "spread",
- "spring",
- "tough",
- "wait",
- "brown",
- "deep",
- "display",
- "flow",
- "hit",
- "objective",
- "shoot",
- "touch",
- "cancel",
- "chemical",
- "cry",
- "dump",
- "extreme",
- "push",
- "conflict",
- "eat",
- "fill",
- "formal",
- "jump",
- "kick",
- "opposite",
- "pass",
- "pitch",
- "remote",
- "total",
- "treat",
- "vast",
- "abuse",
- "beat",
- "burn",
- "deposit",
- "print",
- "raise",
- "sleep",
- "somewhere",
- "advance",
- "anywhere",
- "consist",
- "dark",
- "double",
- "draw",
- "equal",
- "fix",
- "hire",
- "internal",
- "join",
- "kill",
- "sensitive",
- "tap",
- "win",
- "attack",
- "claim",
- "constant",
- "drag",
- "drink",
- "guess",
- "minor",
- "pull",
- "raw",
- "soft",
- "solid",
- "wear",
- "weird",
- "wonder",
- "annual",
- "count",
- "dead",
- "doubt",
- "feed",
- "forever",
- "impress",
- "nobody",
- "repeat",
- "round",
- "sing",
- "slide",
- "strip",
- "whereas",
- "wish",
- "combine",
- "command",
- "dig",
- "divide",
- "equivalent",
- "hang",
- "hunt",
- "initial",
- "march",
- "mention",
- "smell",
- "spiritual",
- "survey",
- "tie",
- "adult",
- "brief",
- "crazy",
- "escape",
- "gather",
- "hate",
- "prior",
- "repair",
- "rough",
- "sad",
- "scratch",
- "sick",
- "strike",
- "employ",
- "external",
- "hurt",
- "illegal",
- "laugh",
- "lay",
- "mobile",
- "nasty",
- "ordinary",
- "respond",
- "royal",
- "senior",
- "split",
- "strain",
- "struggle",
- "swim",
- "train",
- "upper",
- "wash",
- "yellow",
- "convert",
- "crash",
- "dependent",
- "fold",
- "funny",
- "grab",
- "hide",
- "miss",
- "permit",
- "quote",
- "recover",
- "resolve",
- "roll",
- "sink",
- "slip",
- "spare",
- "suspect",
- "sweet",
- "swing",
- "twist",
- "upstairs",
- "usual",
- "abroad",
- "brave",
- "calm",
- "concentrate",
- "estimate",
- "grand",
- "male",
- "mine",
- "prompt",
- "quiet",
- "refuse",
- "regret",
- "reveal",
- "rush",
- "shake",
- "shift",
- "shine",
- "steal",
- "suck",
- "surround",
- "anybody",
- "bear",
- "brilliant",
- "dare",
- "dear",
- "delay",
- "drunk",
- "female",
- "hurry",
- "inevitable",
- "invite",
- "kiss",
- "neat",
- "pop",
- "punch",
- "quit",
- "reply",
- "representative",
- "resist",
- "rip",
- "rub",
- "silly",
- "smile",
- "spell",
- "stretch",
- "stupid",
- "tear",
- "temporary",
- "tomorrow",
- "wake",
- "wrap",
- "yesterday",
-];
-
-const adj = [
- "abandoned",
- "able",
- "absolute",
- "adorable",
- "adventurous",
- "academic",
- "acceptable",
- "acclaimed",
- "accomplished",
- "accurate",
- "aching",
- "acidic",
- "acrobatic",
- "active",
- "actual",
- "adept",
- "admirable",
- "admired",
- "adolescent",
- "adorable",
- "adored",
- "advanced",
- "afraid",
- "affectionate",
- "aged",
- "aggravating",
- "aggressive",
- "agile",
- "agitated",
- "agonizing",
- "agreeable",
- "ajar",
- "alarmed",
- "alarming",
- "alert",
- "alienated",
- "alive",
- "all",
- "altruistic",
- "amazing",
- "ambitious",
- "ample",
- "amused",
- "amusing",
- "anchored",
- "ancient",
- "angelic",
- "angry",
- "anguished",
- "animated",
- "annual",
- "another",
- "antique",
- "anxious",
- "any",
- "apprehensive",
- "appropriate",
- "apt",
- "arctic",
- "arid",
- "aromatic",
- "artistic",
- "ashamed",
- "assured",
- "astonishing",
- "athletic",
- "attached",
- "attentive",
- "attractive",
- "austere",
- "authentic",
- "authorized",
- "automatic",
- "avaricious",
- "average",
- "aware",
- "awesome",
- "awful",
- "awkward",
- "babyish",
- "bad",
- "back",
- "baggy",
- "bare",
- "barren",
- "basic",
- "beautiful",
- "belated",
- "beloved",
- "beneficial",
- "better",
- "best",
- "bewitched",
- "big",
- "big-hearted",
- "biodegradable",
- "bite-sized",
- "bitter",
- "black",
- "black-and-white",
- "bland",
- "blank",
- "blaring",
- "bleak",
- "blind",
- "blissful",
- "blond",
- "blue",
- "blushing",
- "bogus",
- "boiling",
- "bold",
- "bony",
- "boring",
- "bossy",
- "both",
- "bouncy",
- "bountiful",
- "bowed",
- "brave",
- "breakable",
- "brief",
- "bright",
- "brilliant",
- "brisk",
- "broken",
- "bronze",
- "brown",
- "bruised",
- "bubbly",
- "bulky",
- "bumpy",
- "buoyant",
- "burdensome",
- "burly",
- "bustling",
- "busy",
- "buttery",
- "buzzing",
- "calculating",
- "calm",
- "candid",
- "canine",
- "capital",
- "carefree",
- "careful",
- "careless",
- "caring",
- "cautious",
- "cavernous",
- "celebrated",
- "charming",
- "cheap",
- "cheerful",
- "cheery",
- "chief",
- "chilly",
- "chubby",
- "circular",
- "classic",
- "clean",
- "clear",
- "clear-cut",
- "clever",
- "close",
- "closed",
- "cloudy",
- "clueless",
- "clumsy",
- "cluttered",
- "coarse",
- "cold",
- "colorful",
- "colorless",
- "colossal",
- "comfortable",
- "common",
- "compassionate",
- "competent",
- "complete",
- "complex",
- "complicated",
- "composed",
- "concerned",
- "concrete",
- "confused",
- "conscious",
- "considerate",
- "constant",
- "content",
- "conventional",
- "cooked",
- "cool",
- "cooperative",
- "coordinated",
- "corny",
- "corrupt",
- "costly",
- "courageous",
- "courteous",
- "crafty",
- "crazy",
- "creamy",
- "creative",
- "creepy",
- "criminal",
- "crisp",
- "critical",
- "crooked",
- "crowded",
- "cruel",
- "crushing",
- "cuddly",
- "cultivated",
- "cultured",
- "cumbersome",
- "curly",
- "curvy",
- "cute",
- "cylindrical",
- "damaged",
- "damp",
- "dangerous",
- "dapper",
- "daring",
- "darling",
- "dark",
- "dazzling",
- "dead",
- "deadly",
- "deafening",
- "dear",
- "dearest",
- "decent",
- "decimal",
- "decisive",
- "deep",
- "defenseless",
- "defensive",
- "defiant",
- "deficient",
- "definite",
- "definitive",
- "delayed",
- "delectable",
- "delicious",
- "delightful",
- "delirious",
- "demanding",
- "dense",
- "dental",
- "dependable",
- "dependent",
- "descriptive",
- "deserted",
- "detailed",
- "determined",
- "devoted",
- "different",
- "difficult",
- "digital",
- "diligent",
- "dim",
- "dimpled",
- "dimwitted",
- "direct",
- "disastrous",
- "discrete",
- "disfigured",
- "disgusting",
- "disloyal",
- "dismal",
- "distant",
- "downright",
- "dreary",
- "dirty",
- "disguised",
- "dishonest",
- "dismal",
- "distant",
- "distinct",
- "distorted",
- "dizzy",
- "dopey",
- "doting",
- "double",
- "downright",
- "drab",
- "drafty",
- "dramatic",
- "dreary",
- "droopy",
- "dry",
- "dual",
- "dull",
- "dutiful",
- "each",
- "eager",
- "earnest",
- "early",
- "easy",
- "easy-going",
- "ecstatic",
- "edible",
- "educated",
- "elaborate",
- "elastic",
- "elated",
- "elderly",
- "electric",
- "elegant",
- "elementary",
- "elliptical",
- "embarrassed",
- "embellished",
- "eminent",
- "emotional",
- "empty",
- "enchanted",
- "enchanting",
- "energetic",
- "enlightened",
- "enormous",
- "enraged",
- "entire",
- "envious",
- "equal",
- "equatorial",
- "essential",
- "esteemed",
- "ethical",
- "euphoric",
- "even",
- "evergreen",
- "everlasting",
- "every",
- "evil",
- "exalted",
- "excellent",
- "exemplary",
- "exhausted",
- "excitable",
- "excited",
- "exciting",
- "exotic",
- "expensive",
- "experienced",
- "expert",
- "extraneous",
- "extroverted",
- "extra-large",
- "extra-small",
- "fabulous",
- "failing",
- "faint",
- "fair",
- "faithful",
- "fake",
- "false",
- "familiar",
- "famous",
- "fancy",
- "fantastic",
- "far",
- "faraway",
- "far-flung",
- "far-off",
- "fast",
- "fat",
- "fatal",
- "fatherly",
- "favorable",
- "favorite",
- "fearful",
- "fearless",
- "feisty",
- "feline",
- "female",
- "feminine",
- "few",
- "fickle",
- "filthy",
- "fine",
- "finished",
- "firm",
- "first",
- "firsthand",
- "fitting",
- "fixed",
- "flaky",
- "flamboyant",
- "flashy",
- "flat",
- "flawed",
- "flawless",
- "flickering",
- "flimsy",
- "flippant",
- "flowery",
- "fluffy",
- "fluid",
- "flustered",
- "focused",
- "fond",
- "foolhardy",
- "foolish",
- "forceful",
- "forked",
- "formal",
- "forsaken",
- "forthright",
- "fortunate",
- "fragrant",
- "frail",
- "frank",
- "frayed",
- "free",
- "French",
- "fresh",
- "frequent",
- "friendly",
- "frightened",
- "frightening",
- "frigid",
- "frilly",
- "frizzy",
- "frivolous",
- "front",
- "frosty",
- "frozen",
- "frugal",
- "fruitful",
- "full",
- "fumbling",
- "functional",
- "funny",
- "fussy",
- "fuzzy",
- "gargantuan",
- "gaseous",
- "general",
- "generous",
- "gentle",
- "genuine",
- "giant",
- "giddy",
- "gigantic",
- "gifted",
- "giving",
- "glamorous",
- "glaring",
- "glass",
- "gleaming",
- "gleeful",
- "glistening",
- "glittering",
- "gloomy",
- "glorious",
- "glossy",
- "glum",
- "golden",
- "good",
- "good-natured",
- "gorgeous",
- "graceful",
- "gracious",
- "grand",
- "grandiose",
- "granular",
- "grateful",
- "grave",
- "gray",
- "great",
- "greedy",
- "green",
- "gregarious",
- "grim",
- "grimy",
- "gripping",
- "grizzled",
- "gross",
- "grotesque",
- "grouchy",
- "grounded",
- "growing",
- "growling",
- "grown",
- "grubby",
- "gruesome",
- "grumpy",
- "guilty",
- "gullible",
- "gummy",
- "hairy",
- "half",
- "handmade",
- "handsome",
- "handy",
- "happy",
- "happy-go-lucky",
- "hard",
- "hard-to-find",
- "harmful",
- "harmless",
- "harmonious",
- "harsh",
- "hasty",
- "hateful",
- "haunting",
- "healthy",
- "heartfelt",
- "hearty",
- "heavenly",
- "heavy",
- "hefty",
- "helpful",
- "helpless",
- "hidden",
- "hideous",
- "high",
- "high-level",
- "hilarious",
- "hoarse",
- "hollow",
- "homely",
- "honest",
- "honorable",
- "honored",
- "hopeful",
- "horrible",
- "hospitable",
- "hot",
- "huge",
- "humble",
- "humiliating",
- "humming",
- "humongous",
- "hungry",
- "hurtful",
- "husky",
- "icky",
- "icy",
- "ideal",
- "idealistic",
- "identical",
- "idle",
- "idiotic",
- "idolized",
- "ignorant",
- "ill",
- "illegal",
- "ill-fated",
- "ill-informed",
- "illiterate",
- "illustrious",
- "imaginary",
- "imaginative",
- "immaculate",
- "immaterial",
- "immediate",
- "immense",
- "impassioned",
- "impeccable",
- "impartial",
- "imperfect",
- "imperturbable",
- "impish",
- "impolite",
- "important",
- "impossible",
- "impractical",
- "impressionable",
- "impressive",
- "improbable",
- "impure",
- "inborn",
- "incomparable",
- "incompatible",
- "incomplete",
- "inconsequential",
- "incredible",
- "indelible",
- "inexperienced",
- "indolent",
- "infamous",
- "infantile",
- "infatuated",
- "inferior",
- "infinite",
- "informal",
- "innocent",
- "insecure",
- "insidious",
- "insignificant",
- "insistent",
- "instructive",
- "insubstantial",
- "intelligent",
- "intent",
- "intentional",
- "interesting",
- "internal",
- "international",
- "intrepid",
- "ironclad",
- "irresponsible",
- "irritating",
- "itchy",
- "jaded",
- "jagged",
- "jam-packed",
- "jaunty",
- "jealous",
- "jittery",
- "joint",
- "jolly",
- "jovial",
- "joyful",
- "joyous",
- "jubilant",
- "judicious",
- "juicy",
- "jumbo",
- "junior",
- "jumpy",
- "juvenile",
- "kaleidoscopic",
- "keen",
- "key",
- "kind",
- "kindhearted",
- "kindly",
- "klutzy",
- "knobby",
- "knotty",
- "knowledgeable",
- "knowing",
- "known",
- "kooky",
- "kosher",
- "lame",
- "lanky",
- "large",
- "last",
- "lasting",
- "late",
- "lavish",
- "lawful",
- "lazy",
- "leading",
- "lean",
- "leafy",
- "left",
- "legal",
- "legitimate",
- "light",
- "lighthearted",
- "likable",
- "likely",
- "limited",
- "limp",
- "limping",
- "linear",
- "lined",
- "liquid",
- "little",
- "live",
- "lively",
- "livid",
- "loathsome",
- "lone",
- "lonely",
- "long",
- "long-term",
- "loose",
- "lopsided",
- "lost",
- "loud",
- "lovable",
- "lovely",
- "loving",
- "low",
- "loyal",
- "lucky",
- "lumbering",
- "luminous",
- "lumpy",
- "lustrous",
- "luxurious",
- "mad",
- "made-up",
- "magnificent",
- "majestic",
- "major",
- "male",
- "mammoth",
- "married",
- "marvelous",
- "masculine",
- "massive",
- "mature",
- "meager",
- "mealy",
- "mean",
- "measly",
- "meaty",
- "medical",
- "mediocre",
- "medium",
- "meek",
- "mellow",
- "melodic",
- "memorable",
- "menacing",
- "merry",
- "messy",
- "metallic",
- "mild",
- "milky",
- "mindless",
- "miniature",
- "minor",
- "minty",
- "miserable",
- "miserly",
- "misguided",
- "misty",
- "mixed",
- "modern",
- "modest",
- "moist",
- "monstrous",
- "monthly",
- "monumental",
- "moral",
- "mortified",
- "motherly",
- "motionless",
- "mountainous",
- "muddy",
- "muffled",
- "multicolored",
- "mundane",
- "murky",
- "mushy",
- "musty",
- "muted",
- "mysterious",
- "naive",
- "narrow",
- "nasty",
- "natural",
- "naughty",
- "nautical",
- "near",
- "neat",
- "necessary",
- "needy",
- "negative",
- "neglected",
- "negligible",
- "neighboring",
- "nervous",
- "new",
- "next",
- "nice",
- "nifty",
- "nimble",
- "nippy",
- "nocturnal",
- "noisy",
- "nonstop",
- "normal",
- "notable",
- "noted",
- "noteworthy",
- "novel",
- "noxious",
- "numb",
- "nutritious",
- "nutty",
- "obedient",
- "obese",
- "oblong",
- "oily",
- "oblong",
- "obvious",
- "occasional",
- "odd",
- "oddball",
- "offbeat",
- "offensive",
- "official",
- "old",
- "old-fashioned",
- "only",
- "open",
- "optimal",
- "optimistic",
- "opulent",
- "orange",
- "orderly",
- "organic",
- "ornate",
- "ornery",
- "ordinary",
- "original",
- "other",
- "our",
- "outlying",
- "outgoing",
- "outlandish",
- "outrageous",
- "outstanding",
- "oval",
- "overcooked",
- "overdue",
- "overjoyed",
- "overlooked",
- "palatable",
- "pale",
- "paltry",
- "parallel",
- "parched",
- "partial",
- "passionate",
- "past",
- "pastel",
- "peaceful",
- "peppery",
- "perfect",
- "perfumed",
- "periodic",
- "perky",
- "personal",
- "pertinent",
- "pesky",
- "pessimistic",
- "petty",
- "phony",
- "physical",
- "piercing",
- "pink",
- "pitiful",
- "plain",
- "plaintive",
- "plastic",
- "playful",
- "pleasant",
- "pleased",
- "pleasing",
- "plump",
- "plush",
- "polished",
- "polite",
- "political",
- "pointed",
- "pointless",
- "poised",
- "poor",
- "popular",
- "portly",
- "posh",
- "positive",
- "possible",
- "potable",
- "powerful",
- "powerless",
- "practical",
- "precious",
- "present",
- "prestigious",
- "pretty",
- "precious",
- "previous",
- "pricey",
- "prickly",
- "primary",
- "prime",
- "pristine",
- "private",
- "prize",
- "probable",
- "productive",
- "profitable",
- "profuse",
- "proper",
- "proud",
- "prudent",
- "punctual",
- "pungent",
- "puny",
- "pure",
- "purple",
- "pushy",
- "putrid",
- "puzzled",
- "puzzling",
- "quaint",
- "qualified",
- "quarrelsome",
- "quarterly",
- "queasy",
- "querulous",
- "questionable",
- "quick",
- "quick-witted",
- "quiet",
- "quintessential",
- "quirky",
- "quixotic",
- "quizzical",
- "radiant",
- "ragged",
- "rapid",
- "rare",
- "rash",
- "raw",
- "recent",
- "reckless",
- "rectangular",
- "ready",
- "real",
- "realistic",
- "reasonable",
- "red",
- "reflecting",
- "regal",
- "regular",
- "reliable",
- "relieved",
- "remarkable",
- "remorseful",
- "remote",
- "repentant",
- "required",
- "respectful",
- "responsible",
- "repulsive",
- "revolving",
- "rewarding",
- "rich",
- "rigid",
- "right",
- "ringed",
- "ripe",
- "roasted",
- "robust",
- "rosy",
- "rotating",
- "rotten",
- "rough",
- "round",
- "rowdy",
- "royal",
- "rubbery",
- "rundown",
- "ruddy",
- "rude",
- "runny",
- "rural",
- "rusty",
- "sad",
- "safe",
- "salty",
- "same",
- "sandy",
- "sane",
- "sarcastic",
- "sardonic",
- "satisfied",
- "scaly",
- "scarce",
- "scared",
- "scary",
- "scented",
- "scholarly",
- "scientific",
- "scornful",
- "scratchy",
- "scrawny",
- "second",
- "secondary",
- "second-hand",
- "secret",
- "self-assured",
- "self-reliant",
- "selfish",
- "sentimental",
- "separate",
- "serene",
- "serious",
- "serpentine",
- "several",
- "severe",
- "shabby",
- "shadowy",
- "shady",
- "shallow",
- "shameful",
- "shameless",
- "sharp",
- "shimmering",
- "shiny",
- "shocked",
- "shocking",
- "shoddy",
- "short",
- "short-term",
- "showy",
- "shrill",
- "shy",
- "sick",
- "silent",
- "silky",
- "silly",
- "silver",
- "similar",
- "simple",
- "simplistic",
- "sinful",
- "single",
- "sizzling",
- "skeletal",
- "skinny",
- "sleepy",
- "slight",
- "slim",
- "slimy",
- "slippery",
- "slow",
- "slushy",
- "small",
- "smart",
- "smoggy",
- "smooth",
- "smug",
- "snappy",
- "snarling",
- "sneaky",
- "sniveling",
- "snoopy",
- "sociable",
- "soft",
- "soggy",
- "solid",
- "somber",
- "some",
- "spherical",
- "sophisticated",
- "sore",
- "sorrowful",
- "soulful",
- "soupy",
- "sour",
- "Spanish",
- "sparkling",
- "sparse",
- "specific",
- "spectacular",
- "speedy",
- "spicy",
- "spiffy",
- "spirited",
- "spiteful",
- "splendid",
- "spotless",
- "spotted",
- "spry",
- "square",
- "squeaky",
- "squiggly",
- "stable",
- "staid",
- "stained",
- "stale",
- "standard",
- "starchy",
- "stark",
- "starry",
- "steep",
- "sticky",
- "stiff",
- "stimulating",
- "stingy",
- "stormy",
- "straight",
- "strange",
- "steel",
- "strict",
- "strident",
- "striking",
- "striped",
- "strong",
- "studious",
- "stunning",
- "stupendous",
- "stupid",
- "sturdy",
- "stylish",
- "subdued",
- "submissive",
- "substantial",
- "subtle",
- "suburban",
- "sudden",
- "sugary",
- "sunny",
- "super",
- "superb",
- "superficial",
- "superior",
- "supportive",
- "sure-footed",
- "surprised",
- "suspicious",
- "svelte",
- "sweaty",
- "sweet",
- "sweltering",
- "swift",
- "sympathetic",
- "tall",
- "talkative",
- "tame",
- "tan",
- "tangible",
- "tart",
- "tasty",
- "tattered",
- "taut",
- "tedious",
- "teeming",
- "tempting",
- "tender",
- "tense",
- "tepid",
- "terrible",
- "terrific",
- "testy",
- "thankful",
- "that",
- "these",
- "thick",
- "thin",
- "third",
- "thirsty",
- "this",
- "thorough",
- "thorny",
- "those",
- "thoughtful",
- "threadbare",
- "thrifty",
- "thunderous",
- "tidy",
- "tight",
- "timely",
- "tinted",
- "tiny",
- "tired",
- "torn",
- "total",
- "tough",
- "traumatic",
- "treasured",
- "tremendous",
- "tragic",
- "trained",
- "tremendous",
- "triangular",
- "tricky",
- "trifling",
- "trim",
- "trivial",
- "troubled",
- "true",
- "trusting",
- "trustworthy",
- "trusty",
- "truthful",
- "tubby",
- "turbulent",
- "twin",
- "ugly",
- "ultimate",
- "unacceptable",
- "unaware",
- "uncomfortable",
- "uncommon",
- "unconscious",
- "understated",
- "unequaled",
- "uneven",
- "unfinished",
- "unfit",
- "unfolded",
- "unfortunate",
- "unhappy",
- "unhealthy",
- "uniform",
- "unimportant",
- "unique",
- "united",
- "unkempt",
- "unknown",
- "unlawful",
- "unlined",
- "unlucky",
- "unnatural",
- "unpleasant",
- "unrealistic",
- "unripe",
- "unruly",
- "unselfish",
- "unsightly",
- "unsteady",
- "unsung",
- "untidy",
- "untimely",
- "untried",
- "untrue",
- "unused",
- "unusual",
- "unwelcome",
- "unwieldy",
- "unwilling",
- "unwitting",
- "unwritten",
- "upbeat",
- "upright",
- "upset",
- "urban",
- "usable",
- "used",
- "useful",
- "useless",
- "utilized",
- "utter",
- "vacant",
- "vague",
- "vain",
- "valid",
- "valuable",
- "vapid",
- "variable",
- "vast",
- "velvety",
- "venerated",
- "vengeful",
- "verifiable",
- "vibrant",
- "vicious",
- "victorious",
- "vigilant",
- "vigorous",
- "villainous",
- "violet",
- "violent",
- "virtual",
- "virtuous",
- "visible",
- "vital",
- "vivacious",
- "vivid",
- "voluminous",
- "wan",
- "warlike",
- "warm",
- "warmhearted",
- "warped",
- "wary",
- "wasteful",
- "watchful",
- "waterlogged",
- "watery",
- "wavy",
- "wealthy",
- "weak",
- "weary",
- "webbed",
- "weed",
- "weekly",
- "weepy",
- "weighty",
- "weird",
- "welcome",
- "well-documented",
- "well-groomed",
- "well-informed",
- "well-lit",
- "well-made",
- "well-off",
- "well-to-do",
- "well-worn",
- "wet",
- "which",
- "whimsical",
- "whirlwind",
- "whispered",
- "white",
- "whole",
- "whopping",
- "wicked",
- "wide",
- "wide-eyed",
- "wiggly",
- "wild",
- "willing",
- "wilted",
- "winding",
- "windy",
- "winged",
- "wiry",
- "wise",
- "witty",
- "wobbly",
- "woeful",
- "wonderful",
- "wooden",
- "woozy",
- "wordy",
- "worldly",
- "worn",
- "worried",
- "worrisome",
- "worse",
- "worst",
- "worthless",
- "worthwhile",
- "worthy",
- "wrathful",
- "wretched",
- "writhing",
- "wrong",
- "wry",
- "yawning",
- "yearly",
- "yellow",
- "yellowish",
- "young",
- "youthful",
- "yummy",
- "zany",
- "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);
- return {
- first: adj[a],
- second: noun[n],
- };
-}
-
-export function getRandomPassword(): string {
- return encodeCrock(getRandomBytes(16));
-}