summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src')
-rw-r--r--packages/bank-ui/src/Routing.tsx620
-rw-r--r--packages/bank-ui/src/app.tsx231
-rw-r--r--packages/bank-ui/src/assets/empty.pngbin0 -> 2785 bytes
-rw-r--r--packages/bank-ui/src/assets/example/id1.jpgbin0 -> 103558 bytes
-rw-r--r--packages/bank-ui/src/assets/favicon.icobin0 -> 15086 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/android-chrome-192x192.pngbin0 -> 14058 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/android-chrome-512x512.pngbin0 -> 51484 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/apple-touch-icon.pngbin0 -> 12746 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/favicon-16x16.pngbin0 -> 626 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/favicon-32x32.pngbin0 -> 1487 bytes
-rw-r--r--packages/bank-ui/src/assets/icons/languageicon.svg48
-rw-r--r--packages/bank-ui/src/assets/icons/mstile-150x150.pngbin0 -> 9050 bytes
-rw-r--r--packages/bank-ui/src/assets/logo-2021.svg9
-rw-r--r--packages/bank-ui/src/assets/logo-white.svg45
-rw-r--r--packages/bank-ui/src/assets/logo.jpegbin0 -> 39336 bytes
-rw-r--r--packages/bank-ui/src/components/Cashouts/index.ts85
-rw-r--r--packages/bank-ui/src/components/Cashouts/state.ts51
-rw-r--r--packages/bank-ui/src/components/Cashouts/stories.tsx29
-rw-r--r--packages/bank-ui/src/components/Cashouts/test.ts68
-rw-r--r--packages/bank-ui/src/components/Cashouts/views.tsx218
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/index.ts56
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/state.ts25
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/stories.tsx29
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/test.ts28
-rw-r--r--packages/bank-ui/src/components/EmptyComponentExample/views.tsx25
-rw-r--r--packages/bank-ui/src/components/ErrorLoadingWithDebug.tsx24
-rw-r--r--packages/bank-ui/src/components/QR.tsx51
-rw-r--r--packages/bank-ui/src/components/Time.tsx80
-rw-r--r--packages/bank-ui/src/components/Transactions/index.ts84
-rw-r--r--packages/bank-ui/src/components/Transactions/state.ts90
-rw-r--r--packages/bank-ui/src/components/Transactions/stories.tsx44
-rw-r--r--packages/bank-ui/src/components/Transactions/test.ts202
-rw-r--r--packages/bank-ui/src/components/Transactions/views.tsx252
-rw-r--r--packages/bank-ui/src/components/index.examples.ts17
-rw-r--r--packages/bank-ui/src/context/settings.ts44
-rw-r--r--packages/bank-ui/src/context/wallet-integration.ts83
-rw-r--r--packages/bank-ui/src/declaration.d.ts35
-rw-r--r--packages/bank-ui/src/hooks/account.ts313
-rw-r--r--packages/bank-ui/src/hooks/bank-state.ts185
-rw-r--r--packages/bank-ui/src/hooks/form.ts124
-rw-r--r--packages/bank-ui/src/hooks/preferences.ts111
-rw-r--r--packages/bank-ui/src/hooks/regional.ts507
-rw-r--r--packages/bank-ui/src/hooks/session.ts134
-rw-r--r--packages/bank-ui/src/i18n/bank.pot1742
-rw-r--r--packages/bank-ui/src/i18n/de.po1780
-rw-r--r--packages/bank-ui/src/i18n/es.po2065
-rw-r--r--packages/bank-ui/src/i18n/fr.po1752
-rw-r--r--packages/bank-ui/src/i18n/it.po1843
-rw-r--r--packages/bank-ui/src/i18n/poheader26
-rw-r--r--packages/bank-ui/src/i18n/ru.po1794
-rw-r--r--packages/bank-ui/src/i18n/strings.ts2295
-rw-r--r--packages/bank-ui/src/i18n/uk.po1743
-rw-r--r--packages/bank-ui/src/index.html41
-rw-r--r--packages/bank-ui/src/index.tsx27
-rw-r--r--packages/bank-ui/src/manifest.json21
-rw-r--r--packages/bank-ui/src/pages/AccountPage/index.ts135
-rw-r--r--packages/bank-ui/src/pages/AccountPage/state.ts122
-rw-r--r--packages/bank-ui/src/pages/AccountPage/stories.tsx29
-rw-r--r--packages/bank-ui/src/pages/AccountPage/test.ts31
-rw-r--r--packages/bank-ui/src/pages/AccountPage/views.tsx156
-rw-r--r--packages/bank-ui/src/pages/BankFrame.stories.tsx29
-rw-r--r--packages/bank-ui/src/pages/BankFrame.tsx368
-rw-r--r--packages/bank-ui/src/pages/LoginForm.tsx230
-rw-r--r--packages/bank-ui/src/pages/OperationState/index.ts157
-rw-r--r--packages/bank-ui/src/pages/OperationState/state.ts234
-rw-r--r--packages/bank-ui/src/pages/OperationState/stories.tsx29
-rw-r--r--packages/bank-ui/src/pages/OperationState/test.ts31
-rw-r--r--packages/bank-ui/src/pages/OperationState/views.tsx447
-rw-r--r--packages/bank-ui/src/pages/PaymentOptions.stories.tsx35
-rw-r--r--packages/bank-ui/src/pages/PaymentOptions.tsx239
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.stories.tsx35
-rw-r--r--packages/bank-ui/src/pages/PaytoWireTransferForm.tsx924
-rw-r--r--packages/bank-ui/src/pages/ProfileNavigation.tsx202
-rw-r--r--packages/bank-ui/src/pages/PublicHistoriesPage.tsx98
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.stories.tsx32
-rw-r--r--packages/bank-ui/src/pages/QrCodeSection.tsx152
-rw-r--r--packages/bank-ui/src/pages/RegistrationPage.tsx423
-rw-r--r--packages/bank-ui/src/pages/ShowNotifications.tsx55
-rw-r--r--packages/bank-ui/src/pages/SolveChallengePage.tsx793
-rw-r--r--packages/bank-ui/src/pages/WalletWithdrawForm.tsx404
-rw-r--r--packages/bank-ui/src/pages/WireTransfer.tsx119
-rw-r--r--packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx425
-rw-r--r--packages/bank-ui/src/pages/WithdrawalOperationPage.tsx73
-rw-r--r--packages/bank-ui/src/pages/WithdrawalQRCode.tsx310
-rw-r--r--packages/bank-ui/src/pages/account/CashoutListForAccount.tsx89
-rw-r--r--packages/bank-ui/src/pages/account/ShowAccountDetails.tsx500
-rw-r--r--packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx319
-rw-r--r--packages/bank-ui/src/pages/admin/AccountForm.tsx859
-rw-r--r--packages/bank-ui/src/pages/admin/AccountList.tsx234
-rw-r--r--packages/bank-ui/src/pages/admin/AdminHome.tsx622
-rw-r--r--packages/bank-ui/src/pages/admin/CreateNewAccount.tsx226
-rw-r--r--packages/bank-ui/src/pages/admin/DownloadStats.tsx588
-rw-r--r--packages/bank-ui/src/pages/admin/RemoveAccount.tsx273
-rw-r--r--packages/bank-ui/src/pages/index.stories.tsx20
-rw-r--r--packages/bank-ui/src/pages/regional/ConversionConfig.tsx1170
-rw-r--r--packages/bank-ui/src/pages/regional/CreateCashout.tsx732
-rw-r--r--packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx194
-rw-r--r--packages/bank-ui/src/pages/rnd.ts2907
-rw-r--r--packages/bank-ui/src/scss/main.css3
-rw-r--r--packages/bank-ui/src/settings.json11
-rw-r--r--packages/bank-ui/src/settings.ts112
-rw-r--r--packages/bank-ui/src/stories.test.ts83
-rw-r--r--packages/bank-ui/src/stories.tsx41
-rw-r--r--packages/bank-ui/src/utils.ts439
104 files changed, 34815 insertions, 0 deletions
diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx
new file mode 100644
index 000000000..380b267a2
--- /dev/null
+++ b/packages/bank-ui/src/Routing.tsx
@@ -0,0 +1,620 @@
+/*
+ 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 {
+ LocalNotificationBanner,
+ urlPattern,
+ useBankCoreApiContext,
+ useCurrentLocation,
+ useLocalNotification,
+ useNavigationContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+
+import {
+ AbsoluteTime,
+ AccessToken,
+ HttpStatusCode,
+ TranslatedString,
+ assertUnreachable,
+ createRFC8959AccessTokenEncoded
+} from "@gnu-taler/taler-util";
+import { useEffect } from "preact/hooks";
+import { useSessionState } from "./hooks/session.js";
+import { AccountPage } from "./pages/AccountPage/index.js";
+import { BankFrame } from "./pages/BankFrame.js";
+import { LoginForm } from "./pages/LoginForm.js";
+import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js";
+import { RegistrationPage } from "./pages/RegistrationPage.js";
+import { ShowNotifications } from "./pages/ShowNotifications.js";
+import { SolveChallengePage } from "./pages/SolveChallengePage.js";
+import { WireTransfer } from "./pages/WireTransfer.js";
+import { WithdrawalOperationPage } from "./pages/WithdrawalOperationPage.js";
+import { CashoutListForAccount } from "./pages/account/CashoutListForAccount.js";
+import { ShowAccountDetails } from "./pages/account/ShowAccountDetails.js";
+import { UpdateAccountPassword } from "./pages/account/UpdateAccountPassword.js";
+import { AdminHome } from "./pages/admin/AdminHome.js";
+import { CreateNewAccount } from "./pages/admin/CreateNewAccount.js";
+import { DownloadStats } from "./pages/admin/DownloadStats.js";
+import { RemoveAccount } from "./pages/admin/RemoveAccount.js";
+import { ConversionConfig } from "./pages/regional/ConversionConfig.js";
+import { CreateCashout } from "./pages/regional/CreateCashout.js";
+import { ShowCashoutDetails } from "./pages/regional/ShowCashoutDetails.js";
+
+export function Routing(): VNode {
+ const session = useSessionState();
+
+ if (session.state.status === "loggedIn") {
+ const { isUserAdministrator, username } = session.state;
+ return (
+ <BankFrame
+ account={username}
+ routeAccountDetails={privatePages.myAccountDetails}
+ >
+ <PrivateRouting username={username} isAdmin={isUserAdministrator} />
+ </BankFrame>
+ );
+ }
+ return (
+ <BankFrame>
+ <PublicRounting
+ onLoggedUser={(username, token) => {
+ session.logIn({ username, token: token });
+ }}
+ />
+ </BankFrame>
+ );
+}
+
+const publicPages = {
+ login: urlPattern(/\/login/, () => "#/login"),
+ register: urlPattern(/\/register/, () => "#/register"),
+ publicAccounts: urlPattern(/\/public-accounts/, () => "#/public-accounts"),
+ operationDetails: urlPattern<{ wopid: string }>(
+ /\/operation\/(?<wopid>[a-zA-Z0-9-]+)/,
+ ({ wopid }) => `#/operation/${wopid}`,
+ ),
+ solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
+};
+
+function PublicRounting({
+ onLoggedUser,
+}: {
+ onLoggedUser: (username: string, token: AccessToken) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const location = useCurrentLocation(publicPages);
+ const { navigateTo } = useNavigationContext();
+ const { config, lib } = useBankCoreApiContext();
+ const [notification, notify, handleError] = useLocalNotification();
+
+ useEffect(() => {
+ if (location === undefined) {
+ navigateTo(publicPages.login.url({}));
+ }
+ }, [location]);
+
+ if (location === undefined) {
+ return <Fragment />;
+ }
+
+ async function doAutomaticLogin(username: string, password: string) {
+ await handleError(async () => {
+ const resp = await lib
+ .auth(username)
+ .createAccessTokenBasic(username, password, {
+ scope: "readwrite",
+ duration: { d_us: "forever" },
+ refreshable: true,
+ });
+ if (resp.type === "ok") {
+ onLoggedUser(username, createRFC8959AccessTokenEncoded(resp.body.access_token));
+ } else {
+ switch (resp.case) {
+ case HttpStatusCode.Unauthorized:
+ return notify({
+ type: "error",
+ title: i18n.str`Wrong credentials for "${username}"`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ switch (location.name) {
+ case "login": {
+ return (
+ <Fragment>
+ <div class="sm:mx-auto sm:w-full sm:max-w-sm">
+ <h2 class="text-center text-2xl font-bold leading-9 tracking-tight text-gray-900">{i18n.str`Welcome to ${config.bank_name}!`}</h2>
+ </div>
+ <LoginForm routeRegister={publicPages.register} />
+ </Fragment>
+ );
+ }
+ case "publicAccounts": {
+ return <PublicHistoriesPage />;
+ }
+ case "operationDetails": {
+ return (
+ <WithdrawalOperationPage
+ operationId={location.values.wopid}
+ routeWithdrawalDetails={publicPages.operationDetails}
+ purpose="after-confirmation"
+ onOperationAborted={() => navigateTo(publicPages.login.url({}))}
+ routeClose={publicPages.login}
+ onAuthorizationRequired={() =>
+ navigateTo(publicPages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "register": {
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <RegistrationPage
+ onRegistrationSuccesful={doAutomaticLogin}
+ routeCancel={publicPages.login}
+ />
+ </Fragment>
+ );
+ }
+ case "solveSecondFactor": {
+ return (
+ <SolveChallengePage
+ onChallengeCompleted={() => navigateTo(publicPages.login.url({}))}
+ routeClose={publicPages.login}
+ />
+ );
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
+
+export const privatePages = {
+ homeChargeWallet: urlPattern(
+ /\/account\/charge-wallet/,
+ () => "#/account/charge-wallet",
+ ),
+ homeWireTransfer: urlPattern<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>(/\/account\/wire-transfer/, () => "#/account/wire-transfer"),
+ home: urlPattern(/\/account/, () => "#/account"),
+ notifications: urlPattern(/\/notifications/, () => "#/notifications"),
+ solveSecondFactor: urlPattern(/\/2fa/, () => "#/2fa"),
+ cashoutCreate: urlPattern(/\/new-cashout/, () => "#/new-cashout"),
+ cashoutDetails: urlPattern<{ cid: string }>(
+ /\/cashout\/(?<cid>[a-zA-Z0-9]+)/,
+ ({ cid }) => `#/cashout/${cid}`,
+ ),
+ wireTranserCreate: urlPattern<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>(
+ /\/wire-transfer\/(?<account>[a-zA-Z0-9]+)/,
+ ({ account }) => `#/wire-transfer/${account}`,
+ ),
+ publicAccountList: urlPattern(/\/public-accounts/, () => "#/public-accounts"),
+ statsDownload: urlPattern(/\/download-stats/, () => "#/download-stats"),
+ accountCreate: urlPattern(/\/new-account/, () => "#/new-account"),
+ myAccountDelete: urlPattern(
+ /\/delete-my-account/,
+ () => "#/delete-my-account",
+ ),
+ myAccountDetails: urlPattern(/\/my-profile/, () => "#/my-profile"),
+ myAccountPassword: urlPattern(/\/my-password/, () => "#/my-password"),
+ myAccountCashouts: urlPattern(/\/my-cashouts/, () => "#/my-cashouts"),
+ conversionConfig: urlPattern(/\/conversion/, () => "#/conversion"),
+ accountDetails: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/details/,
+ ({ account }) => `#/profile/${account}/details`,
+ ),
+ accountChangePassword: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/change-password/,
+ ({ account }) => `#/profile/${account}/change-password`,
+ ),
+ accountDelete: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/delete/,
+ ({ account }) => `#/profile/${account}/delete`,
+ ),
+ accountCashouts: urlPattern<{ account: string }>(
+ /\/profile\/(?<account>[a-zA-Z0-9_-]+)\/cashouts/,
+ ({ account }) => `#/profile/${account}/cashouts`,
+ ),
+ startOperation: urlPattern<{ wopid: string }>(
+ /\/start-operation\/(?<wopid>[a-zA-Z0-9-]+)/,
+ ({ wopid }) => `#/start-operation/${wopid}`,
+ ),
+ operationDetails: urlPattern<{ wopid: string }>(
+ /\/operation\/(?<wopid>[a-zA-Z0-9-]+)/,
+ ({ wopid }) => `#/operation/${wopid}`,
+ ),
+};
+
+function PrivateRouting({
+ username,
+ isAdmin,
+}: {
+ username: string;
+ isAdmin: boolean;
+}): VNode {
+ const { navigateTo } = useNavigationContext();
+ const location = useCurrentLocation(privatePages);
+ useEffect(() => {
+ if (location === undefined) {
+ navigateTo(privatePages.home.url({}));
+ }
+ }, [location]);
+
+ if (location === undefined) {
+ return <Fragment />;
+ }
+
+ switch (location.name) {
+ case "operationDetails": {
+ return (
+ <WithdrawalOperationPage
+ operationId={location.values.wopid}
+ routeWithdrawalDetails={privatePages.operationDetails}
+ purpose="after-confirmation"
+ onOperationAborted={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "startOperation": {
+ return (
+ <WithdrawalOperationPage
+ operationId={location.values.wopid}
+ routeWithdrawalDetails={privatePages.operationDetails}
+ purpose="after-creation"
+ onOperationAborted={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "solveSecondFactor": {
+ return (
+ <SolveChallengePage
+ onChallengeCompleted={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "publicAccountList": {
+ return <PublicHistoriesPage />;
+ }
+ case "statsDownload": {
+ return <DownloadStats routeCancel={privatePages.home} />;
+ }
+ case "accountCreate": {
+ return (
+ <CreateNewAccount
+ routeCancel={privatePages.home}
+ onCreateSuccess={() => navigateTo(privatePages.home.url({}))}
+ />
+ );
+ }
+ case "accountDetails": {
+ return (
+ <ShowAccountDetails
+ account={location.values.account}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeHere={privatePages.accountDetails}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "accountChangePassword": {
+ return (
+ <UpdateAccountPassword
+ focus
+ account={location.values.account}
+ routeHere={privatePages.accountChangePassword}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "accountDelete": {
+ return (
+ <RemoveAccount
+ account={location.values.account}
+ routeHere={privatePages.accountDelete}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCancel={privatePages.home}
+ />
+ );
+ }
+ case "accountCashouts": {
+ return (
+ <CashoutListForAccount
+ account={location.values.account}
+ routeCreateCashout={privatePages.cashoutCreate}
+ routeCashoutDetails={privatePages.cashoutDetails}
+ routeClose={privatePages.home}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onCashout={() =>
+ navigateTo(privatePages.home.url({}))
+ }
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ />
+ );
+ }
+ case "myAccountDelete": {
+ return (
+ <RemoveAccount
+ account={username}
+ routeHere={privatePages.accountDelete}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCancel={privatePages.home}
+ />
+ );
+ }
+ case "myAccountDetails": {
+ return (
+ <ShowAccountDetails
+ account={username}
+ routeHere={privatePages.accountDetails}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeConversionConfig={privatePages.conversionConfig}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "myAccountPassword": {
+ return (
+ <UpdateAccountPassword
+ focus
+ account={username}
+ routeHere={privatePages.accountChangePassword}
+ onUpdateSuccess={() => navigateTo(privatePages.home.url({}))}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "myAccountCashouts": {
+ return (
+ <CashoutListForAccount
+ account={username}
+ routeCashoutDetails={privatePages.cashoutDetails}
+ routeCreateCashout={privatePages.cashoutCreate}
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ onCashout={() =>
+ navigateTo(privatePages.home.url({}))
+ }
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "home": {
+ if (isAdmin) {
+ return (
+ <AdminHome
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCreate={privatePages.accountCreate}
+ routeRemoveAccount={privatePages.accountDelete}
+ routeShowAccount={privatePages.accountDetails}
+ routeShowCashoutsAccount={privatePages.accountCashouts}
+ routeUpdatePasswordAccount={privatePages.accountChangePassword}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routeDownloadStats={privatePages.statsDownload}
+ />
+ );
+ }
+ return (
+ <AccountPage
+ account={username}
+ tab={undefined}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routePublicAccounts={privatePages.publicAccountList}
+ routeOperationDetails={privatePages.startOperation}
+ routeChargeWallet={privatePages.homeChargeWallet}
+ routeWireTransfer={privatePages.homeWireTransfer}
+ routeSolveSecondFactor={privatePages.solveSecondFactor}
+ routeCashout={privatePages.myAccountCashouts}
+ routeClose={privatePages.home}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onOperationCreated={(wopid) =>
+ navigateTo(privatePages.startOperation.url({ wopid }))
+ }
+ />
+ );
+ }
+ case "cashoutCreate": {
+ return (
+ <CreateCashout
+ account={username}
+ routeHere={privatePages.cashoutCreate}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onCashout={() => navigateTo(privatePages.home.url({}))}
+ routeClose={privatePages.home}
+ />
+ );
+ }
+ case "cashoutDetails": {
+ return (
+ <ShowCashoutDetails
+ id={location.values.cid}
+ routeClose={privatePages.myAccountCashouts}
+ />
+ );
+ }
+ case "wireTranserCreate": {
+ return (
+ <WireTransfer
+ toAccount={location.values.account}
+ withAmount={location.values.amount}
+ withSubject={location.values.subject}
+ routeHere={privatePages.wireTranserCreate}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ routeCancel={privatePages.home}
+ onSuccess={() => navigateTo(privatePages.home.url({}))}
+ />
+ );
+ }
+ case "homeChargeWallet": {
+ return (
+ <AccountPage
+ account={username}
+ tab="charge-wallet"
+ routeChargeWallet={privatePages.homeChargeWallet}
+ routeWireTransfer={privatePages.homeWireTransfer}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routePublicAccounts={privatePages.publicAccountList}
+ routeOperationDetails={privatePages.startOperation}
+ routeCashout={privatePages.myAccountCashouts}
+ routeSolveSecondFactor={privatePages.solveSecondFactor}
+ routeClose={privatePages.home}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onOperationCreated={(wopid) =>
+ navigateTo(privatePages.startOperation.url({ wopid }))
+ }
+ />
+ );
+ }
+ case "conversionConfig": {
+ return (
+ <ConversionConfig
+ routeMyAccountCashout={privatePages.myAccountCashouts}
+ routeMyAccountDelete={privatePages.myAccountDelete}
+ routeMyAccountDetails={privatePages.myAccountDetails}
+ routeMyAccountPassword={privatePages.myAccountPassword}
+ routeConversionConfig={privatePages.conversionConfig}
+ routeCancel={privatePages.home}
+ onUpdateSuccess={() => {
+ navigateTo(privatePages.home.url({}));
+ }}
+ />
+ );
+ }
+ case "homeWireTransfer": {
+ return (
+ <AccountPage
+ account={username}
+ tab="wire-transfer"
+ routeChargeWallet={privatePages.homeChargeWallet}
+ routeWireTransfer={privatePages.homeWireTransfer}
+ routeCreateWireTransfer={privatePages.wireTranserCreate}
+ routePublicAccounts={privatePages.publicAccountList}
+ routeOperationDetails={privatePages.startOperation}
+ routeSolveSecondFactor={privatePages.solveSecondFactor}
+ routeCashout={privatePages.myAccountCashouts}
+ routeClose={privatePages.home}
+ onClose={() => navigateTo(privatePages.home.url({}))}
+ onAuthorizationRequired={() =>
+ navigateTo(privatePages.solveSecondFactor.url({}))
+ }
+ onOperationCreated={(wopid) =>
+ navigateTo(privatePages.startOperation.url({ wopid }))
+ }
+ />
+ );
+ }
+ case "notifications": {
+ return <ShowNotifications />;
+ }
+ default:
+ assertUnreachable(location);
+ }
+}
diff --git a/packages/bank-ui/src/app.tsx b/packages/bank-ui/src/app.tsx
new file mode 100644
index 000000000..1ea8c69ca
--- /dev/null
+++ b/packages/bank-ui/src/app.tsx
@@ -0,0 +1,231 @@
+/*
+ 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 {
+ CacheEvictor,
+ TalerBankConversionCacheEviction,
+ TalerCoreBankCacheEviction,
+ assertUnreachable,
+ canonicalizeBaseUrl,
+ getGlobalLogLevel,
+ setGlobalLogLevelFromString,
+} from "@gnu-taler/taler-util";
+import {
+ BankApiProvider,
+ BrowserHashNavigationProvider,
+ Loading,
+ TalerWalletIntegrationBrowserProvider,
+ TranslationProvider,
+} from "@gnu-taler/web-util/browser";
+import { h } from "preact";
+import { useEffect, useState } from "preact/hooks";
+import { SWRConfig } from "swr";
+import { Routing } from "./Routing.js";
+import { SettingsProvider } from "./context/settings.js";
+import { strings } from "./i18n/strings.js";
+import { BankFrame } from "./pages/BankFrame.js";
+import { UiSettings, fetchSettings } from "./settings.js";
+import {
+ revalidateAccountDetails,
+ revalidatePublicAccounts,
+ revalidateTransactions,
+} from "./hooks/account.js";
+import {
+ revalidateBusinessAccounts,
+ revalidateCashouts,
+ revalidateConversionInfo,
+} from "./hooks/regional.js";
+const WITH_LOCAL_STORAGE_CACHE = false;
+
+export function App() {
+ const [settings, setSettings] = useState<UiSettings>();
+ useEffect(() => {
+ fetchSettings(setSettings);
+ }, []);
+ if (!settings) return <Loading />;
+
+ const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL);
+ return (
+ <SettingsProvider value={settings}>
+ <TranslationProvider
+ source={strings}
+ completeness={{
+ es: strings["es"].completeness,
+ de: strings["de"].completeness,
+ }}
+ >
+ <BankApiProvider
+ baseUrl={new URL("/", baseUrl)}
+ frameOnError={BankFrame}
+ evictors={{
+ bank: evictBankSwrCache,
+ conversion: evictConversionSwrCache,
+ }}
+ >
+ <SWRConfig
+ value={{
+ provider: WITH_LOCAL_STORAGE_CACHE
+ ? localStorageProvider
+ : undefined,
+ // normally, do not revalidate
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ revalidateIfStale: false,
+ revalidateOnMount: undefined,
+ focusThrottleInterval: undefined,
+
+ // normally, do not refresh
+ refreshInterval: undefined,
+ dedupingInterval: 2000,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+
+ // ignore errors
+ shouldRetryOnError: false,
+ errorRetryCount: 0,
+ errorRetryInterval: undefined,
+
+ // do not go to loading again if already has data
+ keepPreviousData: true,
+ }}
+ >
+ <TalerWalletIntegrationBrowserProvider>
+ <BrowserHashNavigationProvider>
+ <Routing />
+ </BrowserHashNavigationProvider>
+ </TalerWalletIntegrationBrowserProvider>
+ </SWRConfig>
+ </BankApiProvider>
+ </TranslationProvider>
+ </SettingsProvider>
+ );
+}
+
+// @ts-expect-error creating a new property for window object
+window.setGlobalLogLevelFromString = setGlobalLogLevelFromString;
+// @ts-expect-error creating a new property for window object
+window.getGlobalLevel = getGlobalLogLevel;
+
+function localStorageProvider(): Map<unknown, unknown> {
+ const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]"));
+
+ window.addEventListener("beforeunload", () => {
+ const appCache = JSON.stringify(Array.from(map.entries()));
+ localStorage.setItem("app-cache", appCache);
+ });
+ return map;
+}
+
+function getInitialBackendBaseURL(
+ backendFromSettings: string | undefined,
+): string {
+ const overrideUrl =
+ typeof localStorage !== "undefined"
+ ? localStorage.getItem("corebank-api-base-url")
+ : undefined;
+ let result: string;
+
+ if (!overrideUrl) {
+ // normal path
+ if (!backendFromSettings) {
+ console.error(
+ "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
+ );
+ result = window.origin;
+ } else {
+ result = backendFromSettings;
+ }
+ } else {
+ // testing/development path
+ result = overrideUrl;
+ }
+ try {
+ return canonicalizeBaseUrl(result);
+ } catch (e) {
+ // fall back
+ return canonicalizeBaseUrl(window.origin);
+ }
+}
+
+const evictBankSwrCache: CacheEvictor<TalerCoreBankCacheEviction> = {
+ async notifySuccess(op) {
+ switch (op) {
+ case TalerCoreBankCacheEviction.DELETE_ACCOUNT: {
+ await Promise.all([
+ revalidatePublicAccounts(),
+ revalidateBusinessAccounts(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.CREATE_ACCOUNT: {
+ // admin balance change on new account
+ await Promise.all([
+ revalidateAccountDetails(),
+ revalidateTransactions(),
+ revalidatePublicAccounts(),
+ revalidateBusinessAccounts(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.UPDATE_ACCOUNT: {
+ await Promise.all([revalidateAccountDetails()]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.CREATE_TRANSACTION: {
+ await Promise.all([
+ revalidateAccountDetails(),
+ revalidateTransactions(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.CONFIRM_WITHDRAWAL: {
+ await Promise.all([
+ revalidateAccountDetails(),
+ revalidateTransactions(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.CREATE_CASHOUT: {
+ await Promise.all([
+ revalidateAccountDetails(),
+ revalidateCashouts(),
+ revalidateTransactions(),
+ ]);
+ return;
+ }
+ case TalerCoreBankCacheEviction.UPDATE_PASSWORD:
+ case TalerCoreBankCacheEviction.ABORT_WITHDRAWAL:
+ case TalerCoreBankCacheEviction.CREATE_WITHDRAWAL:
+ return;
+ default:
+ assertUnreachable(op);
+ }
+ },
+};
+
+const evictConversionSwrCache: CacheEvictor<TalerBankConversionCacheEviction> =
+ {
+ async notifySuccess(op) {
+ switch (op) {
+ case TalerBankConversionCacheEviction.UPDATE_RATE: {
+ await revalidateConversionInfo();
+ return;
+ }
+ default:
+ assertUnreachable(op);
+ }
+ },
+ };
diff --git a/packages/bank-ui/src/assets/empty.png b/packages/bank-ui/src/assets/empty.png
new file mode 100644
index 000000000..5120d3138
--- /dev/null
+++ b/packages/bank-ui/src/assets/empty.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/example/id1.jpg b/packages/bank-ui/src/assets/example/id1.jpg
new file mode 100644
index 000000000..5d022a379
--- /dev/null
+++ b/packages/bank-ui/src/assets/example/id1.jpg
Binary files differ
diff --git a/packages/bank-ui/src/assets/favicon.ico b/packages/bank-ui/src/assets/favicon.ico
new file mode 100644
index 000000000..07419145b
--- /dev/null
+++ b/packages/bank-ui/src/assets/favicon.ico
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/android-chrome-192x192.png b/packages/bank-ui/src/assets/icons/android-chrome-192x192.png
new file mode 100644
index 000000000..93ebe2e2c
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/android-chrome-192x192.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/android-chrome-512x512.png b/packages/bank-ui/src/assets/icons/android-chrome-512x512.png
new file mode 100644
index 000000000..52d1623ea
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/android-chrome-512x512.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/apple-touch-icon.png b/packages/bank-ui/src/assets/icons/apple-touch-icon.png
new file mode 100644
index 000000000..254e4bb4d
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/apple-touch-icon.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/favicon-16x16.png b/packages/bank-ui/src/assets/icons/favicon-16x16.png
new file mode 100644
index 000000000..e81177dcb
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/favicon-16x16.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/favicon-32x32.png b/packages/bank-ui/src/assets/icons/favicon-32x32.png
new file mode 100644
index 000000000..40e9b5b47
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/favicon-32x32.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/icons/languageicon.svg b/packages/bank-ui/src/assets/icons/languageicon.svg
new file mode 100644
index 000000000..22d58da65
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/languageicon.svg
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 2411.2 2794" style="enable-background:new 0 0 2411.2 2794;" xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#FFFFFF;}
+ .st1{fill-rule:evenodd;clip-rule:evenodd;}
+ .st2{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+</style>
+<g id="Layer_2">
+</g>
+<g id="Layer_x5F_1_x5F_1">
+ <g>
+ <polygon points="1204.6,359.2 271.8,30 271.8,2060.1 1204.6,1758.3 "/>
+ <polygon class="st0" points="1182.2,358.1 2150.6,29 2150.6,2059 1182.2,1757.3 "/>
+ <polygon class="st0" points="30,2415.4 1182.2,2031.4 1182.2,357.9 30,742 "/>
+ <polygon points="1707.2,2440.7 1870.5,2709.4 1956.6,2459.8 "/>
+ <g>
+ <path d="M421.7,934.8c-6.1-6,8,49.1,27.6,68.9c34.8,35.1,61.9,39.6,76.4,40.2c32,1.3,71.5-8,94.9-17.8
+ c22.7-9.7,62.4-30,77.5-59.6c3.2-6.3,11.9-17,6.4-43.2c-4.2-20.2-17-27.3-32.7-26.2c-15.7,1.1-63.2,13.7-86.1,20.8
+ c-23,7-70.3,21.4-90.9,25.8C474.3,948.2,429,941.7,421.7,934.8z"/>
+ <path d="M1003.1,1593.7c-9.1-3.3-196.9-81.1-223.6-93.9c-21.8-10.5-75.2-33.1-100.4-43.3c70.8-109.2,115.5-191.6,121.5-204.1
+ c11-23,86-169.6,87.7-178.7c1.7-9.1,3.8-42.9,2.2-51c-1.7-8.2-29.1,7.6-66.4,20.2c-37.4,12.6-108.4,58.8-135.8,64.6
+ c-27.5,5.7-115.5,39.1-160.5,54c-45,14.9-130.2,40.9-165.2,50.4c-35.1,9.5-65.7,10.2-85.3,16.2c0,0,2.6,27.5,7.8,35.7
+ c5.2,8.2,23.7,28.4,45.3,34.1c21.6,5.7,57.3,3.4,73.6-0.3c16.3-3.8,44.4-17.5,48.2-23.6c3.8-6.1-2-24.9,4.5-30.6
+ c6.5-5.6,92.2-25.7,124.6-35.4c32.4-10,156.3-52.6,173.1-50.5c-5.3,17.7-105,215.1-137.1,274c-32.1,58.9-218.6,318-258.3,363.6
+ c-30.1,34.7-103.2,123.5-128.5,143.6c6.4,1.8,51.6-2.1,59.9-7.2c51.3-31.6,136.9-138.1,164.4-170.5
+ c81.9-96,153.8-196.8,210.8-283.4h0.1c11.1,4.6,100.9,77.8,124.4,94c23.4,16.2,115.9,67.8,136,76.4c20,8.7,97.1,44.2,100.3,32.2
+ C1029.4,1668,1012.2,1597.1,1003.1,1593.7z"/>
+ </g>
+ <path class="st1" d="M569,2572c18,11,35,20,54,29c38,19,81,39,122,54c56,21,112,38,168,51c31,7,65,13,98,18c3,0,92,11,110,11h90
+ c35-3,68-5,103-10c28-4,59-9,89-16c22-5,45-10,67-17c21-6,45-14,68-22c15-5,31-12,47-18c13-6,29-13,44-19c18-8,39-19,59-29
+ c16-8,34-18,51-28c13-7,43-30,59-30c18,0,30,16,30,30c0,29-39,38-57,51c-19,13-42,23-62,34c-40,21-81,39-120,54
+ c-51,19-107,37-157,49c-19,4-38,9-57,12c-10,2-114,18-143,18h-132c-35-3-72-7-107-12c-31-5-64-11-95-18c-24-5-50-12-73-19
+ c-40-11-79-25-117-40c-69-26-141-60-209-105c-12-8-13-16-13-25c0-15,11-29,29-29C531,2546,563,2569,569,2572z"/>
+ <path class="st1" d="M1151,2009L61,2372V764l1090-363V2009z M1212,354v1680c-1,5-3,10-7,15c-2,3-6,7-9,8c-25,10-1151,388-1166,388
+ c-12,0-23-8-29-21c0-1-1-2-1-4V739c2-5,3-12,7-16c8-11,22-13,31-16c17-6,1126-378,1142-378C1190,329,1212,336,1212,354z"/>
+ <path class="st1" d="M2120,2017l-907-282V380l907-308V2017z M2181,32v2023c-1,23-17,33-32,33c-13,0-107-32-123-37
+ c-126-39-253-78-378-117c-28-9-57-18-84-27c-24-7-50-15-74-23c-107-33-216-66-323-102c-4-1-14-15-14-18V351c2-5,4-11,9-15
+ c8-9,351-123,486-168c36-13,487-168,501-168C2167,0,2181,13,2181,32z"/>
+ <polygon points="2411.2,2440.7 1199.5,2054.5 1204.6,373.2 2411.2,757.2 "/>
+ <g>
+ <path class="st2" d="M1800.3,1124.6L1681.4,1412l218.6,66.3L1800.3,1124.6z M1729,853.2l156.1,47.3l284.4,1025l-160.3-48.7
+ l-57.6-210.4L1620.2,1566l-71.3,171.4l-160.4-48.7L1729,853.2z"/>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/packages/bank-ui/src/assets/icons/mstile-150x150.png b/packages/bank-ui/src/assets/icons/mstile-150x150.png
new file mode 100644
index 000000000..9cfb889be
--- /dev/null
+++ b/packages/bank-ui/src/assets/icons/mstile-150x150.png
Binary files differ
diff --git a/packages/bank-ui/src/assets/logo-2021.svg b/packages/bank-ui/src/assets/logo-2021.svg
new file mode 100644
index 000000000..8c5ff3e5b
--- /dev/null
+++ b/packages/bank-ui/src/assets/logo-2021.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 201 90">
+ <g fill="#0042b3" fill-rule="evenodd" stroke-width=".3">
+ <path d="M86.7 1.1c15.6 0 29 9.4 36 23.2h-5.9A35.1 35.1 0 0086.7 6.5C67 6.5 51 23.6 51 44.7c0 10.4 3.8 19.7 10 26.6a31.4 31.4 0 01-4.2 3A45.2 45.2 0 0146 44.7c0-24 18.2-43.6 40.7-43.6zm35.8 64.3a40.4 40.4 0 01-39 22.8c3-1.5 6-3.5 8.6-5.7a35.6 35.6 0 0024.6-17.1z" />
+ <path d="M64.2 1.1l3.1.1c-3 1.6-5.9 3.5-8.5 5.8a37.5 37.5 0 00-30.2 37.7c0 14.3 7.3 26.7 18 33.3a29.6 29.6 0 01-8.5.2c-9-8-14.6-20-14.6-33.5 0-24 18.2-43.6 40.7-43.6zm5.4 81.4a35.6 35.6 0 0024.6-17.1h5.9a40.4 40.4 0 01-39 22.8c3-1.5 5.9-3.5 8.5-5.7zm24.8-58.2a37 37 0 00-12.6-12.8 29.6 29.6 0 018.5-.2c4 3.6 7.4 8 9.9 13z" />
+ <path d="M41.8 1.1c1 0 2 0 3.1.2-3 1.5-5.9 3.4-8.5 5.6A37.5 37.5 0 006.1 44.7c0 21.1 16 38.3 35.7 38.3 12.6 0 23.6-7 30-17.6h5.8a40.4 40.4 0 01-35.8 23C19.3 88.4 1 68.8 1 44.7c0-24 18.2-43.6 40.7-43.6zm30.1 23.2a38.1 38.1 0 00-4.5-6.1c1.3-1.2 2.7-2.2 4.3-3 2.3 2.7 4.4 5.8 6 9.1z" />
+ </g>
+ <path d="M76.1 34.4h9.2v-5H61.9v5H71v26h5.1zM92.6 52.9h13.7l3 7.4h5.3l-12.7-31.2h-4.7L84.5 60.3h5.2zm11.8-4.9h-9.9l5-12.4zM123.8 29.4h-4.6v31h20.6v-5h-16zM166.5 29.4H145v31h21.6v-5H150v-8.3h14.5v-4.9h-14.5v-8h16.4zM191.2 39.5c0 1.6-.5 2.8-1.6 3.8s-2.6 1.4-4.4 1.4h-7.4V34.3h7.4c1.9 0 3.4.4 4.4 1.3 1 .9 1.6 2.2 1.6 3.9zm6 20.8l-7.7-11.7c1-.3 1.9-.7 2.7-1.3a8.8 8.8 0 003.6-4.6c.4-1 .5-2.2.5-3.5 0-1.5-.2-2.9-.7-4.1a8.4 8.4 0 00-2.1-3.1c-1-.8-2-1.5-3.4-2-1.3-.4-2.8-.6-4.5-.6h-12.9v31h5V49.4h6.5l7 10.8z" />
+</svg> \ No newline at end of file
diff --git a/packages/bank-ui/src/assets/logo-white.svg b/packages/bank-ui/src/assets/logo-white.svg
new file mode 100644
index 000000000..cb1f023c5
--- /dev/null
+++ b/packages/bank-ui/src/assets/logo-white.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ width="670"
+ height="300"
+ viewBox="0 0 201 90"
+ version="1.1"
+ id="svg8">
+ <g
+ id="logo">
+ <g
+ id="circles"
+ style="fill:#FFF;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.327943">
+ <path
+ d="m 86.662153,1.1211936 c 15.589697,0 29.129227,9.4011664 35.961027,23.2018054 h -5.81736 C 110.4866,13.623304 99.349002,6.5180852 86.662153,6.5180852 c -19.690571,0 -35.652876,17.1120008 -35.652876,38.2205688 0,10.331797 3.825597,19.704678 10.03957,26.582945 -1.342357,1.120912 -2.771532,2.127905 -4.275488,3.006754 C 50.071485,66.553412 45.974857,56.15992 45.974857,44.738654 c 0,-24.089211 18.216325,-43.6174604 40.687296,-43.6174604 z M 122.51416,65.375898 c -6.86645,13.680134 -20.34561,22.980218 -35.852007,22.980218 -1.052702,0 -2.096093,-0.04291 -3.128683,-0.127026 3.052192,-1.561167 5.913582,-3.480387 8.538307,-5.707305 10.320963,-1.684389 19.185983,-8.113638 24.601813,-17.145887 z"
+ id="path2350" />
+ <path
+ d="m 64.212372,1.1211936 c 1.052607,0 2.095998,0.042919 3.128684,0.1270583 C 64.288864,2.8094199 61.427378,4.728606 58.802653,6.9555572 41.679542,9.7498571 28.559494,25.601563 28.559494,44.738654 c 0,14.264563 7.29059,26.702023 18.093843,33.268925 -1.593656,0.26719 -3.226966,0.406948 -4.890748,0.406948 -1.239545,0 -2.46151,-0.07952 -3.663522,-0.229364 C 29.191129,70.184015 23.525076,58.171633 23.525076,44.738654 23.525076,20.649443 41.7414,1.1211936 64.212372,1.1211936 Z M 69.62209,82.521785 C 79.943207,80.837396 88.808164,74.407841 94.224059,65.375422 h 5.840511 c -6.866354,13.680305 -20.345548,22.980694 -35.852198,22.980694 -1.052703,0 -2.095999,-0.04291 -3.128684,-0.127026 3.052002,-1.561371 5.913836,-3.480218 8.538402,-5.707305 z M 94.355885,24.322999 c -3.13939,-5.314721 -7.467551,-9.74275 -12.584511,-12.853269 1.593656,-0.26719 3.226904,-0.406948 4.890779,-0.406948 1.239451,0 2.461512,0.07952 3.663524,0.229364 4.016018,3.607242 7.373195,8.030111 9.849053,13.030853 z"
+ id="path2352" />
+ <path
+ d="m 41.762589,1.1211936 c 1.064296,0 2.118804,0.044379 3.162607,0.1302161 -3.046523,1.558961 -5.903162,3.4745139 -8.52358,5.6968133 C 19.254624,9.7205882 6.1097128,25.583465 6.1097128,44.738654 c 0,21.108568 15.9624012,38.22057 35.6528762,38.22057 12.599746,0 23.672446,-7.007056 30.013748,-17.583802 h 5.838515 C 70.748498,79.055727 57.26924,88.356116 41.762589,88.356116 c -22.470907,0 -40.6871998,-19.52825 -40.6871998,-43.617462 0,-24.089211 18.2162928,-43.6174604 40.6871998,-43.6174604 z M 71.905375,24.322999 c -1.31192,-2.220567 -2.830984,-4.287049 -4.528877,-6.166508 1.342452,-1.120945 2.771374,-2.128381 4.275139,-3.00723 2.372984,2.753011 4.418875,5.834636 6.072489,9.173738 z"
+ id="path2354" />
+ </g>
+ <g
+ id="letters"
+ style="fill:#FFF">
+ <path
+ d="m 76.135411,34.409066 h 9.161042 V 29.36588 H 61.857537 v 5.043186 h 9.161137 v 25.92317 h 5.116737 z"
+ id="path2346" />
+ <path
+ d="m 92.647571,52.856334 h 13.659009 l 2.93009,7.476072 h 5.36461 L 101.89122,29.144903 H 97.187186 L 84.477089,60.332406 h 5.199533 z m 11.802109,-4.822276 h -9.944771 l 4.951718,-12.386462 z"
+ id="path2362" />
+ <path
+ d="m 123.80641,29.366084 h -4.58038 v 30.966322 h 20.54728 v -4.910253 c -5.32227,0 -10.64463,0 -15.9669,0 z"
+ id="path2356" />
+ <path
+ d="m 166.4722,29.366084 h -21.37564 v 30.966322 h 21.58203 v -4.910253 h -16.54771 v -8.27275 h 14.48439 V 42.23925 h -14.48439 v -7.962811 h 16.34132 z"
+ id="path2360" />
+ <path
+ d="m 191.19035,39.474593 c 0,1.59947 -0.53646,2.87535 -1.61628,3.818883 -1.07281,0.95124 -2.52409,1.422837 -4.34678,1.422837 h -7.44851 V 34.276439 h 7.4073 c 1.9051,0 3.38376,0.435027 4.42939,1.312178 1.05226,0.870258 1.57488,2.167734 1.57488,3.885976 z m 6.06602,20.857813 -7.79911,-11.723191 c 1.01771,-0.294794 1.94631,-0.714813 2.78553,-1.260566 0.83885,-0.545619 1.56122,-1.209263 2.16629,-1.990627 0.60541,-0.781738 1.07981,-1.681096 1.42369,-2.698345 0.34378,-1.017553 0.51561,-2.175238 0.51561,-3.472883 0,-1.50409 -0.24743,-2.867948 -0.74267,-4.092048 -0.49515,-1.223794 -1.20344,-2.256186 -2.12499,-3.096734 -0.92173,-0.840446 -2.04957,-1.489252 -3.38375,-1.946452 -1.33447,-0.457267 -2.82692,-0.685476 -4.4774,-0.685476 h -12.87512 v 30.966322 h 5.03433 V 49.538522 h 6.37569 l 7.11829,10.793884 z"
+ id="path2358" />
+ </g>
+ </g>
+</svg>
diff --git a/packages/bank-ui/src/assets/logo.jpeg b/packages/bank-ui/src/assets/logo.jpeg
new file mode 100644
index 000000000..489832f7c
--- /dev/null
+++ b/packages/bank-ui/src/assets/logo.jpeg
Binary files differ
diff --git a/packages/bank-ui/src/components/Cashouts/index.ts b/packages/bank-ui/src/components/Cashouts/index.ts
new file mode 100644
index 000000000..99a946865
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/index.ts
@@ -0,0 +1,85 @@
+/*
+ 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 { Loading, RouteDefinition, utils } from "@gnu-taler/web-util/browser";
+import {
+ AbsoluteTime,
+ AmountJson,
+ TalerCoreBankErrorsByMethod,
+ TalerCorebankApi,
+ TalerError,
+} from "@gnu-taler/taler-util";
+import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js";
+import { useComponentState } from "./state.js";
+import { FailedView, ReadyView } from "./views.js";
+
+export interface Props {
+ account: string;
+ routeCashoutDetails: RouteDefinition<{ cid: string }>;
+}
+
+export type State =
+ | State.Loading
+ | State.Failed
+ | State.LoadingUriError
+ | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "loading-error";
+ error: TalerError;
+ }
+
+ export interface Failed {
+ status: "failed";
+ error: TalerCoreBankErrorsByMethod<"getAccountCashouts">;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ cashouts: (TalerCorebankApi.CashoutStatusResponse & { id: number })[];
+ routeCashoutDetails: RouteDefinition<{ cid: string }>;
+ }
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "loading-error": ErrorLoadingWithDebug,
+ failed: FailedView,
+ ready: ReadyView,
+};
+
+export const Cashouts = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/bank-ui/src/components/Cashouts/state.ts b/packages/bank-ui/src/components/Cashouts/state.ts
new file mode 100644
index 000000000..8616faa1b
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/state.ts
@@ -0,0 +1,51 @@
+/*
+ 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 { useCashouts } from "../../hooks/regional.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({
+ account,
+ routeCashoutDetails,
+}: Props): State {
+ const result = useCashouts(account);
+ if (!result) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (result instanceof TalerError) {
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+ if (result.type === "fail") {
+ return {
+ status: "failed",
+ error: result,
+ };
+ }
+
+ return {
+ status: "ready",
+ error: undefined,
+ cashouts: result.body.cashouts,
+ routeCashoutDetails,
+ };
+}
diff --git a/packages/bank-ui/src/components/Cashouts/stories.tsx b/packages/bank-ui/src/components/Cashouts/stories.tsx
new file mode 100644
index 000000000..37ab64108
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/stories.tsx
@@ -0,0 +1,29 @@
+/*
+ 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: "transaction list",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/bank-ui/src/components/Cashouts/test.ts b/packages/bank-ui/src/components/Cashouts/test.ts
new file mode 100644
index 000000000..4ed0d7c11
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/test.ts
@@ -0,0 +1,68 @@
+/*
+ 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 { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+import { buildNullRoutDefinition } from "@gnu-taler/web-util/browser";
+
+describe("Cashout states", () => {
+ it.skip("should query backend and render transactions", async () => {
+ const env = new SwrMockEnvironment();
+
+ const props: Props = {
+ account: "123",
+ routeCashoutDetails: buildNullRoutDefinition(),
+ };
+
+ // env.addRequestExpectation(CASHOUT_API_EXAMPLE.LIST_FIRST_PAGE, {
+ // response: {
+ // cashouts: [],
+ // },
+ // });
+
+ // env.addRequestExpectation(CASHOUT_API_EXAMPLE.MULTI_GET_EMPTY_FIRST_PAGE, {
+ // response: [],
+ // });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("ready");
+ expect(error).undefined;
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/bank-ui/src/components/Cashouts/views.tsx b/packages/bank-ui/src/components/Cashouts/views.tsx
new file mode 100644
index 000000000..22b8d8c1b
--- /dev/null
+++ b/packages/bank-ui/src/components/Cashouts/views.tsx
@@ -0,0 +1,218 @@
+/*
+ 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,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { useConversionInfo } from "../../hooks/regional.js";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
+import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js";
+import { Time } from "../Time.js";
+import { State } from "./index.js";
+
+export function FailedView({ error }: State.Failed) {
+ const { i18n } = useTranslationContext();
+ switch (error.case) {
+ case HttpStatusCode.NotImplemented: {
+ return (
+ <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+ <i18n.Translate>
+ Cashout should be enable by configuration and the conversion rate
+ should be initialized with fee, ratio and rounding mode.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ default:
+ assertUnreachable(error.case);
+ }
+}
+
+export function ReadyView({
+ cashouts,
+ routeCashoutDetails,
+}: State.Ready): VNode {
+ const { i18n, dateLocale } = useTranslationContext();
+ const resp = useConversionInfo();
+ if (!resp) {
+ return <Loading />;
+ }
+ if (resp instanceof TalerError) {
+ return <ErrorLoadingWithDebug error={resp} />;
+ }
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.NotImplemented: {
+ return (
+ <Attention type="danger" title={i18n.str`Cashout are disabled`}>
+ <i18n.Translate>
+ Cashout should be enable by configuration and the conversion rate
+ should be initialized with fee, ratio and rounding mode.
+ </i18n.Translate>
+ </Attention>
+ );
+ }
+ default:
+ assertUnreachable(resp.case);
+ }
+ }
+
+ if (!cashouts.length) return <div />;
+ const txByDate = cashouts.reduce(
+ (prev, cur) => {
+ const d =
+ cur.creation_time.t_s === "never"
+ ? ""
+ : format(cur.creation_time.t_s * 1000, "dd/MM/yyyy", {
+ locale: dateLocale,
+ });
+ if (!prev[d]) {
+ prev[d] = [];
+ }
+ prev[d].push(cur);
+ return prev;
+ },
+ {} as Record<string, typeof cashouts>,
+ );
+ return (
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Latest cashouts</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+ <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class=" pl-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Created`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Total debit`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Total credit`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+ >{i18n.str`Subject`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Object.entries(txByDate).map(([date, txs], idx) => {
+ return (
+ <Fragment key={idx}>
+ <tr class="border-t border-gray-200">
+ <th
+ colSpan={6}
+ scope="colgroup"
+ class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3"
+ >
+ {date}
+ </th>
+ </tr>
+ {txs.map((item) => {
+ return (
+ <a
+ name="cashout details"
+ key={idx}
+ class="table-row border-b border-gray-200 hover:bg-gray-200 last:border-none"
+ // class="table-row"
+ href={routeCashoutDetails.url({
+ cid: String(item.id),
+ })}
+ >
+ <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <div class="font-medium text-gray-900">
+ <Time
+ format="HH:mm:ss"
+ timestamp={AbsoluteTime.fromProtocolTimestamp(
+ item.creation_time,
+ )}
+ // relative={Duration.fromSpec({ days: 1 })}
+ />
+ </div>
+ {
+ //FIXME: implement responsive view
+ }
+ {/* <dl class="font-normal sm:hidden">
+ <dt class="sr-only sm:hidden"><i18n.Translate>Amount</i18n.Translate></dt>
+ <dd class="mt-1 truncate text-gray-700">
+ {item.negative ? i18n.str`sent` : i18n.str`received`} {item.amount ? (
+ <span data-negative={item.negative ? "true" : "false"} class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600">
+ <RenderAmount value={item.amount} />
+ </span>
+ ) : (
+ <span style={{ color: "grey" }}>&lt;{i18n.str`invalid value`}&gt;</span>
+ )}</dd>
+
+ <dt class="sr-only sm:hidden"><i18n.Translate>Counterpart</i18n.Translate></dt>
+ <dd class="mt-1 truncate text-gray-500 sm:hidden">
+ {item.negative ? i18n.str`to` : i18n.str`from`} {item.counterpart}
+ </dd>
+ <dd class="mt-1 text-gray-500 sm:hidden" >
+ <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
+ {item.subject}
+ </pre>
+ </dd>
+ </dl> */}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-red-600 cursor-pointer">
+ <RenderAmount
+ value={Amounts.parseOrThrow(item.amount_debit)}
+ spec={resp.body.regional_currency_specification}
+ />
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-green-600 cursor-pointer">
+ <RenderAmount
+ value={Amounts.parseOrThrow(item.amount_credit)}
+ spec={resp.body.fiat_currency_specification}
+ />
+ </td>
+
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
+ {item.subject}
+ </td>
+ </a>
+ );
+ })}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/components/EmptyComponentExample/index.ts b/packages/bank-ui/src/components/EmptyComponentExample/index.ts
new file mode 100644
index 000000000..da84f9921
--- /dev/null
+++ b/packages/bank-ui/src/components/EmptyComponentExample/index.ts
@@ -0,0 +1,56 @@
+/*
+ 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 { HookError, Loading, utils } from "@gnu-taler/web-util/browser";
+import { useComponentState } from "./state.js";
+import { LoadingUriView, ReadyView } from "./views.js";
+
+export interface Props {
+ p: string;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "loading-error";
+ error: HookError;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ }
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "loading-error": LoadingUriView,
+ ready: ReadyView,
+};
+
+export const ComponentName = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/bank-ui/src/components/EmptyComponentExample/state.ts b/packages/bank-ui/src/components/EmptyComponentExample/state.ts
new file mode 100644
index 000000000..057664983
--- /dev/null
+++ b/packages/bank-ui/src/components/EmptyComponentExample/state.ts
@@ -0,0 +1,25 @@
+/*
+ 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 { wxApi } from "../../wxApi.js";
+import { Props, State } from "./index.js";
+
+export function useComponentState({ p: _p }: Props): State {
+ return {
+ status: "ready",
+ error: undefined,
+ };
+}
diff --git a/packages/bank-ui/src/components/EmptyComponentExample/stories.tsx b/packages/bank-ui/src/components/EmptyComponentExample/stories.tsx
new file mode 100644
index 000000000..160acdf79
--- /dev/null
+++ b/packages/bank-ui/src/components/EmptyComponentExample/stories.tsx
@@ -0,0 +1,29 @@
+/*
+ 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: "example",
+};
+
+export const Ready = tests.createExample(ReadyView, {});
diff --git a/packages/bank-ui/src/components/EmptyComponentExample/test.ts b/packages/bank-ui/src/components/EmptyComponentExample/test.ts
new file mode 100644
index 000000000..629948d91
--- /dev/null
+++ b/packages/bank-ui/src/components/EmptyComponentExample/test.ts
@@ -0,0 +1,28 @@
+/*
+ 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 { expect } from "chai";
+
+describe("test description", () => {
+ it("should assert", () => {
+ expect([]).deep.equals([]);
+ });
+});
diff --git a/packages/bank-ui/src/components/EmptyComponentExample/views.tsx b/packages/bank-ui/src/components/EmptyComponentExample/views.tsx
new file mode 100644
index 000000000..457933a5f
--- /dev/null
+++ b/packages/bank-ui/src/components/EmptyComponentExample/views.tsx
@@ -0,0 +1,25 @@
+/*
+ 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 { h, VNode } from "preact";
+
+export function LoadingUriView(): VNode {
+ return <div></div>;
+}
+
+export function ReadyView(): VNode {
+ return <div />;
+}
diff --git a/packages/bank-ui/src/components/ErrorLoadingWithDebug.tsx b/packages/bank-ui/src/components/ErrorLoadingWithDebug.tsx
new file mode 100644
index 000000000..8679af050
--- /dev/null
+++ b/packages/bank-ui/src/components/ErrorLoadingWithDebug.tsx
@@ -0,0 +1,24 @@
+/*
+ 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 { ErrorLoading } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { usePreferences } from "../hooks/preferences.js";
+
+export function ErrorLoadingWithDebug({ error }: { error: TalerError }): VNode {
+ const [pref] = usePreferences();
+ return <ErrorLoading error={error} showDetail={pref.showDebugInfo} />;
+}
diff --git a/packages/bank-ui/src/components/QR.tsx b/packages/bank-ui/src/components/QR.tsx
new file mode 100644
index 000000000..b039bbd1e
--- /dev/null
+++ b/packages/bank-ui/src/components/QR.tsx
@@ -0,0 +1,51 @@
+/*
+ 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 { h, VNode } from "preact";
+import { useEffect, useRef } from "preact/hooks";
+import qrcode from "qrcode-generator";
+
+export function QR({ text }: { text: string }): VNode {
+ const divRef = useRef<HTMLDivElement>(null);
+ useEffect(() => {
+ const qr = qrcode(0, "L");
+ qr.addData(text);
+ qr.make();
+ if (divRef.current)
+ divRef.current.innerHTML = qr.createSvgTag({
+ scalable: true,
+ });
+ });
+
+ return (
+ <div
+ style={{
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "left",
+ }}
+ >
+ <div
+ style={{
+ width: "100%",
+ marginRight: "auto",
+ marginLeft: "auto",
+ }}
+ ref={divRef}
+ />
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/components/Time.tsx b/packages/bank-ui/src/components/Time.tsx
new file mode 100644
index 000000000..5c8afe212
--- /dev/null
+++ b/packages/bank-ui/src/components/Time.tsx
@@ -0,0 +1,80 @@
+/*
+ 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, Duration } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ formatISO,
+ format,
+ formatDuration,
+ intervalToDuration,
+} from "date-fns";
+import { Fragment, h, VNode } from "preact";
+
+/**
+ *
+ * @param timestamp time to be formatted
+ * @param relative duration threshold, if the difference is lower
+ * the timestamp will be formatted as relative time from "now"
+ *
+ * @returns
+ */
+export function Time({
+ timestamp,
+ relative,
+ format: formatString,
+}: {
+ timestamp: AbsoluteTime | undefined;
+ relative?: Duration;
+ format: string;
+}): VNode {
+ const { i18n, dateLocale } = useTranslationContext();
+ if (!timestamp) return <Fragment />;
+
+ if (timestamp.t_ms === "never") {
+ return <time>{i18n.str`never`}</time>;
+ }
+
+ const now = AbsoluteTime.now();
+ const diff = AbsoluteTime.difference(now, timestamp);
+ if (relative && now.t_ms !== "never" && Duration.cmp(diff, relative) === -1) {
+ const d = intervalToDuration({
+ start: now.t_ms,
+ end: timestamp.t_ms,
+ });
+ d.seconds = 0;
+ const duration = formatDuration(d, { locale: dateLocale });
+ const isFuture = AbsoluteTime.cmp(now, timestamp) < 0;
+ if (isFuture) {
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ <i18n.Translate>in {duration}</i18n.Translate>
+ </time>
+ );
+ } else {
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ <i18n.Translate>{duration} ago</i18n.Translate>
+ </time>
+ );
+ }
+ }
+ return (
+ <time dateTime={formatISO(timestamp.t_ms)}>
+ {format(timestamp.t_ms, formatString, { locale: dateLocale })}
+ </time>
+ );
+}
diff --git a/packages/bank-ui/src/components/Transactions/index.ts b/packages/bank-ui/src/components/Transactions/index.ts
new file mode 100644
index 000000000..6fccfcd79
--- /dev/null
+++ b/packages/bank-ui/src/components/Transactions/index.ts
@@ -0,0 +1,84 @@
+/*
+ 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, TalerError } from "@gnu-taler/taler-util";
+import { Loading, utils } from "@gnu-taler/web-util/browser";
+import { ErrorLoadingWithDebug } from "../ErrorLoadingWithDebug.js";
+import { useComponentState } from "./state.js";
+import { ReadyView } from "./views.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+export interface Props {
+ account: string;
+ routeCreateWireTransfer:
+ | RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>
+ | undefined;
+}
+
+export type State = State.Loading | State.LoadingUriError | State.Ready;
+
+export namespace State {
+ export interface Loading {
+ status: "loading";
+ error: undefined;
+ }
+
+ export interface LoadingUriError {
+ status: "loading-error";
+ error: TalerError;
+ }
+
+ export interface BaseInfo {
+ error: undefined;
+ }
+ export interface Ready extends BaseInfo {
+ status: "ready";
+ error: undefined;
+ routeCreateWireTransfer:
+ | RouteDefinition<{
+ account?: string;
+ subject?: string;
+ amount?: string;
+ }>
+ | undefined;
+ transactions: Transaction[];
+ onGoStart?: () => void;
+ onGoNext?: () => void;
+ }
+}
+
+export interface Transaction {
+ negative: boolean;
+ counterpart: string;
+ when: AbsoluteTime;
+ amount: AmountJson | undefined;
+ subject: string;
+}
+
+const viewMapping: utils.StateViewMap<State> = {
+ loading: Loading,
+ "loading-error": ErrorLoadingWithDebug,
+ ready: ReadyView,
+};
+
+export const Transactions = utils.compose(
+ (p: Props) => useComponentState(p),
+ viewMapping,
+);
diff --git a/packages/bank-ui/src/components/Transactions/state.ts b/packages/bank-ui/src/components/Transactions/state.ts
new file mode 100644
index 000000000..ce6338e57
--- /dev/null
+++ b/packages/bank-ui/src/components/Transactions/state.ts
@@ -0,0 +1,90 @@
+/*
+ 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,
+ assertUnreachable,
+ parsePaytoUri,
+} from "@gnu-taler/taler-util";
+import { useTransactions } from "../../hooks/account.js";
+import { Props, State, Transaction } from "./index.js";
+
+export function useComponentState({
+ account,
+ routeCreateWireTransfer,
+}: Props): State {
+ const result = useTransactions(account);
+ if (!result) {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+ if (result instanceof TalerError) {
+ return {
+ status: "loading-error",
+ error: result,
+ };
+ }
+ if (result.type === "fail") {
+ return {
+ status: "loading",
+ error: undefined,
+ };
+ }
+
+ const transactions = result.body
+ .map((tx) => {
+ const negative = tx.direction === "debit";
+ const cp = parsePaytoUri(
+ negative ? tx.creditor_payto_uri : tx.debtor_payto_uri,
+ );
+ const counterpart =
+ (cp === undefined || !cp.isKnown
+ ? undefined
+ : cp.targetType === "iban"
+ ? cp.iban
+ : cp.targetType === "x-taler-bank"
+ ? cp.account
+ : cp.targetType === "bitcoin"
+ ? `${cp.address.substring(0, 6)}...`
+ : undefined) ?? "unknown";
+
+ const when = AbsoluteTime.fromProtocolTimestamp(tx.date);
+ const amount = Amounts.parse(tx.amount);
+ const subject = tx.subject;
+ return {
+ negative,
+ counterpart,
+ when,
+ amount,
+ subject,
+ };
+ })
+ .filter((x): x is Transaction => x !== undefined);
+
+ return {
+ status: "ready",
+ error: undefined,
+ routeCreateWireTransfer,
+ transactions,
+ onGoNext: result.isLastPage ? undefined : result.loadNext,
+ onGoStart: result.isFirstPage ? undefined : result.loadFirst,
+ };
+}
diff --git a/packages/bank-ui/src/components/Transactions/stories.tsx b/packages/bank-ui/src/components/Transactions/stories.tsx
new file mode 100644
index 000000000..95014574b
--- /dev/null
+++ b/packages/bank-ui/src/components/Transactions/stories.tsx
@@ -0,0 +1,44 @@
+/*
+ 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";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
+
+export default {
+ title: "transaction list",
+};
+
+export const Ready = tests.createExample(ReadyView, {
+ transactions: [
+ {
+ amount: {
+ currency: "USD",
+ fraction: 0,
+ value: 1,
+ },
+ counterpart: "ASD",
+ negative: false,
+ subject: "Some",
+ when: AbsoluteTime.now(),
+ },
+ ],
+});
diff --git a/packages/bank-ui/src/components/Transactions/test.ts b/packages/bank-ui/src/components/Transactions/test.ts
new file mode 100644
index 000000000..d9442c742
--- /dev/null
+++ b/packages/bank-ui/src/components/Transactions/test.ts
@@ -0,0 +1,202 @@
+/*
+ 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 { TalerErrorCode } from "@gnu-taler/taler-util";
+import * as tests from "@gnu-taler/web-util/testing";
+import { SwrMockEnvironment } from "@gnu-taler/web-util/testing";
+import { expect } from "chai";
+import { Props } from "./index.js";
+import { useComponentState } from "./state.js";
+
+describe("Transaction states", () => {
+ it.skip("should query backend and render transactions", async () => {
+ const env = new SwrMockEnvironment();
+
+ const props: Props = {
+ account: "myAccount",
+ routeCreateWireTransfer: undefined,
+ };
+
+ // env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_FIRST_PAGE, {
+ // response: {
+ // data: {
+ // transactions: [
+ // {
+ // creditorIban: "DE159593",
+ // creditorBic: "SANDBOXX",
+ // creditorName: "exchange company",
+ // debtorIban: "DE118695",
+ // debtorBic: "SANDBOXX",
+ // debtorName: "Name unknown",
+ // amount: "1",
+ // currency: "KUDOS",
+ // subject:
+ // "Taler Withdrawal N588V8XE9TR49HKAXFQ20P0EQ0EYW2AC9NNANV8ZP5P59N6N0410",
+ // date: "2022-12-12Z",
+ // uid: "8PPFR9EM",
+ // direction: "DBIT",
+ // pmtInfId: null,
+ // msgId: null,
+ // },
+ // {
+ // creditorIban: "DE159593",
+ // creditorBic: "SANDBOXX",
+ // creditorName: "exchange company",
+ // debtorIban: "DE118695",
+ // debtorBic: "SANDBOXX",
+ // debtorName: "Name unknown",
+ // amount: "5.00",
+ // currency: "KUDOS",
+ // subject: "HNEWWT679TQC5P1BVXJS48FX9NW18FWM6PTK2N80Z8GVT0ACGNK0",
+ // date: "2022-12-07Z",
+ // uid: "7FZJC3RJ",
+ // direction: "DBIT",
+ // pmtInfId: null,
+ // msgId: null,
+ // },
+ // {
+ // creditorIban: "DE118695",
+ // creditorBic: "SANDBOXX",
+ // creditorName: "Name unknown",
+ // debtorIban: "DE579516",
+ // debtorBic: "SANDBOXX",
+ // debtorName: "The Bank",
+ // amount: "100",
+ // currency: "KUDOS",
+ // subject: "Sign-up bonus",
+ // date: "2022-12-07Z",
+ // uid: "I31A06J8",
+ // direction: "CRDT",
+ // pmtInfId: null,
+ // msgId: null,
+ // },
+ // ],
+ // },
+ // },
+ // });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("ready");
+ expect(error).undefined;
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+
+ // it("should show error message on not found", async () => {
+ // const env = new SwrMockEnvironment();
+
+ // const props: Props = {
+ // account: "myAccount",
+ // };
+
+ // env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_NOT_FOUND, {
+ // response: {
+ // error: {
+ // description: "Transaction page 0 could not be retrieved.",
+ // },
+ // },
+ // });
+
+ // const hookBehavior = await tests.hookBehaveLikeThis(
+ // useComponentState,
+ // props,
+ // [
+ // ({ status, error }) => {
+ // expect(status).equals("loading");
+ // expect(error).undefined;
+ // },
+ // ({ status, error }) => {
+ // expect(status).equals("loading-error");
+ // if (error === undefined || error.type !== ErrorType.CLIENT) {
+ // throw Error("not the expected error");
+ // }
+ // expect(error.payload).deep.equal({
+ // error: {
+ // description: "Transaction page 0 could not be retrieved.",
+ // },
+ // });
+ // },
+ // ],
+ // env.buildTestingContext(),
+ // );
+
+ // expect(hookBehavior).deep.eq({ result: "ok" });
+ // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ // });
+
+ it.skip("should show error message on server error", async () => {
+ const env = new SwrMockEnvironment();
+
+ const props: Props = {
+ account: "myAccount",
+ routeCreateWireTransfer: undefined,
+ };
+
+ // env.addRequestExpectation(TRANSACTION_API_EXAMPLE.LIST_ERROR, {
+ // response: {
+ // error: {
+ // code: TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ // },
+ // },
+ // });
+
+ const hookBehavior = await tests.hookBehaveLikeThis(
+ useComponentState,
+ props,
+ [
+ ({ status, error }) => {
+ expect(status).equals("loading");
+ expect(error).undefined;
+ },
+ ({ status, error }) => {
+ expect(status).equals("loading-error");
+ if (
+ error === undefined ||
+ !error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)
+ ) {
+ throw Error("not the expected error");
+ }
+ expect(error.errorDetail.code).deep.equal(
+ TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ );
+ },
+ ],
+ env.buildTestingContext(),
+ );
+
+ expect(hookBehavior).deep.eq({ result: "ok" });
+ expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
+ });
+});
diff --git a/packages/bank-ui/src/components/Transactions/views.tsx b/packages/bank-ui/src/components/Transactions/views.tsx
new file mode 100644
index 000000000..10d63e6af
--- /dev/null
+++ b/packages/bank-ui/src/components/Transactions/views.tsx
@@ -0,0 +1,252 @@
+/*
+ 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 {
+ Attention,
+ useBankCoreApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { format } from "date-fns";
+import { Fragment, VNode, h } from "preact";
+import { RenderAmount } from "../../pages/PaytoWireTransferForm.js";
+import { Time } from "../Time.js";
+import { State } from "./index.js";
+
+export function ReadyView({
+ transactions,
+ routeCreateWireTransfer,
+ onGoNext,
+ onGoStart,
+}: State.Ready): VNode {
+ const { i18n, dateLocale } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+
+ if (!transactions.length) {
+ return (
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Transactions history</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+
+ <Attention type="low" title={i18n.str`No transactions yet.`}>
+ <i18n.Translate>
+ You can start sending a wire transfer or withdrawing to your wallet.
+ </i18n.Translate>
+ </Attention>
+ </div>
+ );
+ }
+
+ const txByDate = transactions.reduce(
+ (prev, cur) => {
+ const d =
+ cur.when.t_ms === "never"
+ ? ""
+ : format(cur.when.t_ms, "dd/MM/yyyy", { locale: dateLocale });
+ if (!prev[d]) {
+ prev[d] = [];
+ }
+ prev[d].push(cur);
+ return prev;
+ },
+ {} as Record<string, typeof transactions>,
+ );
+ return (
+ <div class="px-4 mt-8">
+ <div class="sm:flex sm:items-center">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Transactions history</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+ <div class="-mx-4 mt-5 ring-1 ring-gray-300 sm:mx-0 rounded-lg min-w-fit bg-white">
+ <table class="min-w-full divide-y divide-gray-300">
+ <thead>
+ <tr>
+ <th
+ scope="col"
+ class="pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "
+ >{i18n.str`Date`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "
+ >{i18n.str`Amount`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "
+ >{i18n.str`Counterpart`}</th>
+ <th
+ scope="col"
+ class="hidden sm:table-cell pl-2 py-3.5 text-left text-sm font-semibold text-gray-900 "
+ >{i18n.str`Subject`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Object.entries(txByDate).map(([date, txs], idx) => {
+ return (
+ <Fragment key={idx}>
+ <tr class="border-t border-gray-200">
+ <th
+ colSpan={4}
+ scope="colgroup"
+ class="bg-gray-50 py-2 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-3"
+ >
+ {date}
+ </th>
+ </tr>
+ {txs.map((item) => {
+ return (
+ <tr
+ key={idx}
+ class="border-b border-gray-200 last:border-none"
+ >
+ <td class="relative py-2 pl-2 pr-2 text-sm ">
+ <div class="font-medium text-gray-900">
+ <Time
+ format="HH:mm:ss"
+ timestamp={item.when}
+ // relative={Duration.fromSpec({ days: 1 })}
+ />
+ </div>
+ <dl class="font-normal sm:hidden">
+ <dt class="sr-only sm:hidden">
+ <i18n.Translate>Amount</i18n.Translate>
+ </dt>
+ <dd class="mt-1 truncate text-gray-700">
+ {item.negative
+ ? i18n.str`sent`
+ : i18n.str`received`}{" "}
+ {item.amount ? (
+ <span
+ data-negative={
+ item.negative ? "true" : "false"
+ }
+ class="data-[negative=false]:text-green-600 data-[negative=true]:text-red-600"
+ >
+ <RenderAmount
+ value={item.amount}
+ spec={config.currency_specification}
+ />
+ </span>
+ ) : (
+ <span style={{ color: "grey" }}>
+ &lt;{i18n.str`Invalid value`}&gt;
+ </span>
+ )}
+ </dd>
+
+ <dt class="sr-only sm:hidden">
+ <i18n.Translate>Counterpart</i18n.Translate>
+ </dt>
+ <dd class="mt-1 truncate text-gray-500 sm:hidden">
+ {item.negative ? i18n.str`to` : i18n.str`from`}{" "}
+ {!routeCreateWireTransfer ? (
+ item.counterpart
+ ) : (
+ <a
+ name={`transfer to ${item.counterpart}`}
+ href={routeCreateWireTransfer.url({
+ account: item.counterpart,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {item.counterpart}
+ </a>
+ )}
+ </dd>
+ <dd class="mt-1 text-gray-500 sm:hidden">
+ <pre class="break-words w-56 whitespace-break-spaces p-2 rounded-md mx-auto my-2 bg-gray-100">
+ {item.subject}
+ </pre>
+ </dd>
+ </dl>
+ </td>
+ <td
+ data-negative={item.negative ? "true" : "false"}
+ class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 "
+ >
+ {item.amount ? (
+ <RenderAmount
+ value={item.amount}
+ negative={item.negative}
+ withColor
+ spec={config.currency_specification}
+ />
+ ) : (
+ <span style={{ color: "grey" }}>
+ &lt;{i18n.str`Invalid value`}&gt;
+ </span>
+ )}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500">
+ {!routeCreateWireTransfer ? (
+ item.counterpart
+ ) : (
+ <a
+ name={`wire transfer to ${item.counterpart}`}
+ href={routeCreateWireTransfer.url({
+ account: item.counterpart,
+ })}
+ class="text-indigo-600 hover:text-indigo-900"
+ >
+ {item.counterpart}
+ </a>
+ )}
+ </td>
+ <td class="hidden sm:table-cell px-3 py-3.5 text-sm text-gray-500 break-all min-w-md">
+ {item.subject}
+ </td>
+ </tr>
+ );
+ })}
+ </Fragment>
+ );
+ })}
+ </tbody>
+ </table>
+
+ <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>
+ );
+}
diff --git a/packages/bank-ui/src/components/index.examples.ts b/packages/bank-ui/src/components/index.examples.ts
new file mode 100644
index 000000000..20e013070
--- /dev/null
+++ b/packages/bank-ui/src/components/index.examples.ts
@@ -0,0 +1,17 @@
+/*
+ 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 tx from "./Transactions/stories.js";
diff --git a/packages/bank-ui/src/context/settings.ts b/packages/bank-ui/src/context/settings.ts
new file mode 100644
index 000000000..6c61a7b4a
--- /dev/null
+++ b/packages/bank-ui/src/context/settings.ts
@@ -0,0 +1,44 @@
+/*
+ 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 { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+import { UiSettings } from "../settings.js";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+export type Type = UiSettings;
+
+const initial: UiSettings = {};
+const Context = createContext<Type>(initial);
+
+export const useSettingsContext = (): Type => useContext(Context);
+
+export const SettingsProvider = ({
+ children,
+ value,
+}: {
+ value: UiSettings;
+ children: ComponentChildren;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/bank-ui/src/context/wallet-integration.ts b/packages/bank-ui/src/context/wallet-integration.ts
new file mode 100644
index 000000000..e14988ed1
--- /dev/null
+++ b/packages/bank-ui/src/context/wallet-integration.ts
@@ -0,0 +1,83 @@
+/*
+ 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 { stringifyTalerUri, TalerUri } from "@gnu-taler/taler-util";
+import { ComponentChildren, createContext, h, VNode } from "preact";
+import { useContext } from "preact/hooks";
+
+/**
+ * https://docs.taler.net/design-documents/039-taler-browser-integration.html
+ *
+ * @param uri
+ */
+function createHeadMetaTag(uri: TalerUri, onNotFound?: () => void) {
+ const meta = document.createElement("meta");
+ meta.setAttribute("name", "taler-uri");
+ meta.setAttribute("content", stringifyTalerUri(uri));
+
+ document.head.appendChild(meta);
+
+ let walletFound = false;
+ window.addEventListener("beforeunload", () => {
+ walletFound = true;
+ });
+ setTimeout(() => {
+ if (!walletFound && onNotFound) {
+ onNotFound();
+ }
+ }, 10); //very short timeout
+}
+interface Type {
+ /**
+ * Tell the active wallet that an action is found
+ *
+ * @param uri
+ * @returns
+ */
+ publishTalerAction: (uri: TalerUri, onNotFound?: () => void) => void;
+}
+
+// @ts-expect-error default value to undefined, should it be another thing?
+const Context = createContext<Type>(undefined);
+
+export const useTalerWalletIntegrationAPI = (): Type => useContext(Context);
+
+export const TalerWalletIntegrationBrowserProvider = ({
+ children,
+}: {
+ children: ComponentChildren;
+}): VNode => {
+ const value: Type = {
+ publishTalerAction: createHeadMetaTag,
+ };
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
+
+export const TalerWalletIntegrationTestingProvider = ({
+ children,
+ value,
+}: {
+ children: ComponentChildren;
+ value: Type;
+}): VNode => {
+ return h(Context.Provider, {
+ value,
+ children,
+ });
+};
diff --git a/packages/bank-ui/src/declaration.d.ts b/packages/bank-ui/src/declaration.d.ts
new file mode 100644
index 000000000..581cbcd07
--- /dev/null
+++ b/packages/bank-ui/src/declaration.d.ts
@@ -0,0 +1,35 @@
+/*
+ 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/>
+ */
+
+declare module "*.css" {
+ const mapping: Record<string, string>;
+ export default mapping;
+}
+declare module "*.svg" {
+ const content: string;
+ export default content;
+}
+declare module "*.jpeg" {
+ const content: string;
+ export default content;
+}
+declare module "*.png" {
+ const content: string;
+ export default content;
+}
+
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
diff --git a/packages/bank-ui/src/hooks/account.ts b/packages/bank-ui/src/hooks/account.ts
new file mode 100644
index 000000000..43d43a3f2
--- /dev/null
+++ b/packages/bank-ui/src/hooks/account.ts
@@ -0,0 +1,313 @@
+/*
+ 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,
+ OperationOk,
+ TalerCoreBankResultByMethod,
+ TalerHttpError,
+ WithdrawalOperationStatus,
+} from "@gnu-taler/taler-util";
+import { useEffect, useState } from "preact/hooks";
+import { useSessionState } from "./session.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+import _useSWR, { SWRHook, mutate } from "swr";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { PAGINATED_LIST_REQUEST } from "../utils.js";
+const useSWR = _useSWR as unknown as SWRHook;
+
+export interface InstanceTemplateFilter {
+ // FIXME: add filter to the template list
+ position?: string;
+}
+
+export function revalidateAccountDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getAccount",
+ undefined,
+ { revalidate: true },
+ );
+}
+
+export function useAccountDetails(account: string) {
+ const { state: credentials } = useSessionState();
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([username, token]: [string, AccessToken]) {
+ return await api.getAccount({ username, token });
+ }
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getAccount">,
+ TalerHttpError
+ >([account, token, "getAccount"], fetcher, {});
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateWithdrawalDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getWithdrawalById",
+ undefined,
+ { revalidate: true },
+ );
+}
+
+export function useWithdrawalDetails(wid: string) {
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ const [latestStatus, setLatestStatus] = useState<WithdrawalOperationStatus>();
+
+ async function fetcher([wid, old_state]: [
+ string,
+ WithdrawalOperationStatus | undefined,
+ ]) {
+ return await api.getWithdrawalById(
+ wid,
+ old_state === undefined ? undefined : { old_state, timeoutMs: 15000 },
+ );
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getWithdrawalById">,
+ TalerHttpError
+ >([wid, latestStatus, "getWithdrawalById"], fetcher, {
+ refreshInterval: 3000,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ const currentStatus =
+ data !== undefined && data.type === "ok" ? data.body.status : undefined;
+
+ useEffect(() => {
+ if (currentStatus !== undefined && currentStatus !== latestStatus) {
+ setLatestStatus(currentStatus);
+ }
+ }, [currentStatus]);
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateTransactionDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getTransactionById",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useTransactionDetails(account: string, tid: number) {
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([username, token, txid]: [
+ string,
+ AccessToken,
+ number,
+ ]) {
+ return await api.getTransactionById({ username, token }, txid);
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getTransactionById">,
+ TalerHttpError
+ >([account, token, tid, "getTransactionById"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export async function revalidatePublicAccounts() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getPublicAccounts",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function usePublicAccounts(
+ filterAccount: string | undefined,
+ initial?: number,
+) {
+ const [offset, setOffset] = useState<number | undefined>(initial);
+
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([account, txid]: [
+ string | undefined,
+ number | undefined,
+ ]) {
+ return await api.getPublicAccounts(
+ { account },
+ {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: txid ? String(txid) : undefined,
+ order: "asc",
+ },
+ );
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getPublicAccounts">,
+ TalerHttpError
+ >([filterAccount, offset, "getPublicAccounts"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ // if (data.type !== "ok") return data;
+
+ //TODO: row_id should not be optional
+ return buildPaginatedResult(
+ data.body.public_accounts,
+ offset,
+ setOffset,
+ (d) => d.row_id ?? 0,
+ );
+}
+
+type PaginatedResult<T> = OperationOk<T> & {
+ isLastPage: boolean;
+ isFirstPage: boolean;
+ loadNext(): void;
+ loadFirst(): void;
+};
+//TODO: consider sending this to web-util
+export function buildPaginatedResult<DataType, OffsetId>(
+ data: DataType[],
+ offset: OffsetId | undefined,
+ setOffset: (o: OffsetId | undefined) => void,
+ getId: (r: DataType) => OffsetId,
+): PaginatedResult<DataType[]> {
+ const isLastPage = data.length < PAGINATED_LIST_REQUEST;
+ const isFirstPage = offset === undefined;
+
+ const result = structuredClone(data);
+ if (result.length == PAGINATED_LIST_REQUEST) {
+ //do now show the last element, used to know if this is the last page
+ result.pop();
+ }
+ return {
+ type: "ok",
+ body: result,
+ isLastPage,
+ isFirstPage,
+ loadNext: () => {
+ if (!result.length) return;
+ const id = getId(result[result.length - 1]);
+ setOffset(id);
+ },
+ loadFirst: () => {
+ setOffset(undefined);
+ },
+ };
+}
+
+export function revalidateTransactions() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getTransactions",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useTransactions(account: string, initial?: number) {
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ const [offset, setOffset] = useState<number | undefined>(initial);
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([username, token, txid]: [
+ string,
+ AccessToken,
+ number | undefined,
+ ]) {
+ return await api.getTransactions(
+ { username, token },
+ {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: txid ? String(txid) : undefined,
+ order: "dec",
+ },
+ );
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getTransactions">,
+ TalerHttpError
+ >([account, token, offset, "getTransactions"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ refreshWhenOffline: false,
+ // revalidateOnMount: false,
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ });
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ return buildPaginatedResult(
+ data.body.transactions,
+ offset,
+ setOffset,
+ (d) => d.row_id,
+ );
+}
diff --git a/packages/bank-ui/src/hooks/bank-state.ts b/packages/bank-ui/src/hooks/bank-state.ts
new file mode 100644
index 000000000..616678ddc
--- /dev/null
+++ b/packages/bank-ui/src/hooks/bank-state.ts
@@ -0,0 +1,185 @@
+/*
+ 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,
+ Codec,
+ TalerCorebankApi,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAbsoluteTime,
+ codecForAny,
+ codecForConstString,
+ codecForString,
+ codecForTanTransmission,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { AppLocation } from "@gnu-taler/web-util/browser";
+
+export type ChallengeInProgess =
+ | DeleteAccountChallenge
+ | UpdateAccountChallenge
+ | UpdatePasswordChallenge
+ | CreateTransactionChallenge
+ | ConfirmWithdrawalChallenge
+ | CashoutChallenge;
+
+type BaseChallenge<OpType extends string, ReqType> = {
+ id: string;
+ operation: OpType;
+ sent: AbsoluteTime;
+ location: AppLocation;
+ info?: TalerCorebankApi.TanTransmission;
+ request: ReqType;
+};
+
+type DeleteAccountChallenge = BaseChallenge<"delete-account", string>;
+type UpdateAccountChallenge = BaseChallenge<
+ "update-account",
+ TalerCorebankApi.AccountReconfiguration
+>;
+type UpdatePasswordChallenge = BaseChallenge<
+ "update-password",
+ TalerCorebankApi.AccountPasswordChange
+>;
+type CreateTransactionChallenge = BaseChallenge<
+ "create-transaction",
+ TalerCorebankApi.CreateTransactionRequest
+>;
+type ConfirmWithdrawalChallenge = BaseChallenge<"confirm-withdrawal", string>;
+type CashoutChallenge = BaseChallenge<
+ "create-cashout",
+ TalerCorebankApi.CashoutRequest
+>;
+
+const codecForChallengeUpdatePassword = (): Codec<UpdatePasswordChallenge> =>
+ buildCodecForObject<UpdatePasswordChallenge>()
+ .property("operation", codecForConstString("update-password"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("UpdatePasswordChallenge");
+
+const codecForChallengeDeleteAccount = (): Codec<DeleteAccountChallenge> =>
+ buildCodecForObject<DeleteAccountChallenge>()
+ .property("operation", codecForConstString("delete-account"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("request", codecForString())
+ .property("info", codecOptional(codecForTanTransmission()))
+ .build("DeleteAccountChallenge");
+
+const codecForChallengeUpdateAccount = (): Codec<UpdateAccountChallenge> =>
+ buildCodecForObject<UpdateAccountChallenge>()
+ .property("operation", codecForConstString("update-account"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("UpdateAccountChallenge");
+
+const codecForChallengeCreateTransaction =
+ (): Codec<CreateTransactionChallenge> =>
+ buildCodecForObject<CreateTransactionChallenge>()
+ .property("operation", codecForConstString("create-transaction"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("CreateTransactionChallenge");
+
+const codecForChallengeConfirmWithdrawal =
+ (): Codec<ConfirmWithdrawalChallenge> =>
+ buildCodecForObject<ConfirmWithdrawalChallenge>()
+ .property("operation", codecForConstString("confirm-withdrawal"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForString())
+ .build("ConfirmWithdrawalChallenge");
+
+const codecForAppLocation = codecForString as () => Codec<AppLocation>;
+
+const codecForChallengeCashout = (): Codec<CashoutChallenge> =>
+ buildCodecForObject<CashoutChallenge>()
+ .property("operation", codecForConstString("create-cashout"))
+ .property("id", codecForString())
+ .property("location", codecForAppLocation())
+ .property("sent", codecForAbsoluteTime)
+ .property("info", codecOptional(codecForTanTransmission()))
+ .property("request", codecForAny())
+ .build("CashoutChallenge");
+
+const codecForChallenge = (): Codec<ChallengeInProgess> =>
+ buildCodecForUnion<ChallengeInProgess>()
+ .discriminateOn("operation")
+ .alternative("confirm-withdrawal", codecForChallengeConfirmWithdrawal())
+ .alternative("create-cashout", codecForChallengeCashout())
+ .alternative("create-transaction", codecForChallengeCreateTransaction())
+ .alternative("delete-account", codecForChallengeDeleteAccount())
+ .alternative("update-account", codecForChallengeUpdateAccount())
+ .alternative("update-password", codecForChallengeUpdatePassword())
+ .build("ChallengeInProgess");
+
+interface BankState {
+ currentWithdrawalOperationId: string | undefined;
+ currentChallenge: ChallengeInProgess | undefined;
+}
+
+export const codecForBankState = (): Codec<BankState> =>
+ buildCodecForObject<BankState>()
+ .property("currentWithdrawalOperationId", codecOptional(codecForString()))
+ .property("currentChallenge", codecOptional(codecForChallenge()))
+ .build("BankState");
+
+const defaultBankState: BankState = {
+ currentWithdrawalOperationId: undefined,
+ currentChallenge: undefined,
+};
+
+const BANK_STATE_KEY = buildStorageKey("bank-app-state", codecForBankState());
+
+/**
+ * Client state saved in local storage.
+ *
+ * This information is saved in the client because
+ * the backend server session API is not enough.
+ *
+ * @returns tuple of [state, update(), reset()]
+ */
+export function useBankState(): [
+ Readonly<BankState>,
+ <T extends keyof BankState>(key: T, value: BankState[T]) => void,
+ () => void,
+] {
+ const { value, update } = useLocalStorage(BANK_STATE_KEY, defaultBankState);
+
+ function updateField<T extends keyof BankState>(k: T, v: BankState[T]) {
+ const newValue = { ...value, [k]: v };
+ update(newValue);
+ }
+ function reset() {
+ update(defaultBankState);
+ }
+ return [value, updateField, reset];
+}
diff --git a/packages/bank-ui/src/hooks/form.ts b/packages/bank-ui/src/hooks/form.ts
new file mode 100644
index 000000000..fae11c05c
--- /dev/null
+++ b/packages/bank-ui/src/hooks/form.ts
@@ -0,0 +1,124 @@
+/*
+ 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, TranslatedString } from "@gnu-taler/taler-util";
+import { useState } from "preact/hooks";
+
+export type UIField = {
+ value: string | undefined;
+ onUpdate: (s: string) => void;
+ error: TranslatedString | undefined;
+};
+
+type FormHandler<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? UIField
+ : T[k] extends AmountJson
+ ? UIField
+ : FormHandler<T[k]>;
+};
+
+export type FormValues<T> = {
+ [k in keyof T]: T[k] extends string
+ ? string | undefined
+ : T[k] extends AmountJson
+ ? string | undefined
+ : FormValues<T[k]>;
+};
+
+export type RecursivePartial<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? string
+ : T[k] extends AmountJson
+ ? AmountJson
+ : RecursivePartial<T[k]>;
+};
+
+export type FormErrors<T> = {
+ [k in keyof T]?: T[k] extends string
+ ? TranslatedString
+ : T[k] extends AmountJson
+ ? TranslatedString
+ : FormErrors<T[k]>;
+};
+
+export type FormStatus<T> =
+ | {
+ status: "ok";
+ result: T;
+ errors: undefined;
+ }
+ | {
+ status: "fail";
+ result: RecursivePartial<T>;
+ errors: FormErrors<T>;
+ };
+
+function constructFormHandler<T>(
+ form: FormValues<T>,
+ updateForm: (d: FormValues<T>) => void,
+ errors: FormErrors<T> | undefined,
+): FormHandler<T> {
+
+ const keys = Object.keys(form) as Array<keyof T>;
+
+ const handler = keys.reduce((prev, fieldName) => {
+ const currentValue: unknown = form[fieldName];
+ const currentError: unknown = errors ? errors[fieldName] : undefined;
+ function updater(newValue: unknown) {
+ updateForm({ ...form, [fieldName]: newValue });
+ }
+ if (typeof currentValue === "object") {
+ // @ts-expect-error FIXME better typing
+ const group = constructFormHandler(currentValue, updater, currentError);
+ // @ts-expect-error FIXME better typing
+ prev[fieldName] = group;
+ return prev;
+ }
+ const field: UIField = {
+ // @ts-expect-error FIXME better typing
+ error: currentError,
+ // @ts-expect-error FIXME better typing
+ value: currentValue,
+ onUpdate: updater,
+ };
+ // @ts-expect-error FIXME better typing
+ prev[fieldName] = field;
+ return prev;
+ }, {} as FormHandler<T>);
+
+ return handler;
+}
+
+/**
+ * FIXME: Consider sending this to web-utils
+ *
+ *
+ * @param defaultValue
+ * @param check
+ * @returns
+ */
+export function useFormState<T>(
+ defaultValue: FormValues<T>,
+ check: (f: FormValues<T>) => FormStatus<T>,
+): [FormHandler<T>, FormStatus<T>] {
+ const [form, updateForm] = useState<FormValues<T>>(defaultValue);
+
+ const status = check(form);
+ const handler = constructFormHandler(form, updateForm, status.errors);
+
+ return [handler, status];
+}
diff --git a/packages/bank-ui/src/hooks/preferences.ts b/packages/bank-ui/src/hooks/preferences.ts
new file mode 100644
index 000000000..bb3dcb153
--- /dev/null
+++ b/packages/bank-ui/src/hooks/preferences.ts
@@ -0,0 +1,111 @@
+/*
+ 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 {
+ Codec,
+ TranslatedString,
+ buildCodecForObject,
+ codecForBoolean,
+ codecForNumber,
+} from "@gnu-taler/taler-util";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+
+interface Preferences {
+ showWithdrawalSuccess: boolean;
+ showDemoDescription: boolean;
+ showInstallWallet: boolean;
+ maxWithdrawalAmount: number;
+ fastWithdrawal: boolean;
+ showDebugInfo: boolean;
+}
+
+export const codecForPreferences = (): Codec<Preferences> =>
+ buildCodecForObject<Preferences>()
+ .property("showWithdrawalSuccess", codecForBoolean())
+ .property("showDemoDescription", codecForBoolean())
+ .property("showInstallWallet", codecForBoolean())
+ .property("fastWithdrawal", codecForBoolean())
+ .property("showDebugInfo", codecForBoolean())
+ .property("maxWithdrawalAmount", codecForNumber())
+ .build("Settings");
+
+const defaultPreferences: Preferences = {
+ showWithdrawalSuccess: true,
+ showDemoDescription: true,
+ showInstallWallet: true,
+ maxWithdrawalAmount: 25,
+ fastWithdrawal: false,
+ showDebugInfo: false,
+};
+
+const BANK_PREFERENCES_KEY = buildStorageKey(
+ "bank-preferences",
+ codecForPreferences(),
+);
+/**
+ * User preferences.
+ *
+ * @returns tuple of [state, update()]
+ */
+export function usePreferences(): [
+ Readonly<Preferences>,
+ <T extends keyof Preferences>(key: T, value: Preferences[T]) => void,
+] {
+ const { value, update } = useLocalStorage(
+ BANK_PREFERENCES_KEY,
+ defaultPreferences,
+ );
+
+ function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) {
+ const newValue = { ...value, [k]: v };
+ update(newValue);
+ }
+ return [value, updateField];
+}
+
+export function getAllBooleanPreferences(): Array<keyof Preferences> {
+ return [
+ "fastWithdrawal",
+ "showDebugInfo",
+ "showDemoDescription",
+ "showInstallWallet",
+ "showWithdrawalSuccess",
+ ];
+}
+
+export function getLabelForPreferences(
+ k: keyof Preferences,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): TranslatedString {
+ switch (k) {
+ case "maxWithdrawalAmount":
+ return i18n.str`Max withdrawal amount`;
+ case "showWithdrawalSuccess":
+ return i18n.str`Show withdrawal confirmation`;
+ case "showDemoDescription":
+ return i18n.str`Show demo description`;
+ case "showInstallWallet":
+ return i18n.str`Show install wallet first`;
+ case "fastWithdrawal":
+ return i18n.str`Use fast withdrawal form`;
+ case "showDebugInfo":
+ return i18n.str`Show debug info`;
+ }
+}
diff --git a/packages/bank-ui/src/hooks/regional.ts b/packages/bank-ui/src/hooks/regional.ts
new file mode 100644
index 000000000..e0c861a0f
--- /dev/null
+++ b/packages/bank-ui/src/hooks/regional.ts
@@ -0,0 +1,507 @@
+/*
+ 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 { useSessionState } from "./session.js";
+
+import {
+ AbsoluteTime,
+ AccessToken,
+ AmountJson,
+ Amounts,
+ HttpStatusCode,
+ OperationOk,
+ TalerBankConversionResultByMethod,
+ TalerCoreBankErrorsByMethod,
+ TalerCoreBankResultByMethod,
+ TalerCorebankApi,
+ TalerError,
+ TalerHttpError,
+ opFixedSuccess,
+} from "@gnu-taler/taler-util";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useState } from "preact/hooks";
+import _useSWR, { SWRHook, mutate } from "swr";
+import { buildPaginatedResult } from "./account.js";
+import { PAGINATED_LIST_REQUEST } from "../utils.js";
+
+// FIX default import https://github.com/microsoft/TypeScript/issues/49189
+const useSWR = _useSWR as unknown as SWRHook;
+
+export type TransferCalculation =
+ | {
+ debit: AmountJson;
+ credit: AmountJson;
+ beforeFee: AmountJson;
+ }
+ | "amount-is-too-small";
+type EstimatorFunction = (
+ amount: AmountJson,
+ fee: AmountJson,
+) => Promise<TransferCalculation>;
+
+type ConversionEstimators = {
+ estimateByCredit: EstimatorFunction;
+ estimateByDebit: EstimatorFunction;
+};
+
+export function revalidateConversionInfo() {
+ return mutate(
+ (key) =>
+ Array.isArray(key) && key[key.length - 1] === "getConversionInfoAPI",
+ );
+}
+export function useConversionInfo() {
+ const {
+ lib: { conversion },
+ config,
+ } = useBankCoreApiContext();
+
+ async function fetcher() {
+ return await conversion.getConfig();
+ }
+ const { data, error } = useSWR<
+ TalerBankConversionResultByMethod<"getConfig">,
+ TalerHttpError
+ >(!config.allow_conversion ? undefined : ["getConversionInfoAPI"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function useCashinEstimator(): ConversionEstimators {
+ const {
+ lib: { conversion },
+ } = useBankCoreApiContext();
+ return {
+ estimateByCredit: async (fiatAmount, fee) => {
+ const resp = await conversion.getCashinRate({
+ credit: fiatAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small";
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.sub(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ estimateByDebit: async (regionalAmount, fee) => {
+ const resp = await conversion.getCashinRate({
+ debit: regionalAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small";
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.add(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ };
+}
+
+export function useCashoutEstimator(): ConversionEstimators {
+ const {
+ lib: { conversion },
+ } = useBankCoreApiContext();
+ return {
+ estimateByCredit: async (fiatAmount, fee) => {
+ const resp = await conversion.getCashoutRate({
+ credit: fiatAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small";
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.sub(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ estimateByDebit: async (regionalAmount, fee) => {
+ const resp = await conversion.getCashoutRate({
+ debit: regionalAmount,
+ });
+ if (resp.type === "fail") {
+ switch (resp.case) {
+ case HttpStatusCode.Conflict: {
+ return "amount-is-too-small";
+ }
+ // this below can't happen
+ case HttpStatusCode.NotImplemented: //it should not be able to call this function
+ case HttpStatusCode.BadRequest: //we are using just one parameter
+ throw TalerError.fromDetail(resp.detail.code, {}, resp.detail.hint);
+ }
+ }
+ const credit = Amounts.parseOrThrow(resp.body.amount_credit);
+ const debit = Amounts.parseOrThrow(resp.body.amount_debit);
+ const beforeFee = Amounts.add(credit, fee).amount;
+
+ return {
+ debit,
+ beforeFee,
+ credit,
+ };
+ },
+ };
+}
+
+/**
+ * @deprecated use useCashoutEstimator
+ */
+export function useEstimator(): ConversionEstimators {
+ return useCashoutEstimator();
+}
+
+export async function revalidateBusinessAccounts() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getAccounts",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useBusinessAccounts() {
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ const [offset, setOffset] = useState<number | undefined>();
+
+ function fetcher([token, aid]: [AccessToken, number]) {
+ // FIXME: add account name filter
+ return api.getAccounts(
+ token,
+ {},
+ {
+ limit: PAGINATED_LIST_REQUEST,
+ offset: aid ? String(aid) : undefined,
+ order: "asc",
+ },
+ );
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getAccounts">,
+ TalerHttpError
+ >([token, offset ?? 0, "getAccounts"], fetcher, {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ });
+
+ if (error) return error;
+ if (data === undefined) return undefined;
+ if (data.type !== "ok") return data;
+
+ //TODO: row_id should not be optional
+ return buildPaginatedResult(
+ data.body.accounts,
+ offset,
+ setOffset,
+ (d) => d.row_id ?? 0,
+ );
+}
+
+type CashoutWithId = TalerCorebankApi.CashoutStatusResponse & { id: number };
+function notUndefined(c: CashoutWithId | undefined): c is CashoutWithId {
+ return c !== undefined;
+}
+export function revalidateOnePendingCashouts() {
+ return mutate(
+ (key) =>
+ Array.isArray(key) && key[key.length - 1] === "useOnePendingCashouts",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useOnePendingCashouts(account: string) {
+ const { state: credentials } = useSessionState();
+ const {
+ lib: { bank: api },
+ config,
+ } = useBankCoreApiContext();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ async function fetcher([username, token]: [string, AccessToken]) {
+ const list = await api.getAccountCashouts({ username, token });
+ if (list.type !== "ok") {
+ return list;
+ }
+ const pendingCashout =
+ list.body.cashouts.length > 0 ? list.body.cashouts[0] : undefined;
+ if (!pendingCashout) return opFixedSuccess(undefined);
+ const cashoutInfo = await api.getCashoutById(
+ { username, token },
+ pendingCashout.cashout_id,
+ );
+ if (cashoutInfo.type !== "ok") {
+ return cashoutInfo;
+ }
+ return opFixedSuccess({
+ ...cashoutInfo.body,
+ id: pendingCashout.cashout_id,
+ });
+ }
+
+ const { data, error } = useSWR<
+ | OperationOk<CashoutWithId | undefined>
+ | TalerCoreBankErrorsByMethod<"getAccountCashouts">
+ | TalerCoreBankErrorsByMethod<"getCashoutById">,
+ TalerHttpError
+ >(
+ !config.allow_conversion
+ ? undefined
+ : [account, token, "useOnePendingCashouts"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateCashouts() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "useCashouts",
+ );
+}
+export function useCashouts(account: string) {
+ const { state: credentials } = useSessionState();
+ const {
+ lib: { bank: api },
+ config,
+ } = useBankCoreApiContext();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ async function fetcher([username, token]: [string, AccessToken]) {
+ const list = await api.getAccountCashouts({ username, token });
+ if (list.type !== "ok") {
+ return list;
+ }
+ const all: Array<CashoutWithId | undefined> = await Promise.all(
+ list.body.cashouts.map(async (c) => {
+ const r = await api.getCashoutById({ username, token }, c.cashout_id);
+ if (r.type === "fail") {
+ return undefined;
+ }
+ return { ...r.body, id: c.cashout_id };
+ }),
+ );
+ const cashouts = all.filter(notUndefined);
+ return { type: "ok" as const, body: { cashouts } };
+ }
+ const { data, error } = useSWR<
+ | OperationOk<{ cashouts: CashoutWithId[] }>
+ | TalerCoreBankErrorsByMethod<"getAccountCashouts">,
+ TalerHttpError
+ >(
+ !config.allow_conversion ? undefined : [account, token, "useCashouts"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+
+export function revalidateCashoutDetails() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "getCashoutById",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useCashoutDetails(cashoutId: number | undefined) {
+ const { state: credentials } = useSessionState();
+ const creds = credentials.status !== "loggedIn" ? undefined : credentials;
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+
+ async function fetcher([username, token, id]: [string, AccessToken, number]) {
+ return api.getCashoutById({ username, token }, id);
+ }
+
+ const { data, error } = useSWR<
+ TalerCoreBankResultByMethod<"getCashoutById">,
+ TalerHttpError
+ >(
+ cashoutId === undefined
+ ? undefined
+ : [creds?.username, creds?.token, cashoutId, "getCashoutById"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
+export type MonitorMetrics = {
+ lastHour: TalerCoreBankResultByMethod<"getMonitor">;
+ lastDay: TalerCoreBankResultByMethod<"getMonitor">;
+ lastMonth: TalerCoreBankResultByMethod<"getMonitor">;
+};
+
+export type LastMonitor = {
+ current: TalerCoreBankResultByMethod<"getMonitor">;
+ previous: TalerCoreBankResultByMethod<"getMonitor">;
+};
+export function revalidateLastMonitorInfo() {
+ return mutate(
+ (key) => Array.isArray(key) && key[key.length - 1] === "useLastMonitorInfo",
+ undefined,
+ { revalidate: true },
+ );
+}
+export function useLastMonitorInfo(
+ currentMoment: AbsoluteTime,
+ previousMoment: AbsoluteTime,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+) {
+ const {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ const { state: credentials } = useSessionState();
+ const token =
+ credentials.status !== "loggedIn" ? undefined : credentials.token;
+
+ async function fetcher([token, timeframe]: [
+ AccessToken,
+ TalerCorebankApi.MonitorTimeframeParam,
+ ]) {
+ const [current, previous] = await Promise.all([
+ api.getMonitor(token, { timeframe, date: currentMoment }),
+ api.getMonitor(token, { timeframe, date: previousMoment }),
+ ]);
+ return {
+ current,
+ previous,
+ };
+ }
+
+ const { data, error } = useSWR<LastMonitor, TalerHttpError>(
+ !token ? undefined : [token, timeframe, "useLastMonitorInfo"],
+ fetcher,
+ {
+ refreshInterval: 0,
+ refreshWhenHidden: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ refreshWhenOffline: false,
+ errorRetryCount: 0,
+ errorRetryInterval: 1,
+ shouldRetryOnError: false,
+ keepPreviousData: true,
+ },
+ );
+
+ if (data) return data;
+ if (error) return error;
+ return undefined;
+}
diff --git a/packages/bank-ui/src/hooks/session.ts b/packages/bank-ui/src/hooks/session.ts
new file mode 100644
index 000000000..4520d0e4a
--- /dev/null
+++ b/packages/bank-ui/src/hooks/session.ts
@@ -0,0 +1,134 @@
+/*
+ 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,
+ Codec,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForBoolean,
+ codecForConstString,
+ codecForString,
+} from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { mutate } from "swr";
+
+/**
+ * Has the information to reach and
+ * authenticate at the bank's backend.
+ */
+export type SessionState = LoggedIn | LoggedOut | Expired;
+
+interface LoggedIn {
+ status: "loggedIn";
+ isUserAdministrator: boolean;
+ username: string;
+ token: AccessToken;
+}
+interface Expired {
+ status: "expired";
+ isUserAdministrator: boolean;
+ username: string;
+}
+interface LoggedOut {
+ status: "loggedOut";
+}
+
+export const codecForSessionStateLoggedIn = (): Codec<LoggedIn> =>
+ buildCodecForObject<LoggedIn>()
+ .property("status", codecForConstString("loggedIn"))
+ .property("username", codecForString())
+ .property("token", codecForString() as Codec<AccessToken>)
+ .property("isUserAdministrator", codecForBoolean())
+ .build("SessionState.LoggedIn");
+
+export const codecForSessionStateExpired = (): Codec<Expired> =>
+ buildCodecForObject<Expired>()
+ .property("status", codecForConstString("expired"))
+ .property("username", codecForString())
+ .property("isUserAdministrator", codecForBoolean())
+ .build("SessionState.Expired");
+
+export const codecForSessionStateLoggedOut = (): Codec<LoggedOut> =>
+ buildCodecForObject<LoggedOut>()
+ .property("status", codecForConstString("loggedOut"))
+ .build("SessionState.LoggedOut");
+
+export const codecForSessionState = (): Codec<SessionState> =>
+ buildCodecForUnion<SessionState>()
+ .discriminateOn("status")
+ .alternative("loggedIn", codecForSessionStateLoggedIn())
+ .alternative("loggedOut", codecForSessionStateLoggedOut())
+ .alternative("expired", codecForSessionStateExpired())
+ .build("SessionState");
+
+export const defaultState: SessionState = {
+ status: "loggedOut",
+};
+
+export interface SessionStateHandler {
+ state: SessionState;
+ logOut(): void;
+ expired(): void;
+ logIn(info: { username: string; token: AccessToken }): void;
+}
+
+const SESSION_STATE_KEY = buildStorageKey(
+ "bank-session",
+ codecForSessionState(),
+);
+
+/**
+ * Return getters and setters for
+ * login credentials and backend's
+ * base URL.
+ */
+export function useSessionState(): SessionStateHandler {
+ const { value: state, update } = useLocalStorage(
+ SESSION_STATE_KEY,
+ defaultState,
+ );
+
+ return {
+ state,
+ logOut() {
+ update(defaultState);
+ },
+ expired() {
+ if (state.status === "loggedOut") return;
+ const nextState: SessionState = {
+ status: "expired",
+ username: state.username,
+ isUserAdministrator: state.username === "admin",
+ };
+ update(nextState);
+ },
+ logIn(info) {
+ // admin is defined by the username
+ const nextState: SessionState = {
+ status: "loggedIn",
+ ...info,
+ isUserAdministrator: info.username === "admin",
+ };
+ update(nextState);
+ cleanAllCache();
+ },
+ };
+}
+
+function cleanAllCache(): void {
+ mutate(() => true, undefined, { revalidate: false });
+}
diff --git a/packages/bank-ui/src/i18n/bank.pot b/packages/bank-ui/src/i18n/bank.pot
new file mode 100644
index 000000000..1d8595d17
--- /dev/null
+++ b/packages/bank-ui/src/i18n/bank.pot
@@ -0,0 +1,1742 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr ""
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr ""
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr ""
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr ""
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr ""
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr ""
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr ""
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server version "
+"\"%2$s\""
+msgstr ""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr ""
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr ""
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr ""
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr ""
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid "use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, c-format
+msgid "amount to transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgctxt "wire_transfer"
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgctxt "wire_transfer"
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr ""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid "The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, c-format
+msgid "Confirm the withdrawal operation"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with the "
+"GNU Taler WebExtension now or click the link if your WebExtension have the "
+"\"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, c-format
+msgid "Complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on this "
+"site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"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 %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid "This part of the demo shows how a bank that supports Taler directly would work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr ""
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"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"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid "The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected account "
+"is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:232
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:248
+#, c-format
+msgid "Confirm the operation"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid "Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, c-format
+msgid "Amount to send"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to confirm "
+"the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid "You can't change the legal name, please contact the your account administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid "You can't change the debt limit, please contact the your account administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on the "
+"next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make sure "
+"that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
diff --git a/packages/bank-ui/src/i18n/de.po b/packages/bank-ui/src/i18n/de.po
new file mode 100644
index 000000000..54fda4377
--- /dev/null
+++ b/packages/bank-ui/src/i18n/de.po
@@ -0,0 +1,1780 @@
+# This file is part of GNU Taler
+# (C) 2021 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-05-05 09:32+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/de/>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.4.3\n"
+
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr "Vorgang abgebrochen, bitte Fehler berichten"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr "Zeitüberschreitung der Anforderung"
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr "Anfrage verzögert sich"
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr "Unstimmige Antwort"
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr "Netzwerkfehler"
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr "Unerwarteter Fehler bei der Anforderung"
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr "Unerwarteter Fehler"
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "IBAN-Nummern haben normalerweise mehr als 4 Ziffern"
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "IBAN-Nummern haben normalerweise weniger als 34 Ziffern"
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr "IBAN-Ländercode wurde nicht gefunden"
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "IBAN-Nummer ist ungültig, die Prüfsumme ist falsch"
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server "
+"version \"%2$s\""
+msgstr ""
+"Das Bank-Backend wird nicht unterstützt. Unterstützte Version \"%1$s\", "
+"Serverversion \"%2$s\""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr "Höchste Abhebesumme"
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr ""
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr ""
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr ""
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid ""
+"use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, fuzzy, c-format
+msgid "subject"
+msgstr "Verwendungszweck"
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr "Betrag"
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, fuzzy, c-format
+msgid "amount to transfer"
+msgstr "Betrag"
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr "Zurück"
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr ""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, fuzzy, c-format
+msgid "password of the account"
+msgstr "Buchungen auf öffentlich sichtbaren Konten"
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr "Datum"
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Empfänger"
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr "Verwendungszweck"
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid ""
+"The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, fuzzy, c-format
+msgid "Confirm the withdrawal operation"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr "Schließen"
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with "
+"the GNU Taler WebExtension now or click the link if your WebExtension have "
+"the \"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, fuzzy, c-format
+msgid "Complete or cancel the operation in"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr "Weiter"
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on "
+"this site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"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 %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr ""
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:72
+#, fuzzy, c-format
+msgid "Accounts"
+msgstr "Betrag"
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr "Buchungen auf öffentlich sichtbaren Konten"
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"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"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid ""
+"The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected "
+"account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:232
+#, fuzzy, c-format
+msgid "Withdrawal"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/SolveChallengePage.tsx:248
+#, fuzzy, c-format
+msgid "Confirm the operation"
+msgstr "Abhebung bestätigen"
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr "Bestätigen"
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid ""
+"Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, fuzzy, c-format
+msgid "Amount to send"
+msgstr "Betrag"
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to "
+"confirm the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid ""
+"You can't change the legal name, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid ""
+"You can't change the debt limit, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your "
+"account administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on "
+"the next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make "
+"sure that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#, fuzzy, c-format
+#~ msgid "Confirmed"
+#~ msgstr "Bestätigen"
+
+#, c-format
+#~ msgid "Logout"
+#~ msgstr "Abmelden"
+
+#, c-format
+#~ msgid "Skip to main content"
+#~ msgstr "Navigationsmenü überspringen"
+
+#, c-format
+#~ msgid "Taler logo"
+#~ msgstr "Taler-Logo"
+
+#, c-format
+#~ msgid "Please login!"
+#~ msgstr "Bitte melden Sie sich an!"
+
+#, c-format
+#~ msgid "payto address"
+#~ msgstr "payto-Adresse"
+
+#, c-format
+#~ msgid "Bank account balance"
+#~ msgstr "Kontostand"
diff --git a/packages/bank-ui/src/i18n/es.po b/packages/bank-ui/src/i18n/es.po
new file mode 100644
index 000000000..39527f1dd
--- /dev/null
+++ b/packages/bank-ui/src/i18n/es.po
@@ -0,0 +1,2065 @@
+# This file is part of GNU Taler
+# (C) 2021 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2024-02-13 14:40+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/es/>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr "La operaicón falló, por favor reportelo"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr "La petición al servidor agoto su tiempo"
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr "La petición al servidor interrumpida"
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr "Respuesta malformada"
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr "Error de conexión"
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr "Error de pedido inesperado"
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr "Error inesperado"
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Los números IBAN usualmente tienen mas de 4 digitos"
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Los números IBAN usualmente tienen menos de 34 digitos"
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr "Código de pais de IBAN no encontrado"
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "El número IBAN no es válido, falló la verificación"
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server "
+"version \"%2$s\""
+msgstr ""
+"El servidor de bank no esta spoportado. Version soportada \"%1$s\", version "
+"del server \"%2$s\""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr "Monto máximo de extracción"
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr "Mostrar confirmación de extracción"
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr "Mostrar descripción de demo"
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr "Mostrar instalar la billetera primero"
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr "Usar formulario de extracción rápida"
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr "Mostrar información de depuración"
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr "requerido"
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr "IBAN debería tener letras mayúsculas y números"
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr "no válido"
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr "Debería ser mas grande que 0"
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr "el saldo no es suficiente"
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr "no tiene un patrón valido"
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr "solo cuentas \"IBAN\" son soportadas"
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr "usa el parámetro \"amount\" para indicar el monto a ser transferido"
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr "el monto no es válido"
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid ""
+"use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+"usa el parámetro \"message\" para indicar un texto de referencia en la "
+"transferencia"
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+"El pedido era inválido o el URI payto:// usado tiene características "
+"inaceptables."
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr "Sin permisos suficientes para completar la operación."
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr "La cuenta de destino \"%1$s\" no fue encontrada."
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr "El origen y destino de la transferencia no puede ser la misma."
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr "El saldo no es suficiente."
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr "La cuenta origen \"%1$s\" no fue encontrada."
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr "Transferencia bancaria creada!"
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr "Usando un formulario"
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr "Importando un URI payto://"
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr "Destinatario"
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr "Numero IBAN de la cuenta destinataria"
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr "Asunto de transferencia"
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr "asunto"
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr "algún texto para identificar la transferencia"
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr "Monto"
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, c-format
+msgid "amount to transfer"
+msgstr "monto a transferir"
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr "payto URI:"
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr "identificador de recurso uniforme de la cuenta destino"
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr "payto://iban/[iban-destinatario]?message=[asunto]&amount=[%1$s:X.Y]"
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgctxt "wire_transfer"
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgctxt "wire_transfer"
+msgid "Send"
+msgstr "Envíar"
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr "Falta nombre de usuario"
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr "Falta contraseña"
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr "Credenciales incorrectas para \"%1$s\""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr "Cuenta no encontrada"
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr "Nombre de usuario"
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr "nombre de usuario de la cuenta"
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr "Contraseña"
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr "contraseña de la cuenta"
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr "Verificar"
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr "Acceso"
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr "Registrarse"
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr "Últimas transacciones"
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr "Fecha"
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Contraparte"
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr "Asunto"
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr "enviado"
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr "recibido"
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr "valor inválido"
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr "hacia"
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr "desde"
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr "Primera página"
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr "Siguiente"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr "Transferencia bancaria completada!"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr "La extracción fue abortada anteriormente y no puede ser confirmada"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+"La operación de extracción no puede ser confirmada antes de que una "
+"billetera acepte la transaccion."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr "El id de operación es invalido."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr "La operación no se encontró."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr "El saldo no es suficiente para la operación."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid ""
+"The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+"La operación en la reserva ya ha sido confirmada previamente y no puede ser "
+"abortada"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, c-format
+msgid "Confirm the withdrawal operation"
+msgstr "Confirme la operación de extracción"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr "Detalle de transferencia bancaria"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr "Cuenta del operador del Taler Exchange"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr "Nombre del operador del Taler Exchange"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr "Transferencia"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr "Autenticación requerida"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr "Esta operación fue creada con otro usuario"
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+"No autorizado para hacer la operación, quizá la sesión haya expirado or "
+"cambió la contraseña."
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr "La operación fue rechazada debido a saldo insuficiente."
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr "La extracción fue confirmada"
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+"La transferencia bancaria al operador Taler fue iniciada. Pronto recibirás "
+"el monto pedido en tu billetera Taler."
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr "No mostrar otra vez"
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr "Cerrar"
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr "En este dispositivo"
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with "
+"the GNU Taler WebExtension now or click the link if your WebExtension have "
+"the \"Inject Taler support\" option enabled."
+msgstr ""
+"Si esta usando un explorador web de escritorio deberías acceder ahora a tu "
+"billletera con la GNU Taler WebExtension o hacer click en el link si tu "
+"extensión tiene la configuración \"Inyectar soporte para Taler\" habilitada."
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr "Comenzar"
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr "En un dispotivo mobile"
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr "Escanear el código QR con tu dispotivo móvil."
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr "Ya hay una operación"
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, c-format
+msgid "Complete or cancel the operation in"
+msgstr "Completa o cancela la operación en"
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr "esta página"
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr "inválido"
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr "El servidor respondió con una URI de extracción inválida"
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr "URI de extracción: %1$s"
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr "La operación fue rechazada debido a fundos insuficientes"
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr "Continuar"
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr "Prepare su billetera"
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on "
+"this site."
+msgstr ""
+"Despues de usar tu billetera necesitarás confirmar o cancelar la operación "
+"en este sitio."
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr "Necesitas una GNU Taler Wallet"
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr "Si no tienes una todavía puedes seguir las instrucciones en"
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr "Enviar dinero"
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr "a una billetera %1$s"
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr "Extraer dinero digital a tu billetera móvil o extesión web"
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr "operación lista"
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr "a otra cuenta bancaria"
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+"Hacer una transferencia bancaria a una cuenta con un número de cuenta "
+"conocido."
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr "Detalles de transferencia"
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr "Este es un banco de demostración"
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"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 %1$s."
+msgstr ""
+"Esta parte de la demostración muestra cómo funciona un banco que soporta "
+"Taler directamente. Además de usar tu propia cuenta de banco, también podrás "
+"ver el historial de transacciones de algunas %1$s."
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work."
+msgstr ""
+"Esta parte de la demostración muetra como un banco que soporta Taler "
+"directamente funcionaría."
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr "Operación pendiente de eliminación de cuenta"
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr "Operación pendiente de actualización de cuenta"
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr "Operación pendiente de actualización de password"
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr "Operación pendiente de transacción"
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr "Operación pendiente de extracción"
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr "Operación pendiente de egreso"
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr "Puedes completar o cancelar la operación en"
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr "Error interno, por favor reporte el error."
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr "Preferencias"
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr "Bienvenido/a, %1$s"
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr "Hacer una transferencia bancaria"
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr "Cuentas"
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr "Una lista de todas las cuentas en el banco."
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr "Crear cuenta"
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr "Nombre"
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr "Saldo"
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr "Acciones"
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr "desconocido"
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr "cambiar contraseña"
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr "egresos"
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr "elimiar"
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr "Egreso no soportado"
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr "Seleccione una sección"
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr "Última hora"
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr "Último día"
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr "Último mes"
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr "Último año"
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr "Último Año"
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr "Vólumen de comercio en %1$s comparado con %2$s"
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr "Ingreso"
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr "Egreso"
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr "Envios de dinero"
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr "Recibos de dinero"
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr "descargar estadísticas en CSV"
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr "Descendiente por"
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr "Ascendente por"
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr "Descargar estadísticas del banco"
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr "Incluir métrica horaria"
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr "Incluir métrica diaria"
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr "Incluir métrica mensual"
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr "Incluir métrica anual"
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr "Incluir encabezado de tabla"
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr "Agregar métrica previa para comparar"
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr "Fallar en el primer error"
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr "Descargar"
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr "descargando... %1$s"
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr "Descarga completada"
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr "click aquí para guardar el archivo en su computadora"
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr "Historial de cuentas públicas"
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr "Actualmente, el banco no está aceptado nuevos registros!"
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr "Falta nombre"
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr "Solo use letras y números, y comience con una letra minúscula"
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr "La contraseña no coincide"
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr "El servidor repondio con teléfono o dirección de correo inválido."
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+"El registro está deshabilitado porque el banco se quedó sin crédito bonus."
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr "Sin permisos suficientes para crear esa cuenta."
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr "El identificador de cuenta ya está tomado."
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr "El nombre de usuario ya está tomado."
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr "El nombre de usuario no puede ser usado porque esta reservado."
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr "Solo el administrador tiene permitido cambiar el límite de deuda."
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr "No hay información para el canal de autenticación seleccionado."
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr "Canal de autenticación no esta soportado."
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+"Solo el administrador puede crear cuentas con el segundo factor de "
+"autenticación."
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr "Registro de cuenta"
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr "Repita la contraseña"
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr "Crear un usuario aleatorio temporal"
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr "Si tienes una billetera Taler instalada en este dispositivo"
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"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"
+msgstr ""
+"Veras los detalles de la operación en tu billetera incluyendo comisiones (si "
+"aplicán). Si todavía no tienes una puedes instalarla siguiendo las "
+"instrucciones en"
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr "Retirar"
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr "O si tienes la billetera en otro dispositivo"
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr "Escanea el QR debajo para comenzar la extracción."
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr "Operación abortada"
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+"La transferencia bancaria a la cuenta del operador del Taler Exchange fue "
+"abortada, su saldo no fue afectado."
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+"Ya puedes cerrar esta pagina or continuar a la página de estado de cuenta."
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr "Listo"
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr "Operación cancelada"
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid ""
+"The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+"La operación está marcada como 'seleccionada' pero algunos pasos en la "
+"extracción fallaron"
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+"La cuenta está seleccionada pero no se encontró el identificador de "
+"extracción."
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+"Hay un identificador de extracción pero la cuenta no ha sido seleccionada o "
+"la selccionada es inválida."
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected "
+"account is invalid."
+msgstr ""
+"No hay un identificador de extracción y ninguna cuenta a sido seleccionada o "
+"la seleccionada es inválida."
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr "Operación no encontrada"
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+"Esta operación no es conocida por el servidor. El identificador de operación "
+"es incorrecto o el server borró la información de la operación antes de "
+"llegar hasta aquí."
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr "Continuar al panel"
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr "Egreso no econtrado. También puede significar que ya ha sido abortado."
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr "Desafío no encontrado."
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr "Este usuario no está autorizado para completar este desafío."
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr "Demasiados intentos, intente otro código."
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr "El código de confirmación es erroneo, intente otra vez."
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr "La operación expiró."
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr "La operación falló."
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr "La operación necesita otra confirmación para completar."
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr "Eliminación de cuenta"
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr "Actualización de cuenta"
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr "Actualización de contraseña"
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr "Transferencia bancaria"
+
+#: src/pages/SolveChallengePage.tsx:232
+#, c-format
+msgid "Withdrawal"
+msgstr "Extracción"
+
+#: src/pages/SolveChallengePage.tsx:248
+#, c-format
+msgid "Confirm the operation"
+msgstr "Confirmar la operación"
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr "Ingresar el código de confirmación"
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr "Confirmar"
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr "Enviar otra vez"
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr "Enviar código"
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr "Detalles de operación"
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr "Detalles del desafío"
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr "Enviado a"
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr "Al teléfono"
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr "Al email"
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr "El URI de estracción no es válido"
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr "Últimos egresos"
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr "Creado"
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr "Débito total"
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr "Crédito total"
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr "Detalles"
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr "Borrar"
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr "Credenciales"
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr "Egresos"
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr "Imposible crear un egreso"
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr "La configuración del banco no soporta operaciones de egreso."
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr "necesita ser mayor debido a las comisiones"
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr "el total de la transferencia en destino será cero"
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr "Egreso creado"
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid ""
+"Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+"Se detectó una petición duplicada, verifique si la operación tuvo éxito o "
+"intente otra vez."
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr "La tasa de conversión se aplicó incorrectamente"
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr "La cuenta no tiene fondos suficientes"
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr "Egresos no están soportados"
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr "Falta dirección de egreso en el perfíl"
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+"El envío del mensaje de confirmación falló, intente mas tarde o contacte al "
+"administrador."
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr "Tasa de conversión"
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr "Comisión"
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr "Hacia cuenta"
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr "No hay cuenta de egreso"
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr "Antes de hacer un egreso necesita completar su perfíl"
+
+#: src/pages/business/CreateCashout.tsx:440
+#, c-format
+msgid "Amount to send"
+msgstr "Monto a enviar"
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr "Monto a recibir"
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr "Costo total"
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr "Saldo remanente"
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr "Antes de comisión"
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr "Total de egreso"
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr "No hay canal de egreso disponible"
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to "
+"confirm the operation"
+msgstr ""
+"Antes de hacer un egreso el servidor necesita proveer un segundo canal para "
+"confirmar la operación"
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr "Segundo factor de autenticación"
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr "Correo eletrónico"
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr "agrege un correo en su perfíl para habilitar esta opción"
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr "SMS"
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr "agregue un número de teléfono para habilitar esta opción"
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr "Egreso para cuenta %1$s"
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr "no tiene el patrón de un número IBAN"
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr "no tiene el patrón de un correo electrónico"
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr "debería comenzar con un +"
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr "número de teléfono no puede tener otra cosa que numeros"
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr "identificador de cuenta en el banco"
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr "nombre de la persona dueña de la cuenta"
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr "IBAN interno"
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr "si está vacío un número de cuenta aleatorio será asignado"
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr "identificador de cuenta para transferencia bancaria"
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr "Teléfono"
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr "IBAN de egreso"
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+"numero de cuenta donde el dinero será enviado cuando se ejecuten egresos"
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr "Máxima deuda"
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr "cuanto tiene habilitado a transferir despues de un saldo en cero"
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr "Es un Taler Exchange?"
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr "Este servidor no tiene soporte para segundo factor de autenticación."
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr "Hábilitar segundo factor de autenticación"
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr "Usando correo eletrónico"
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr "Usando SMS"
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr "Es una cuenta pública?"
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr "las cuentas públicas tienen su saldo accesible al público"
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr "Cuenta actualizada"
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr "Los permisos para cambiar la cuenta no son suficientes"
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr "El nombre de usaurio no se encontró"
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid ""
+"You can't change the legal name, please contact the your account "
+"administrator."
+msgstr ""
+"No puede cambiar el nombre legal, por favor contacte el administrador de la "
+"cuenta."
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid ""
+"You can't change the debt limit, please contact the your account "
+"administrator."
+msgstr ""
+"No puede cambiar el límite de deuda, por favor contacte el administrador de "
+"la cuenta."
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+"No puede cambiar la dirección de egreso, por favor contacte al administrador "
+"de la cuenta."
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr "Cuenta \"%1$s\""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr "Cambiar detalles"
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr "Actualizar"
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr "la contraseña no coincide"
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr "La contraseña cambió"
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr "No está autorizado a cambiar el password, quizá la sesión es invalida."
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your "
+"account administrator."
+msgstr ""
+"Se necesita el password viejo para cambiar la contraseña. Si no lo tiene "
+"contacte a su administrador."
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+"Su actual contraseña no coincide, no puede cambiar a una nueva contraseña."
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr "Actualizar contraseña"
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr "Nueva contraseña"
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr "Escribalo otra vez"
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr "repita la misma contraseña"
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr "Contraseña actual"
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr "su actual contraseña, por seguridad"
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr "Cambiar"
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on "
+"the next login."
+msgstr ""
+"Cuenta creada con contraseña \"%1$s\". El usuario debe cambiar la contraseña "
+"en el siguiente ingreso."
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr "El servidor respondió que el teléfono o correo eletrónico es invalido"
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr "Los permisos para ejecutar la operación no son suficientes"
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr "El nombre del usuario ya está tomado"
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr "El id de cuenta ya está tomado"
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr "El banco no tiene mas crédito de bonus."
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+"El nombre de usuario de la cuenta no puede userse porque está reservado"
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr "No puede crear cuentas"
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr "Solo los administradores del sistema pueden crear cuentas."
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr "Nueva cuenta"
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr "Crear"
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr "No se puede eliminar la cuenta"
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make "
+"sure that the owner make a complete cashout."
+msgstr ""
+"La cuenta no puede ser eliminada mientras tiene saldo. Primero aseguresé que "
+"el dueño haga un egreso completo."
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr "Cuenta eliminada"
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr "No tiene permisos suficientes para eliminar la cuenta."
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr "El nombr ede usuario no se encontró."
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr "No se puede eliminar un nombre de usuario reservado."
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr "No se puede eliminar una cuenta con saldo diferente a cero."
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr "el nombre no coincide"
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr "Está por eliminar la cuenta"
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr "Este paso no puede ser deshecho."
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr "Borrando cuenta \"%1$s\""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr "Verificación"
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr "ingrese el nombre de cuenta que será eliminado"
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr "debería ser un correo electrónico"
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr "Este egreso no se encontró. Quizá fue abortado."
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr "Detalles de egreso"
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr "Debitado"
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr "Acreditado"
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr "Bienvenido a %1$s!"
+
+#, c-format
+#~ msgid ""
+#~ "You can't change the contact data, please contact the your account "
+#~ "administrator."
+#~ msgstr ""
+#~ "No puede cambiar los datos de contacto, por favor contacte al "
+#~ "administrador de la cuenta."
+
+#, c-format
+#~ msgid "Account not found."
+#~ msgstr "Cuenta no encontrada."
+
+#, c-format
+#~ msgid "Confirmed"
+#~ msgstr "Confirmado"
+
+#, c-format
+#~ msgid "Status"
+#~ msgstr "Estado"
+
+#, c-format
+#~ msgid "never"
+#~ msgstr "nunca"
+
+#, c-format
+#~ msgid "Cashout was already confimed."
+#~ msgstr "Egreso ya fue confirmado."
+
+#, c-format
+#~ msgid "Cashout operation is not supported."
+#~ msgstr "Operación de egreso no soportada."
+
+#, c-format
+#~ msgid "The cashout operation is already aborted."
+#~ msgstr "La operación de egreso ya está abortada."
+
+#, c-format
+#~ msgid "Missing destination account."
+#~ msgstr "Falta cuenta destino."
+
+#, c-format
+#~ msgid "Too many failed attempts."
+#~ msgstr "Demasiados intentos fallidos."
+
+#, c-format
+#~ msgid "The code for this cashout is invalid."
+#~ msgstr "El código para este egreso es invalido."
+
+#, c-format
+#~ msgid "Abort"
+#~ msgstr "Abortar"
+
+#, fuzzy, c-format
+#~ msgid ""
+#~ "The Taler Exchange operator is selected but the Taler Exchange operator "
+#~ "account is missing or invalid."
+#~ msgstr ""
+#~ "El operador esta seleccionado pero la URI payto del operador falta o es "
+#~ "invalida."
+
+#, c-format
+#~ msgid "could not be parsed"
+#~ msgstr "inválido"
+
+#, c-format
+#~ msgid "Confirmation the operation using"
+#~ msgstr "Confirme la operación usando"
+
+#, c-format
+#~ msgid "Is public"
+#~ msgstr "es publica"
+
+#, c-format
+#~ msgid "Logout"
+#~ msgstr "Cierre de sesión"
+
+#, c-format
+#~ msgid "Skip to main content"
+#~ msgstr "Saltar el menú de navegación"
+
+#, c-format
+#~ msgid "Taler logo"
+#~ msgstr "Logo Taler"
+
+#, c-format
+#~ msgid "Please login!"
+#~ msgstr "Por favor inicia sesión!"
+
+#, c-format
+#~ msgid "Login"
+#~ msgstr "Iniciar sesión"
+
+#, c-format
+#~ msgid "Missing IBAN"
+#~ msgstr "Falta IBAN"
+
+#, c-format
+#~ msgid "Missing subject"
+#~ msgstr "Falta asunto"
+
+#, c-format
+#~ msgid "Receiver IBAN:"
+#~ msgstr "IBAN receptor:"
+
+#, c-format
+#~ msgid "Amount:"
+#~ msgstr "Monto:"
+
+#, c-format
+#~ msgid "Field(s) missing."
+#~ msgstr "Faltan campo(s)."
+
+#, c-format
+#~ msgid "Want to try the raw payto://-format?"
+#~ msgstr "Quieres probar el formato payto:// ?"
+
+#, c-format
+#~ msgid "Missing payto address"
+#~ msgstr "Falta direccion payto"
+
+#, c-format
+#~ msgid "Transfer money to account identified by payto:// URI:"
+#~ msgstr "Transferir dinero a la cuenta identificada por la URI payto://:"
+
+#, c-format
+#~ msgid "payto address"
+#~ msgstr "direccion payto"
+
+#, c-format
+#~ msgid "Could not create the wire transfer"
+#~ msgstr "No se pudo create la transferencia bancaria"
+
+#, c-format
+#~ msgid "Transfer creation gave response error"
+#~ msgstr "La creación de la transferencia dió una respuesta erronea"
+
+#, c-format
+#~ msgid "No credentials given."
+#~ msgstr "Se dieron las credenciales incorrectas."
+
+#, c-format
+#~ msgid "Withdrawal creation gave response error"
+#~ msgstr "La creación de retiro dió una respuesta errónea"
+
+#, c-format
+#~ msgid "Obtain digital cash"
+#~ msgstr "Obtener dinero digital"
+
+#, c-format
+#~ msgid "Click %1$s to open your Taler wallet!"
+#~ msgstr "Click %1$s para abrir una cartera Taler!"
+
+#, c-format
+#~ msgid "Authorize withdrawal by solving challenge"
+#~ msgstr "Autorizar retiro resolviendo una pregunta"
+
+#, c-format
+#~ msgid "What is"
+#~ msgstr "Cuanto es"
+
+#, c-format
+#~ msgid "Answer is wrong."
+#~ msgstr "La respuesta es incorrecta."
+
+#, c-format
+#~ msgid ""
+#~ "A this point, a %1$s bank would ask for an additional authentication "
+#~ "proof (PIN/TAN, one time password, ..), instead of a simple calculation."
+#~ msgstr ""
+#~ "En este punto, un banco %1$s preguntaría por una prueba adicional de "
+#~ "autenticación (PIN/TAN, password de un solo uso, ....), en vez de un "
+#~ "simple cálculo."
+
+#, c-format
+#~ msgid "Could not confirm the withdrawal"
+#~ msgstr "No se pudo confirmar la retirada"
+
+#, c-format
+#~ msgid "Withdrawal confirmation gave response error"
+#~ msgstr "La confirmación de retiro dió una respuesta errónea"
+
+#, c-format
+#~ msgid "Withdrawal aborted!"
+#~ msgstr "Este retiro fue cancelado!"
+
+#, c-format
+#~ msgid "withdrawal (%1$s) was never (correctly) created at the bank..."
+#~ msgstr "retiro (%1$s) nunca fue (correctamente) generado en el banco..."
+
+#, c-format
+#~ msgid "Username or account label '%1$s' not found. Won't login."
+#~ msgstr ""
+#~ "Nombre de usuario o etiqueta de cuenta '%1$s' no encontrada. No se "
+#~ "iniciará sesión."
+
+#, c-format
+#~ msgid "Account information could not be retrieved."
+#~ msgstr "La información de la cuenta no pudo ser accedida."
+
+#, c-format
+#~ msgid "Bank account balance"
+#~ msgstr "Balance de cuenta bancaria"
+
+#, c-format
+#~ msgid "Payments"
+#~ msgstr "Pagos"
+
+#, c-format
+#~ msgid "List of public accounts could not be retrieved."
+#~ msgstr "La lista de cuentas públicas no pudo ser accedida."
+
+#, c-format
+#~ msgid "Please register!"
+#~ msgstr "Por favor, registrese!"
+
+#, c-format
+#~ msgid "New registration gave response error"
+#~ msgstr "Nuevo registro dió una respuesta errónea"
+
+#, c-format
+#~ msgid "Bank menu"
+#~ msgstr "Menu del banco"
+
+#, c-format
+#~ msgid "Select option2"
+#~ msgstr "Seleccione opción 2"
+
+#, c-format
+#~ msgid "days"
+#~ msgstr "días"
+
+#, c-format
+#~ msgid "hours"
+#~ msgstr "horas"
+
+#, c-format
+#~ msgid "minutes"
+#~ msgstr "minutos"
+
+#, c-format
+#~ msgid "seconds"
+#~ msgstr "segundos"
+
+#~ msgid "this link"
+#~ msgstr "este link"
diff --git a/packages/bank-ui/src/i18n/fr.po b/packages/bank-ui/src/i18n/fr.po
new file mode 100644
index 000000000..b02cbe618
--- /dev/null
+++ b/packages/bank-ui/src/i18n/fr.po
@@ -0,0 +1,1752 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: 2024-02-28 08:07+0000\n"
+"Last-Translator: d0p1 <contact@d0p1.eu>\n"
+"Language-Team: French <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/fr/>\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr "L'opération a échoué, veuillez le signaler"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr ""
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr ""
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr ""
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr "Erreur réseau"
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr ""
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr ""
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server "
+"version \"%2$s\""
+msgstr ""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr ""
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr ""
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr ""
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr ""
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid ""
+"use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, c-format
+msgid "amount to transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr ""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid ""
+"The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, c-format
+msgid "Confirm the withdrawal operation"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with "
+"the GNU Taler WebExtension now or click the link if your WebExtension have "
+"the \"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, c-format
+msgid "Complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on "
+"this site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"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 %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr ""
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"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"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid ""
+"The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected "
+"account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:232
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:248
+#, c-format
+msgid "Confirm the operation"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid ""
+"Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, c-format
+msgid "Amount to send"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to "
+"confirm the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid ""
+"You can't change the legal name, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid ""
+"You can't change the debt limit, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your "
+"account administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on "
+"the next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make "
+"sure that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
diff --git a/packages/bank-ui/src/i18n/it.po b/packages/bank-ui/src/i18n/it.po
new file mode 100644
index 000000000..7aeaca3a8
--- /dev/null
+++ b/packages/bank-ui/src/i18n/it.po
@@ -0,0 +1,1843 @@
+# This file is part of GNU Taler
+# (C) 2021 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: 2023-08-15 07:28+0000\n"
+"Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/taler-"
+"bank-spa/it/>\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
+
+#: src/utils.ts:137
+#, fuzzy, c-format
+msgid "Operation failed, please report"
+msgstr "Registrazione"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr ""
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr ""
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr ""
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr ""
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr ""
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr ""
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr ""
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr ""
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr ""
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr ""
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server "
+"version \"%2$s\""
+msgstr ""
+
+#: src/hooks/preferences.ts:55
+#, fuzzy, c-format
+msgid "Max withdrawal amount"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/hooks/preferences.ts:57
+#, fuzzy, c-format
+msgid "Show withdrawal confirmation"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr ""
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, fuzzy, c-format
+msgid "Use fast withdrawal form"
+msgstr "Ritira contante"
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid ""
+"use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, fuzzy, c-format
+msgid "Not enough permission to complete the operation."
+msgstr "La banca sta creando l'operazione..."
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, fuzzy, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr "Lista conti pubblici non trovata."
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, fuzzy, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr "Lista conti pubblici non trovata."
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, fuzzy, c-format
+msgid "Wire transfer created!"
+msgstr "Bonifico"
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, fuzzy, c-format
+msgid "Transfer subject"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, fuzzy, c-format
+msgid "subject"
+msgstr "Soggetto"
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr "Importo"
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, fuzzy, c-format
+msgid "amount to transfer"
+msgstr "Somma da ritirare"
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, fuzzy, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr "Credenziali invalide."
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, fuzzy, c-format
+msgid "username of the account"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, fuzzy, c-format
+msgid "password of the account"
+msgstr "Storico dei conti pubblici"
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr "Registrati"
+
+#: src/components/Transactions/views.tsx:52
+#, fuzzy, c-format
+msgid "Latest transactions"
+msgstr "Ultime transazioni:"
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr "Data"
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Controparte"
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr "Soggetto"
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, fuzzy, c-format
+msgid "Wire transfer completed!"
+msgstr "Bonifico"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, fuzzy, c-format
+msgid "The operation was not found."
+msgstr "Lista conti pubblici non trovata."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid ""
+"The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, fuzzy, c-format
+msgid "Confirm the withdrawal operation"
+msgstr "Conferma il ritiro"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, fuzzy, c-format
+msgid "Wire transfer details"
+msgstr "Bonifico"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, fuzzy, c-format
+msgid "Withdrawal confirmed"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with "
+"the GNU Taler WebExtension now or click the link if your WebExtension have "
+"the \"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, fuzzy, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr "Usa questo codice QR per ritirare contante nel tuo wallet:"
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, fuzzy, c-format
+msgid "Complete or cancel the operation in"
+msgstr "Conferma il ritiro"
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, fuzzy, c-format
+msgid "Withdraw URI: %1$s"
+msgstr "Prelevare"
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on "
+"this site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, fuzzy, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr "Ritira contante nel portafoglio Taler"
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, fuzzy, c-format
+msgid "to another bank account"
+msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, fuzzy, c-format
+msgid "Transfer details"
+msgstr "Effettua un bonifico"
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"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 %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid ""
+"This part of the demo shows how a bank that supports Taler directly would "
+"work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, fuzzy, c-format
+msgid "Internal error, please report."
+msgstr "Registrazione"
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, fuzzy, c-format
+msgid "Make a wire transfer"
+msgstr "Chiudi il bonifico"
+
+#: src/pages/admin/AccountList.tsx:72
+#, fuzzy, c-format
+msgid "Accounts"
+msgstr "Importo"
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, fuzzy, c-format
+msgid "cashouts"
+msgstr "Ultime transazioni:"
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr "Storico dei conti pubblici"
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, fuzzy, c-format
+msgid "Missing name"
+msgstr "indirizzo Payto"
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"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"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr "Prelevare"
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, fuzzy, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr "Chiudi il ritiro Taler"
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid ""
+"The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected "
+"account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, fuzzy, c-format
+msgid "The operation failed."
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, fuzzy, c-format
+msgid "Wire transfer"
+msgstr "Bonifico"
+
+#: src/pages/SolveChallengePage.tsx:232
+#, fuzzy, c-format
+msgid "Withdrawal"
+msgstr "Prelevare"
+
+#: src/pages/SolveChallengePage.tsx:248
+#, fuzzy, c-format
+msgid "Confirm the operation"
+msgstr "Conferma il ritiro"
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr "Conferma"
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, fuzzy, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr "Questo ritiro è stato annullato!"
+
+#: src/components/Cashouts/views.tsx:100
+#, fuzzy, c-format
+msgid "Latest cashouts"
+msgstr "Ultime transazioni:"
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, fuzzy, c-format
+msgid "Credentials"
+msgstr "Credenziali invalide."
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid ""
+"Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, fuzzy, c-format
+msgid "Amount to send"
+msgstr "Somma da ritirare"
+
+#: src/pages/business/CreateCashout.tsx:441
+#, fuzzy, c-format
+msgid "Amount to receive"
+msgstr "Somma da ritirare"
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to "
+"confirm the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid ""
+"You can't change the legal name, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid ""
+"You can't change the debt limit, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your "
+"account administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on "
+"the next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make "
+"sure that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
+
+#, fuzzy, c-format
+#~ msgid "Account not found."
+#~ msgstr "Lista conti pubblici non trovata."
+
+#, fuzzy, c-format
+#~ msgid "Confirmed"
+#~ msgstr "Conferma"
+
+#, c-format
+#~ msgid "Abort"
+#~ msgstr "Annulla"
+
+#, c-format
+#~ msgid "Skip to main content"
+#~ msgstr "Saltare il menu di navigazione"
+
+#, c-format
+#~ msgid "Please login!"
+#~ msgstr "Accedi!"
+
+#, c-format
+#~ msgid "Login"
+#~ msgstr "Accedi"
+
+#, fuzzy, c-format
+#~ msgid "Amount:"
+#~ msgstr "Somma"
+
+#, c-format
+#~ msgid "Want to try the raw payto://-format?"
+#~ msgstr "Prova il trasferimento tramite il formato Payto!"
+
+#, fuzzy, c-format
+#~ msgid "Transfer money to account identified by payto:// URI:"
+#~ msgstr "Trasferisci fondi a un altro conto di questa banca:"
+
+#, c-format
+#~ msgid "payto address"
+#~ msgstr "indirizzo Payto"
+
+#, fuzzy, c-format
+#~ msgid "No credentials given."
+#~ msgstr "Credenziali invalide."
+
+#, c-format
+#~ msgid "Username or account label '%1$s' not found. Won't login."
+#~ msgstr "L'utente '%1$s' non esiste. Login impossibile"
+
+#, c-format
+#~ msgid "Account information could not be retrieved."
+#~ msgstr "Impossibile ricevere le informazioni relative al conto."
+
+#, fuzzy, c-format
+#~ msgid "Bank account balance"
+#~ msgstr "Bilancio:"
+
+#, c-format
+#~ msgid "List of public accounts could not be retrieved."
+#~ msgstr "Lista conti pubblici non pervenuta."
+
+#, fuzzy, c-format
+#~ msgid "Please register!"
+#~ msgstr "Accedi!"
+
+#~ msgid "this link"
+#~ msgstr "questo link"
+
+#~ msgid "Clear"
+#~ msgstr "Cancella"
+
+#~ msgid "Demo Bank"
+#~ msgstr "Banca 'demo'"
+
+#~ msgid "Go back"
+#~ msgstr "Indietro"
+
+#~ msgid "Transfer money via the Payto system:"
+#~ msgstr "Effettua un bonifico tramite il sistema Payto:"
+
+#~ msgid "Withdraw Money into a Taler wallet"
+#~ msgstr "Ritira contante nel portafoglio Taler"
+
+#~ msgid "Register to the euFin bank!"
+#~ msgstr "Apri un conto in banca euFin!"
+
+#~ msgid "Page has a problem: logged in but backend state is lost."
+#~ msgstr ""
+#~ "Stato inconsistente: accesso utente effettuato ma stato con server perso."
+
+#, fuzzy
+#~ msgid "Welcome to the euFin bank!"
+#~ msgstr "Benvenuti in banca euFin!"
diff --git a/packages/bank-ui/src/i18n/poheader b/packages/bank-ui/src/i18n/poheader
new file mode 100644
index 000000000..d7a371934
--- /dev/null
+++ b/packages/bank-ui/src/i18n/poheader
@@ -0,0 +1,26 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/bank-ui/src/i18n/ru.po b/packages/bank-ui/src/i18n/ru.po
new file mode 100644
index 000000000..8cd1eec53
--- /dev/null
+++ b/packages/bank-ui/src/i18n/ru.po
@@ -0,0 +1,1794 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: 2024-05-10 00:13+0000\n"
+"Last-Translator: Lily Ponomareva <lilyponomareva2017@gmail.com>\n"
+"Language-Team: Russian <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/ru/>\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.4.3\n"
+
+#: src/utils.ts:137
+#, c-format
+msgid "Operation failed, please report"
+msgstr "Не удалось выполнить операцию, сообщите об этом"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr "Тайм-аут запроса"
+
+#: src/utils.ts:165
+#, c-format
+msgid "Request throttled"
+msgstr "Запрос замедлен"
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr "Неправильный ответ"
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr "Ошибка сети"
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr "Неожиданная ошибка запроса"
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr "Непредвиденная ошибка"
+
+#: src/utils.ts:377
+#, c-format
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Номера IBAN обычно содержат более 4 цифр"
+
+#: src/utils.ts:379
+#, c-format
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Номера IBAN обычно содержат менее 34 цифр"
+
+#: src/utils.ts:387
+#, c-format
+msgid "IBAN country code not found"
+msgstr "Код страны IBAN не найден"
+
+#: src/utils.ts:401
+#, c-format
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "Номер IBAN недействителен, контрольная сумма неверна"
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server version "
+"\"%2$s\""
+msgstr ""
+"Бэкенд банка не поддерживается. Поддерживаемая версия \"%1$s\" а версия "
+"сервера \"%2$s\""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr "Максимальная сумма вывода"
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr "Показать подтверждение вывода средств"
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr "Показать описание демо"
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr "Сначала показать как установить кошелёк"
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr "Используйте форму быстрого вывода средств"
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr "Показать информацию для отладки"
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr "обязательно"
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr "IBAN должен состоять только из прописных букв и цифр"
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr "недопустимый"
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr "должно быть больше 0"
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr "Недостаточно средств на балансе"
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr "не следует шаблону"
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr "поддерживаются только \"IBAN\""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr "Используйте параметр \"Сумма\" для указания суммы перевода"
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr "сумма не является действительной"
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid "use the \"message\" parameter to specify a reference text for the transfer"
+msgstr "используйте параметр \"message\" для текста причины перевода"
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+"Запрос был неверным или payto://-URI использовал недопустимую "
+"функциональность."
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr "Не хватает разрешения для завершения операции."
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr "Целевой счет \"%1$s\" не найден."
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr "Пункт отправления и пункт назначения перевода не могут совпадать."
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr "Вашего баланса недостаточно."
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr "Исходный аккаунт \"%1$s\" не найден."
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr "Банковский перевод создан!"
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr "Используя форму"
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr "Импорт payto:// URI"
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr "Получатель"
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr "IBAN счета получателя"
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr "Причина перевода"
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr "причина"
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr "какой-то текст для идентификации перевода"
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr "Сумма"
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, c-format
+msgid "amount to transfer"
+msgstr "сумма для перевода"
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr "payto URI:"
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr "унифицированный идентификатор ресурса целевой учетной записи"
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+"payto://iban/[iban_получателя]?message=[причина_платежа]&amount=[%1$s:X.Y]"
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgctxt "wire_transfer"
+msgid "Cancel"
+msgstr "Отмена"
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgctxt "wire_transfer"
+msgid "Send"
+msgstr "Отправить"
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr "Отсутствует имя пользователя"
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr "Отсутствует пароль"
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr "Неверные учетные данные для «%1$s» ‎"
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr "Учётная запись не найдена"
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr "Имя пользователя"
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr "имя пользователя счёта"
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr "Пароль"
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr "пароль от счёта"
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr "Проверить"
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr "Войти"
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr "Регистрация"
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr "Последние транзакции"
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr "Дата"
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr "Контрагент"
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr "Причина"
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr "отправлено"
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr "получено"
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr "Недопустимое значение"
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr "к"
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr "от"
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr "Первая страница"
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr "Далее"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr "Отправка перевода завершена!"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr "Вывод средств был прерван ранее и не может быть подтвержден"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+"Операция по выводу средств не может быть подтверждена до того как кошёлек "
+"примет транзакцию."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr "Идентификатор операции недействителен."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr "Операция не найдена."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr "Вашего баланса недостаточно для проведения операции."
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid "The reserve operation has been confirmed previously and can't be aborted"
+msgstr "Резервная операция была подтверждена ранее и не может быть прервана"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, c-format
+msgid "Confirm the withdrawal operation"
+msgstr "Подтвердите операцию вывода"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr "Детали банковского перевода"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr "Счет оператора Обменника Taler"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr "Название оператора Обменника Taler"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr "Перевести"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr "Требуется аутентификация"
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr "Эта операция была создана с другим именем пользователя"
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+"Неавторизированное выполнение операции, возможно истек сеанс или изменён "
+"пароль."
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr "Операция отклонена из-за нехватки средств."
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr "Вывод подтверждён"
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+"Инициирован банковский перевод оператору Taler. Вскоре вы получите "
+"запрошенную сумму на свой кошелёк Taler."
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr "Не показывать снова"
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr "Закрыть"
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr "На этом устройстве"
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with the "
+"GNU Taler WebExtension now or click the link if your WebExtension have the "
+"\"Inject Taler support\" option enabled."
+msgstr ""
+"Если вы используете веб-браузер на рабочем столе, вы можете получить доступ "
+"к своему кошельку с помощью расширения браузера GNU Taler прямо сейчас или "
+"нажать на ссылку если в вашем расширении браузера включена опция «Встронная "
+"поддержка Taler»."
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr "Старт"
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr "На мобильном телефоне"
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr "Отсканируйте QR-код с помощью мобильного устройства."
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr "Операция уже идет"
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, c-format
+msgid "Complete or cancel the operation in"
+msgstr "Завершите или отмените операцию в"
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr "этой странице"
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr "недействительно"
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr "Сервер ответил с недопустимым URI вывода"
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr "URI вывода: %1$s"
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr "Операция отклонена из-за нехватки средств."
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr "Продолжить"
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr "Подготовьте свой кошелёк"
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on this "
+"site."
+msgstr ""
+"После использования кошелька вам нужно будет подтвердить или отменить "
+"операцию на этом сайте."
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr "Вам нужен кошелёк Taler"
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr "Если у вас его еще нет, вы можете следовать инструкциям на"
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr "Отправить деньги"
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr "на кошелёк %1$s"
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+"Выводите цифровые деньги на свой мобильный кошелёк или расширение для "
+"браузера"
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr "операция готова"
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr "на другой банковский счет"
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+"Сделайте банковский перевод на счет с известным номером банковского счета."
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr "Подробности перевода"
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr "Это демо-банк"
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"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 %1$s."
+msgstr ""
+"В этой части демонстрации показано как будет работать банк поддерживающий "
+"Taler напрямую. Помимо использования собственного банковского счёта, вы "
+"также можете просмотреть историю транзакций некоторых %1$s."
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid "This part of the demo shows how a bank that supports Taler directly would work."
+msgstr ""
+"В этой части демонстрации показано как будет работать банк поддерживающий "
+"Taler напрямую."
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr "Ожидание операции удаления счёта"
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr "Ожидание операции обновления счёта"
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr "Ожидание операции обновления пароля"
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr "Ожидание операции транзакции"
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr "Ожидание операции вывода средств"
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr "Ожидание операции обналички"
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr "Завершить или отменить операцию можно в"
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr "Внутренняя ошибка, пожалуйста, сообщите."
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr "Настройки"
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr "Добро пожаловать, %1$s"
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr "Сделать банковский перевод"
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr "Счета"
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr "Список всех бизнес-счетов в банке."
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr "Создать учётную запись"
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr "Название"
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr "Баланс"
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr "Действия"
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr "неизвестно"
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr "изменить пароль"
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr "выплаты"
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr "удалить"
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr "Обналичка не реализована"
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr "Выберите раздел"
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr "Последний час"
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr "Последний день"
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr "Последний месяц"
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr "Последний год"
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr "Прошлый год"
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr "Объем торгов на %1$s по сравнению с %2$s"
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr "Внесения"
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr "Выплата"
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr "Отплата"
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr "Выплата"
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr "скачать статистику в формате CSV"
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr "Уменьшилось на"
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr "Увеличение на"
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr "Скачивать статистику банка"
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr "Включить часовую метрику"
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr "Включить дневную метрику"
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr "Включить месячную метрику"
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr "Включить годовую метрику"
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr "Включить заголовок таблицы"
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr "Добавить предыдущую метрику для сравнения"
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr "Сбой при первой ошибке"
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr "Скачивать"
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr "скачивание... %1$s"
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr "Скачивание завершено"
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr "Нажмите здесь, чтобы сохранить файл на своем компьютере"
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr "История публичных счетов"
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr "В настоящее время банк не принимает новые регистрации!"
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr "Отсутствует имя"
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr "Используйте только буквы и цифры и начинайте со строчной буквы"
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr "Пароли не совпадают"
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr "Сервер ответил что телефон или электронной почта недействительны."
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr "Регистрация отключена, так как в банке закончился бонусный кредит."
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr "Недостаточно разрешений для создания этого счёта."
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr "Этот идентификатор счёта уже занят."
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr "Это имя пользователя уже используется."
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+"Это имя пользователя не может быть использовано, так как оно зарезервировано."
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr "Только администратор может установить лимит задолженности."
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr "Нет информации о выбранном канале аутентификации."
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr "Канал аутентификации не поддерживается."
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+"Только администратор может создавать учетные записи со второй "
+"аутентификацией."
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr "Регистрация счёта"
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr "Повторите Пароль"
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr "Создать случайного временного пользователя"
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr "Если в этом устройстве установлен кошелёк Taler"
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"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"
+msgstr ""
+"Вы увидите подробности операции в своем кошельке, включая комиссию (если "
+"применимо). Если у вас его еще нет, вы можете установить его следуя "
+"инструкциям на"
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr "Снять средства"
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr "Или если у вас есть кошелёк в другом устройстве"
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr "Отсканируйте QR-код ниже чтобы начать вывод средств."
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr "Операция прервана"
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+"Банковский перевод на счет оператора Обменника Taler был прерван, ваш баланс "
+"не пострадал."
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr "Теперь вы можете закрыть эту страницу или перейти на страницу счёта."
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr "Готово"
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr "Операция отменена"
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid "The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+"Операция помечена как «выбранная», но какой-то шаг в выводе средств не удался"
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr "Счёт выбран, но идентификатор вывода средств не найден."
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+"Есть идентификатор вывода средств, но счёт не был выбран или выбранный счёт "
+"недействителен."
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected account "
+"is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr "Удаление счёта"
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr "Обновление счёта"
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr "Перевод"
+
+#: src/pages/SolveChallengePage.tsx:232
+#, c-format
+msgid "Withdrawal"
+msgstr "Вывод"
+
+#: src/pages/SolveChallengePage.tsx:248
+#, c-format
+msgid "Confirm the operation"
+msgstr "Подтвердить операцию"
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr "Введите код подтверждения"
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr "Подтвердить"
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr "Отправить ещё раз"
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr "Отправить код"
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr "Сведения об операции"
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr "Детали подтверждения"
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr "Время отправления"
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr "На телефон"
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr "На email"
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr "URI вывода недействителен"
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr "Последние обналички"
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr "Создано"
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr "Всего дебет"
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr "Итого кредит"
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr "Подробности"
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr "Удалить"
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr "Учетные данные"
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr "Выплаты"
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr "Не удается создать выплату"
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr "Конфигурация банка не поддерживает операции выплаты."
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr "должна быть выше из-за комиссий"
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr "общая сумма перевода в назначенее будет равна нулю"
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr "Выплата создана"
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid "Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+"Обнаружен дубликат запроса, проверьте, успешно ли выполнена операция, или "
+"повторите попытку."
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr "Неправильно применен курс конвертации"
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr "На счете недостаточно средств"
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr "Выплаты не поддерживаются"
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr "Отсутствующий URI вылат в профиле"
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+"Не удалось отправить сообщение с подтверждением, повторите попытку позже или "
+"обратитесь к администратору."
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr "Обменный курс"
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr "Комиссия"
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr "На счёт"
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr "Нет счёта для выплат"
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr "Перед тем, как сделать выплату, вам необходимо заполнить свой профиль"
+
+#: src/pages/business/CreateCashout.tsx:440
+#, c-format
+msgid "Amount to send"
+msgstr "Сумма к отправке"
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr "Сумма к получению"
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr "Общая стоимость"
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr "Остаток баланса"
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr "Комиссия до"
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr "Общий сумма перевода выплаты"
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr "Канал вывода средств недоступен"
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to confirm "
+"the operation"
+msgstr ""
+"Перед тем, как сделать кэшаут, серверу необходимо предоставить второй канал "
+"для подтверждения операции"
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr "Двухфакторная аутентификация"
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr "Email"
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+"Добавьте адрес электронной почты в свой профиль, чтобы включить эту опцию"
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr "SMS"
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr "Добавьте номер телефона в свой профиль, чтобы включить эту опцию"
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr "Выплата для аккаунта %1$s"
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr "у него нет шаблона номера IBAN"
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr "У него нет шаблона электронного письма"
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr "должен начинаться с +"
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr "Номер телефона не может иметь ничего, кроме цифр"
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr "Идентификация счета в банке"
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr "имя владельца счёта"
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr "Внутренний IBAN"
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr "Если пусто, будет присвоен случайный номер счета"
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr "Идентификация счета для банковского перевода"
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr "Телефон"
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr "IBAN выплаты"
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr "номер счета, на который будут отправлены деньги при выводе средств"
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr "Максимальная задолженность"
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr "Какую сумму пользователь может перевести после нулевого баланса"
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr "Это Обменник Taler?"
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr "Этот сервер не поддерживает двухфакторную аутентификацию."
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr "Включите двухфакторную аутентификацию"
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr "Используя email"
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr "Используя SMS"
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr "Является ли этот счёт общедоступным?"
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr "Баланс публичных счётов находится в открытом доступе"
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr "Счёт обновлён"
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr "Недостаточно прав на изменение счёта"
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr "Имя пользователя не найдено"
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid "You can't change the legal name, please contact the your account administrator."
+msgstr ""
+"Вы не можете изменить официальное имя, обратитесь к администратору вашей "
+"учетной записи."
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid "You can't change the debt limit, please contact the your account administrator."
+msgstr ""
+"Вы не можете изменить лимит задолженности, обратитесь к администратору "
+"аккаунта."
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+"Вы не можете изменить адрес для вывода средств, пожалуйста, свяжитесь с "
+"администратором вашего аккаунта."
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr "Счет \"%1$s\""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr "Изменение реквизитов"
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr "Обновить"
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr "пароль не совпадает"
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr "Пароль изменен"
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr "Обновить пароль"
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr "Новый пароль"
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr "Введите его ещё раз"
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr "повторите этот же пароль"
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr "Текущий пароль"
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr "Изменить"
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on the "
+"next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr "Новый бизнес счёт"
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr "Создать"
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make sure "
+"that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr "Удаление счёта \"%1$s\""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr "Проверка"
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr "Подробности обналичивания"
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr "Дебетировано"
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr "Кредитировано"
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr "Добро пожаловать в %1$s!"
diff --git a/packages/bank-ui/src/i18n/strings.ts b/packages/bank-ui/src/i18n/strings.ts
new file mode 100644
index 000000000..f55e5efbc
--- /dev/null
+++ b/packages/bank-ui/src/i18n/strings.ts
@@ -0,0 +1,2295 @@
+/*
+ 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 interface StringsType {
+ // X-Domain or 'messages'
+ domain: string;
+ lang: string;
+ completeness: number;
+ plural_forms: string;
+ locale_data: {
+ messages: Record<string, unknown>;
+ };
+}
+export const strings: Record<string, StringsType> = {};
+
+strings["it"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ },
+ "Operation failed, please report": ["Registrazione"],
+ "Request timeout": [""],
+ "Request throttled": [""],
+ "Malformed response": [""],
+ "Network error": [""],
+ "Unexpected request error": [""],
+ "Unexpected error": [""],
+ "IBAN numbers usually have more that 4 digits": [""],
+ "IBAN numbers usually have less that 34 digits": [""],
+ "IBAN country code not found": [""],
+ "IBAN number is not valid, checksum is wrong": [""],
+ "Max withdrawal amount": ["Questo ritiro è stato annullato!"],
+ "Show withdrawal confirmation": ["Questo ritiro è stato annullato!"],
+ "Show demo description": [""],
+ "Show install wallet first": [""],
+ "Use fast withdrawal form": ["Ritira contante"],
+ "Show debug info": [""],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [""],
+ "The operation id is invalid.": [""],
+ "The operation was not found.": ["Lista conti pubblici non trovata."],
+ "If you have a Taler wallet installed in this device": [""],
+ "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":
+ [""],
+ "this page": [""],
+ Withdraw: ["Prelevare"],
+ "Or if you have the wallet in another device": [""],
+ "Scan the QR below to start the withdrawal.": ["Chiudi il ritiro Taler"],
+ required: [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "not valid": [""],
+ "should be greater than 0": [""],
+ "balance is not enough": [""],
+ "does not follow the pattern": [""],
+ 'only "IBAN" target are supported': [""],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ "",
+ ],
+ "the amount is not valid": [""],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [""],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [""],
+ "Not enough permission to complete the operation.": [
+ "La banca sta creando l'operazione...",
+ ],
+ 'The destination account "%1$s" was not found.': [
+ "Lista conti pubblici non trovata.",
+ ],
+ "The origin and the destination of the transfer can't be the same.": [""],
+ "Your balance is not enough.": [""],
+ 'The origin account "%1$s" was not found.': [
+ "Lista conti pubblici non trovata.",
+ ],
+ "Using a form": [""],
+ "Import payto:// URI": [""],
+ Recipient: [""],
+ "IBAN of the recipient's account": [""],
+ "Transfer subject": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ subject: ["Soggetto"],
+ "some text to identify the transfer": [""],
+ Amount: ["Importo"],
+ "amount to transfer": ["Somma da ritirare"],
+ "payto URI:": [""],
+ "uniform resource identifier of the target account": [""],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [""],
+ Cancel: [""],
+ Send: [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ 'Wrong credentials for "%1$s"': ["Credenziali invalide."],
+ "Account not found": [""],
+ Username: [""],
+ "username of the account": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ Password: [""],
+ "password of the account": ["Storico dei conti pubblici"],
+ Check: [""],
+ "Log in": [""],
+ Register: ["Registrati"],
+ "Wire transfer completed!": ["Bonifico"],
+ "The withdrawal has been aborted previously and can't be confirmed": [""],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [""],
+ "Your balance is not enough for the operation.": [""],
+ "Confirm the withdrawal operation": ["Conferma il ritiro"],
+ "Wire transfer details": ["Bonifico"],
+ "Taler Exchange operator's account": [""],
+ "Taler Exchange operator's name": [""],
+ Transfer: [""],
+ "Authentication required": [""],
+ "This operation was created with other username": [""],
+ "Operation aborted": [""],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [""],
+ "You can close this page now or continue to the account page.": [""],
+ Continue: [""],
+ "Withdrawal confirmed": ["Questo ritiro è stato annullato!"],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [""],
+ Done: [""],
+ "Operation canceled": [""],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [""],
+ "The account is selected but no withdrawal identification found.": [""],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [""],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [""],
+ "Operation not found": [""],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [""],
+ "Cotinue to dashboard": [""],
+ "The Withdrawal URI is not valid": ["Questo ritiro è stato annullato!"],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [""],
+ "Internal error, please report.": ["Registrazione"],
+ Preferences: [""],
+ "Welcome, %1$s": [""],
+ "Latest transactions": ["Ultime transazioni:"],
+ Date: ["Data"],
+ Counterpart: ["Controparte"],
+ Subject: ["Soggetto"],
+ sent: [""],
+ received: [""],
+ "invalid value": [""],
+ to: [""],
+ from: [""],
+ "First page": [""],
+ Next: [""],
+ "History of public accounts": ["Storico dei conti pubblici"],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Missing name": ["indirizzo Payto"],
+ "Use letters and numbers only, and start with a lowercase letter": [""],
+ "Passwords don't match": [""],
+ "Server replied with invalid phone or email.": [""],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "",
+ ],
+ "No enough permission to create that account.": [""],
+ "That account id is already taken.": [""],
+ "That username is already taken.": [""],
+ "That username can't be used because is reserved.": [""],
+ "Only admin is allow to set debt limit.": [""],
+ "No information for the selected authentication channel.": [""],
+ "Authentication channel is not supported.": [""],
+ "Only admin can create accounts with second factor authentication.": [""],
+ "Account registration": [""],
+ "Repeat password": [""],
+ Name: [""],
+ "Create a random temporary user": [""],
+ "Make a wire transfer": ["Chiudi il bonifico"],
+ "Wire transfer created!": ["Bonifico"],
+ Accounts: ["Importo"],
+ "A list of all business account in the bank.": [""],
+ "Create account": [""],
+ Balance: [""],
+ Actions: [""],
+ unknown: [""],
+ "change password": [""],
+ remove: [""],
+ "Select a section": [""],
+ "Last hour": [""],
+ "Last day": [""],
+ "Last month": [""],
+ "Last year": [""],
+ "Last Year": [""],
+ "Trading volume on %1$s compared to %2$s": [""],
+ Cashin: [""],
+ Cashout: [""],
+ Payin: [""],
+ Payout: [""],
+ "download stats as CSV": [""],
+ "Descreased by": [""],
+ "Increased by": [""],
+ "Unable to create a cashout": [""],
+ "The bank configuration does not support cashout operations.": [""],
+ invalid: [""],
+ "need to be higher due to fees": [""],
+ "the total transfer at destination will be zero": [""],
+ "Cashout created": [""],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [""],
+ "The conversion rate was incorrectly applied": [""],
+ "The account does not have sufficient funds": [""],
+ "Cashouts are not supported": [""],
+ "Missing cashout URI in the profile": [""],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [""],
+ "Convertion rate": [""],
+ Fee: [""],
+ "To account": [""],
+ "No cashout account": [""],
+ "Before doing a cashout you need to complete your profile": [""],
+ "Amount to send": ["Somma da ritirare"],
+ "Amount to receive": ["Somma da ritirare"],
+ "Total cost": [""],
+ "Balance left": [""],
+ "Before fee": [""],
+ "Total cashout transfer": [""],
+ "No cashout channel available": [""],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [""],
+ "Second factor authentication": [""],
+ Email: [""],
+ "add a email in your profile to enable this option": [""],
+ SMS: [""],
+ "add a phone number in your profile to enable this option": [""],
+ Details: [""],
+ Delete: [""],
+ Credentials: ["Credenziali invalide."],
+ Cashouts: [""],
+ "it doesnt have the pattern of an IBAN number": [""],
+ "it doesnt have the pattern of an email": [""],
+ "should start with +": [""],
+ "phone number can't have other than numbers": [""],
+ "account identification in the bank": [""],
+ "name of the person owner the account": [""],
+ "Internal IBAN": [""],
+ "if empty a random account number will be assigned": [""],
+ "account identification for bank transfer": [""],
+ Phone: [""],
+ "Cashout IBAN": [""],
+ "account number where the money is going to be sent when doing cashouts":
+ [""],
+ "Max debt": [""],
+ "how much is user able to transfer after zero balance": [""],
+ "Is this a Taler Exchange?": [""],
+ "This server doesn't support second factor authentication.": [""],
+ "Enable second factor authentication": [""],
+ "Using email": [""],
+ "Using SMS": [""],
+ "Is this account public?": [""],
+ "public accounts have their balance publicly accesible": [""],
+ "Account updated": [""],
+ "The rights to change the account are not sufficient": [""],
+ "The username was not found": [""],
+ "You can't change the legal name, please contact the your account administrator.":
+ [""],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [""],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [""],
+ "You can't change the contact data, please contact the your account administrator.":
+ [""],
+ 'Account "%1$s"': [""],
+ "Change details": [""],
+ Update: [""],
+ "password doesn't match": [""],
+ "Password changed": [""],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [""],
+ "Your current password doesn't match, can't change to a new password.": [
+ "",
+ ],
+ "Update password": [""],
+ "New password": [""],
+ "Type it again": [""],
+ "repeat the same password": [""],
+ "Current password": [""],
+ "your current password, for security": [""],
+ Change: [""],
+ "Can't delete the account": [""],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [""],
+ "Account removed": [""],
+ "No enough permission to delete the account.": [""],
+ "The username was not found.": [""],
+ "Can't delete a reserved username.": [""],
+ "Can't delete an account with balance different than zero.": [""],
+ "name doesn't match": [""],
+ "You are going to remove the account": [""],
+ "This step can't be undone.": [""],
+ 'Deleting account "%1$s"': [""],
+ Verification: [""],
+ "enter the account name that is going to be deleted": [""],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [""],
+ "Server replied that phone or email is invalid": [""],
+ "The rights to perform the operation are not sufficient": [""],
+ "Account username is already taken": [""],
+ "Account id is already taken": [""],
+ "Bank ran out of bonus credit.": [""],
+ "Account username can't be used because is reserved": [""],
+ "Can't create accounts": [""],
+ "Only system admin can create accounts.": [""],
+ "New business account": [""],
+ Create: [""],
+ "Cashout not supported.": [""],
+ "Account not found.": ["Lista conti pubblici non trovata."],
+ "Latest cashouts": ["Ultime transazioni:"],
+ Created: [""],
+ Confirmed: ["Conferma"],
+ "Total debit": [""],
+ "Total credit": [""],
+ Status: [""],
+ never: [""],
+ "Cashout for account %1$s": [""],
+ "This cashout not found. Maybe already aborted.": [""],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "",
+ ],
+ "Cashout was already confimed.": [""],
+ "Cashout operation is not supported.": [""],
+ "The cashout operation is already aborted.": [""],
+ "Missing destination account.": [""],
+ "Too many failed attempts.": [""],
+ "The code for this cashout is invalid.": [""],
+ "Cashout detail": [""],
+ Debited: [""],
+ Credited: [""],
+ "Enter the confirmation code": [""],
+ Abort: ["Annulla"],
+ Confirm: ["Conferma"],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [""],
+ "The operation was rejected due to insufficient funds.": [""],
+ "Do not show this again": [""],
+ Close: [""],
+ "On this device": [""],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [""],
+ Start: [""],
+ "On a mobile phone": [""],
+ "Scan the QR code with your mobile device.": [
+ "Usa questo codice QR per ritirare contante nel tuo wallet:",
+ ],
+ "There is an operation already": [""],
+ "Complete or cancel the operation in": ["Conferma il ritiro"],
+ "Server responded with an invalid withdraw URI": [""],
+ "Withdraw URI: %1$s": ["Prelevare"],
+ "The operation was rejected due to insufficient funds": [""],
+ "Prepare your wallet": [""],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [""],
+ "You need a GNU Taler Wallet": ["Ritira contante nel portafoglio Taler"],
+ "If you don't have one yet you can follow the instruction in": [""],
+ "Send money": [""],
+ "to a %1$s wallet": [""],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "",
+ ],
+ "operation ready": [""],
+ "to another bank account": [
+ "Trasferisci fondi a un altro conto di questa banca:",
+ ],
+ "Make a wire transfer to an account with known bank account number.": [
+ "",
+ ],
+ "Transfer details": ["Effettua un bonifico"],
+ "This is a demo bank": [""],
+ "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 %1$s.":
+ [""],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [""],
+ "Pending account delete operation": [""],
+ "Pending account update operation": [""],
+ "Pending password update operation": [""],
+ "Pending transaction operation": [""],
+ "Pending withdrawal operation": [""],
+ "Pending cashout operation": [""],
+ "You can complete or cancel the operation in": [""],
+ "Download bank stats": [""],
+ "Include hour metric": [""],
+ "Include day metric": [""],
+ "Include month metric": [""],
+ "Include year metric": [""],
+ "Include table header": [""],
+ "Add previous metric for compare": [""],
+ "Fail on first error": [""],
+ Download: [""],
+ "downloading... %1$s": [""],
+ "Download completed": [""],
+ "click here to save the file in your computer": [""],
+ "Challenge not found.": [""],
+ "This user is not authorized to complete this challenge.": [""],
+ "Too many attemps, try another code.": [""],
+ "The confirmation code is wrong, try again.": [""],
+ "The operation expired.": [""],
+ "The operation failed.": ["Questo ritiro è stato annullato!"],
+ "The operation needs another confirmation to complete.": [""],
+ "Account delete": [""],
+ "Account update": [""],
+ "Password update": [""],
+ "Wire transfer": ["Bonifico"],
+ Withdrawal: ["Prelevare"],
+ "Confirm the operation": ["Conferma il ritiro"],
+ "Send again": [""],
+ "Send code": [""],
+ "Operation details": [""],
+ "Challenge details": [""],
+ "Sent at": [""],
+ "To phone": [""],
+ "To email": [""],
+ "Welcome to %1$s!": [""],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "it",
+ completeness: 14,
+};
+
+strings["fr"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n > 1;",
+ lang: "fr",
+ },
+ "Operation failed, please report": [""],
+ "Request timeout": [""],
+ "Request throttled": [""],
+ "Malformed response": [""],
+ "Network error": [""],
+ "Unexpected request error": [""],
+ "Unexpected error": [""],
+ "IBAN numbers usually have more that 4 digits": [""],
+ "IBAN numbers usually have less that 34 digits": [""],
+ "IBAN country code not found": [""],
+ "IBAN number is not valid, checksum is wrong": [""],
+ "Max withdrawal amount": [""],
+ "Show withdrawal confirmation": [""],
+ "Show demo description": [""],
+ "Show install wallet first": [""],
+ "Use fast withdrawal form": [""],
+ "Show debug info": [""],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [""],
+ "The operation id is invalid.": [""],
+ "The operation was not found.": [""],
+ "If you have a Taler wallet installed in this device": [""],
+ "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":
+ [""],
+ "this page": [""],
+ Withdraw: [""],
+ "Or if you have the wallet in another device": [""],
+ "Scan the QR below to start the withdrawal.": [""],
+ required: [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "not valid": [""],
+ "should be greater than 0": [""],
+ "balance is not enough": [""],
+ "does not follow the pattern": [""],
+ 'only "IBAN" target are supported': [""],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ "",
+ ],
+ "the amount is not valid": [""],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [""],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [""],
+ "Not enough permission to complete the operation.": [""],
+ 'The destination account "%1$s" was not found.': [""],
+ "The origin and the destination of the transfer can't be the same.": [""],
+ "Your balance is not enough.": [""],
+ 'The origin account "%1$s" was not found.': [""],
+ "Using a form": [""],
+ "Import payto:// URI": [""],
+ Recipient: [""],
+ "IBAN of the recipient's account": [""],
+ "Transfer subject": [""],
+ subject: [""],
+ "some text to identify the transfer": [""],
+ Amount: [""],
+ "amount to transfer": [""],
+ "payto URI:": [""],
+ "uniform resource identifier of the target account": [""],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [""],
+ Cancel: [""],
+ Send: [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ 'Wrong credentials for "%1$s"': [""],
+ "Account not found": [""],
+ Username: [""],
+ "username of the account": [""],
+ Password: [""],
+ "password of the account": [""],
+ Check: [""],
+ "Log in": [""],
+ Register: [""],
+ "Wire transfer completed!": [""],
+ "The withdrawal has been aborted previously and can't be confirmed": [""],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [""],
+ "Your balance is not enough for the operation.": [""],
+ "Confirm the withdrawal operation": [""],
+ "Wire transfer details": [""],
+ "Taler Exchange operator's account": [""],
+ "Taler Exchange operator's name": [""],
+ Transfer: [""],
+ "Authentication required": [""],
+ "This operation was created with other username": [""],
+ "Operation aborted": [""],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [""],
+ "You can close this page now or continue to the account page.": [""],
+ Continue: [""],
+ "Withdrawal confirmed": [""],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [""],
+ Done: [""],
+ "Operation canceled": [""],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [""],
+ "The account is selected but no withdrawal identification found.": [""],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [""],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [""],
+ "Operation not found": [""],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [""],
+ "Cotinue to dashboard": [""],
+ "The Withdrawal URI is not valid": [""],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [""],
+ "Internal error, please report.": [""],
+ Preferences: [""],
+ "Welcome, %1$s": [""],
+ "Latest transactions": [""],
+ Date: [""],
+ Counterpart: [""],
+ Subject: [""],
+ sent: [""],
+ received: [""],
+ "invalid value": [""],
+ to: [""],
+ from: [""],
+ "First page": [""],
+ Next: [""],
+ "History of public accounts": [""],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Missing name": [""],
+ "Use letters and numbers only, and start with a lowercase letter": [""],
+ "Passwords don't match": [""],
+ "Server replied with invalid phone or email.": [""],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "",
+ ],
+ "No enough permission to create that account.": [""],
+ "That account id is already taken.": [""],
+ "That username is already taken.": [""],
+ "That username can't be used because is reserved.": [""],
+ "Only admin is allow to set debt limit.": [""],
+ "No information for the selected authentication channel.": [""],
+ "Authentication channel is not supported.": [""],
+ "Only admin can create accounts with second factor authentication.": [""],
+ "Account registration": [""],
+ "Repeat password": [""],
+ Name: [""],
+ "Create a random temporary user": [""],
+ "Make a wire transfer": [""],
+ "Wire transfer created!": [""],
+ Accounts: [""],
+ "A list of all business account in the bank.": [""],
+ "Create account": [""],
+ Balance: [""],
+ Actions: [""],
+ unknown: [""],
+ "change password": [""],
+ remove: [""],
+ "Select a section": [""],
+ "Last hour": [""],
+ "Last day": [""],
+ "Last month": [""],
+ "Last year": [""],
+ "Last Year": [""],
+ "Trading volume on %1$s compared to %2$s": [""],
+ Cashin: [""],
+ Cashout: [""],
+ Payin: [""],
+ Payout: [""],
+ "download stats as CSV": [""],
+ "Descreased by": [""],
+ "Increased by": [""],
+ "Unable to create a cashout": [""],
+ "The bank configuration does not support cashout operations.": [""],
+ invalid: [""],
+ "need to be higher due to fees": [""],
+ "the total transfer at destination will be zero": [""],
+ "Cashout created": [""],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [""],
+ "The conversion rate was incorrectly applied": [""],
+ "The account does not have sufficient funds": [""],
+ "Cashouts are not supported": [""],
+ "Missing cashout URI in the profile": [""],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [""],
+ "Convertion rate": [""],
+ Fee: [""],
+ "To account": [""],
+ "No cashout account": [""],
+ "Before doing a cashout you need to complete your profile": [""],
+ "Amount to send": [""],
+ "Amount to receive": [""],
+ "Total cost": [""],
+ "Balance left": [""],
+ "Before fee": [""],
+ "Total cashout transfer": [""],
+ "No cashout channel available": [""],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [""],
+ "Second factor authentication": [""],
+ Email: [""],
+ "add a email in your profile to enable this option": [""],
+ SMS: [""],
+ "add a phone number in your profile to enable this option": [""],
+ Details: [""],
+ Delete: [""],
+ Credentials: [""],
+ Cashouts: [""],
+ "it doesnt have the pattern of an IBAN number": [""],
+ "it doesnt have the pattern of an email": [""],
+ "should start with +": [""],
+ "phone number can't have other than numbers": [""],
+ "account identification in the bank": [""],
+ "name of the person owner the account": [""],
+ "Internal IBAN": [""],
+ "if empty a random account number will be assigned": [""],
+ "account identification for bank transfer": [""],
+ Phone: [""],
+ "Cashout IBAN": [""],
+ "account number where the money is going to be sent when doing cashouts":
+ [""],
+ "Max debt": [""],
+ "how much is user able to transfer after zero balance": [""],
+ "Is this a Taler Exchange?": [""],
+ "This server doesn't support second factor authentication.": [""],
+ "Enable second factor authentication": [""],
+ "Using email": [""],
+ "Using SMS": [""],
+ "Is this account public?": [""],
+ "public accounts have their balance publicly accesible": [""],
+ "Account updated": [""],
+ "The rights to change the account are not sufficient": [""],
+ "The username was not found": [""],
+ "You can't change the legal name, please contact the your account administrator.":
+ [""],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [""],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [""],
+ "You can't change the contact data, please contact the your account administrator.":
+ [""],
+ 'Account "%1$s"': [""],
+ "Change details": [""],
+ Update: [""],
+ "password doesn't match": [""],
+ "Password changed": [""],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [""],
+ "Your current password doesn't match, can't change to a new password.": [
+ "",
+ ],
+ "Update password": [""],
+ "New password": [""],
+ "Type it again": [""],
+ "repeat the same password": [""],
+ "Current password": [""],
+ "your current password, for security": [""],
+ Change: [""],
+ "Can't delete the account": [""],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [""],
+ "Account removed": [""],
+ "No enough permission to delete the account.": [""],
+ "The username was not found.": [""],
+ "Can't delete a reserved username.": [""],
+ "Can't delete an account with balance different than zero.": [""],
+ "name doesn't match": [""],
+ "You are going to remove the account": [""],
+ "This step can't be undone.": [""],
+ 'Deleting account "%1$s"': [""],
+ Verification: [""],
+ "enter the account name that is going to be deleted": [""],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [""],
+ "Server replied that phone or email is invalid": [""],
+ "The rights to perform the operation are not sufficient": [""],
+ "Account username is already taken": [""],
+ "Account id is already taken": [""],
+ "Bank ran out of bonus credit.": [""],
+ "Account username can't be used because is reserved": [""],
+ "Can't create accounts": [""],
+ "Only system admin can create accounts.": [""],
+ "New business account": [""],
+ Create: [""],
+ "Cashout not supported.": [""],
+ "Account not found.": [""],
+ "Latest cashouts": [""],
+ Created: [""],
+ Confirmed: [""],
+ "Total debit": [""],
+ "Total credit": [""],
+ Status: [""],
+ never: [""],
+ "Cashout for account %1$s": [""],
+ "This cashout not found. Maybe already aborted.": [""],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "",
+ ],
+ "Cashout was already confimed.": [""],
+ "Cashout operation is not supported.": [""],
+ "The cashout operation is already aborted.": [""],
+ "Missing destination account.": [""],
+ "Too many failed attempts.": [""],
+ "The code for this cashout is invalid.": [""],
+ "Cashout detail": [""],
+ Debited: [""],
+ Credited: [""],
+ "Enter the confirmation code": [""],
+ Abort: [""],
+ Confirm: [""],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [""],
+ "The operation was rejected due to insufficient funds.": [""],
+ "Do not show this again": [""],
+ Close: [""],
+ "On this device": [""],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [""],
+ Start: [""],
+ "On a mobile phone": [""],
+ "Scan the QR code with your mobile device.": [""],
+ "There is an operation already": [""],
+ "Complete or cancel the operation in": [""],
+ "Server responded with an invalid withdraw URI": [""],
+ "Withdraw URI: %1$s": [""],
+ "The operation was rejected due to insufficient funds": [""],
+ "Prepare your wallet": [""],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [""],
+ "You need a GNU Taler Wallet": [""],
+ "If you don't have one yet you can follow the instruction in": [""],
+ "Send money": [""],
+ "to a %1$s wallet": [""],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "",
+ ],
+ "operation ready": [""],
+ "to another bank account": [""],
+ "Make a wire transfer to an account with known bank account number.": [
+ "",
+ ],
+ "Transfer details": [""],
+ "This is a demo bank": [""],
+ "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 %1$s.":
+ [""],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [""],
+ "Pending account delete operation": [""],
+ "Pending account update operation": [""],
+ "Pending password update operation": [""],
+ "Pending transaction operation": [""],
+ "Pending withdrawal operation": [""],
+ "Pending cashout operation": [""],
+ "You can complete or cancel the operation in": [""],
+ "Download bank stats": [""],
+ "Include hour metric": [""],
+ "Include day metric": [""],
+ "Include month metric": [""],
+ "Include year metric": [""],
+ "Include table header": [""],
+ "Add previous metric for compare": [""],
+ "Fail on first error": [""],
+ Download: [""],
+ "downloading... %1$s": [""],
+ "Download completed": [""],
+ "click here to save the file in your computer": [""],
+ "Challenge not found.": [""],
+ "This user is not authorized to complete this challenge.": [""],
+ "Too many attemps, try another code.": [""],
+ "The confirmation code is wrong, try again.": [""],
+ "The operation expired.": [""],
+ "The operation failed.": [""],
+ "The operation needs another confirmation to complete.": [""],
+ "Account delete": [""],
+ "Account update": [""],
+ "Password update": [""],
+ "Wire transfer": [""],
+ Withdrawal: [""],
+ "Confirm the operation": [""],
+ "Send again": [""],
+ "Send code": [""],
+ "Operation details": [""],
+ "Challenge details": [""],
+ "Sent at": [""],
+ "To phone": [""],
+ "To email": [""],
+ "Welcome to %1$s!": [""],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n > 1;",
+ lang: "fr",
+ completeness: 0,
+};
+
+strings["es"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ },
+ "Operation failed, please report": [
+ "La operaicón falló, por favor reportelo",
+ ],
+ "Request timeout": ["La petición al servidor agoto su tiempo"],
+ "Request throttled": ["La petición al servidor interrumpida"],
+ "Malformed response": ["Respuesta malformada"],
+ "Network error": ["Error de conexión"],
+ "Unexpected request error": ["Error de pedido inesperado"],
+ "Unexpected error": ["Error inesperado"],
+ "IBAN numbers usually have more that 4 digits": [
+ "Los números IBAN usualmente tienen mas de 4 digitos",
+ ],
+ "IBAN numbers usually have less that 34 digits": [
+ "Los números IBAN usualmente tienen menos de 34 digitos",
+ ],
+ "IBAN country code not found": ["Código de pais de IBAN no encontrado"],
+ "IBAN number is not valid, checksum is wrong": [
+ "El número IBAN no es válido, falló la verificación",
+ ],
+ "Max withdrawal amount": ["Monto máximo de extracción"],
+ "Show withdrawal confirmation": ["Mostrar confirmación de extracción"],
+ "Show demo description": ["Mostrar descripción de demo"],
+ "Show install wallet first": ["Mostrar instalar la billetera primero"],
+ "Use fast withdrawal form": ["Usar formulario de extracción rápida"],
+ "Show debug info": ["Mostrar información de depuración"],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [
+ "La operación en la reserva ya ha sido confirmada previamente y no puede ser abortada",
+ ],
+ "The operation id is invalid.": ["El id de operación es invalido."],
+ "The operation was not found.": ["La operación no se encontró."],
+ "If you have a Taler wallet installed in this device": [
+ "Si tienes una billetera Taler instalada en este dispositivo",
+ ],
+ "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":
+ [
+ "Veras los detalles de la operación en tu billetera incluyendo comisiones (si aplicán). Si todavía no tienes una puedes instalarla siguiendo las instrucciones en",
+ ],
+ "this page": ["esta página"],
+ Withdraw: ["Retirar"],
+ "Or if you have the wallet in another device": [
+ "O si tienes la billetera en otro dispositivo",
+ ],
+ "Scan the QR below to start the withdrawal.": [
+ "Escanea el QR debajo para comenzar la extracción.",
+ ],
+ required: ["requerido"],
+ "IBAN should have just uppercased letters and numbers": [
+ "IBAN debería tener letras mayúsculas y números",
+ ],
+ "not valid": ["no válido"],
+ "should be greater than 0": ["Debería ser mas grande que 0"],
+ "balance is not enough": ["el saldo no es suficiente"],
+ "does not follow the pattern": ["no tiene un patrón valido"],
+ 'only "IBAN" target are supported': [
+ 'solo cuentas "IBAN" son soportadas',
+ ],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ 'usa el parámetro "amount" para indicar el monto a ser transferido',
+ ],
+ "the amount is not valid": ["el monto no es válido"],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [
+ 'usa el parámetro "message" para indicar un texto de referencia en la transferencia',
+ ],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [
+ "El pedido era inválido o el URI payto:// usado tiene características inaceptables.",
+ ],
+ "Not enough permission to complete the operation.": [
+ "Sin permisos suficientes para completar la operación.",
+ ],
+ 'The destination account "%1$s" was not found.': [
+ 'La cuenta de destino "%1$s" no fue encontrada.',
+ ],
+ "The origin and the destination of the transfer can't be the same.": [
+ "El origen y destino de la transferencia no puede ser la misma.",
+ ],
+ "Your balance is not enough.": ["El saldo no es suficiente."],
+ 'The origin account "%1$s" was not found.': [
+ 'La cuenta origen "%1$s" no fue encontrada.',
+ ],
+ "Using a form": ["Usando un formulario"],
+ "Import payto:// URI": ["Importando un URI payto://"],
+ Recipient: ["Destinatario"],
+ "IBAN of the recipient's account": [
+ "Numero IBAN de la cuenta destinataria",
+ ],
+ "Transfer subject": ["Asunto de transferencia"],
+ subject: ["asunto"],
+ "some text to identify the transfer": [
+ "algún texto para identificar la transferencia",
+ ],
+ Amount: ["Monto"],
+ "amount to transfer": ["monto a transferir"],
+ "payto URI:": ["payto URI:"],
+ "uniform resource identifier of the target account": [
+ "identificador de recurso uniforme de la cuenta destino",
+ ],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [
+ "payto://iban/[iban-destinatario]?message=[asunto]&amount=[%1$s:X.Y]",
+ ],
+ Cancel: ["Cancelar"],
+ Send: ["Envíar"],
+ "Missing username": ["Falta nombre de usuario"],
+ "Missing password": ["Falta contraseña"],
+ 'Wrong credentials for "%1$s"': ['Credenciales incorrectas para "%1$s"'],
+ "Account not found": ["Cuenta no encontrada"],
+ Username: ["Nombre de usuario"],
+ "username of the account": ["nombre de usuario de la cuenta"],
+ Password: ["Contraseña"],
+ "password of the account": ["contraseña de la cuenta"],
+ Check: ["Verificar"],
+ "Log in": ["Acceso"],
+ Register: ["Registrarse"],
+ "Wire transfer completed!": ["Transferencia bancaria completada!"],
+ "The withdrawal has been aborted previously and can't be confirmed": [
+ "La extracción fue abortada anteriormente y no puede ser confirmada",
+ ],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [
+ "La operación de extracción no puede ser confirmada antes de que una billetera acepte la transaccion.",
+ ],
+ "Your balance is not enough for the operation.": [
+ "El saldo no es suficiente para la operación.",
+ ],
+ "Confirm the withdrawal operation": [
+ "Confirme la operación de extracción",
+ ],
+ "Wire transfer details": ["Detalle de transferencia bancaria"],
+ "Taler Exchange operator's account": [
+ "Cuenta del operador del Taler Exchange",
+ ],
+ "Taler Exchange operator's name": [
+ "Nombre del operador del Taler Exchange",
+ ],
+ Transfer: ["Transferencia"],
+ "Authentication required": ["Autenticación requerida"],
+ "This operation was created with other username": [
+ "Esta operación fue creada con otro usuario",
+ ],
+ "Operation aborted": ["Operación abortada"],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [
+ "La transferencia bancaria a la cuenta del operador del Taler Exchange fue abortada, su saldo no fue afectado.",
+ ],
+ "You can close this page now or continue to the account page.": [
+ "Ya puedes cerrar esta pagina or continuar a la página de estado de cuenta.",
+ ],
+ Continue: ["Continuar"],
+ "Withdrawal confirmed": ["La extracción fue confirmada"],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [
+ "La transferencia bancaria al operador Taler fue iniciada. Pronto recibirás el monto pedido en tu billetera Taler.",
+ ],
+ Done: ["Listo"],
+ "Operation canceled": ["Operación cancelada"],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [
+ "La operación está marcada como 'seleccionada' pero algunos pasos en la extracción fallaron",
+ ],
+ "The account is selected but no withdrawal identification found.": [
+ "La cuenta está seleccionada pero no se encontró el identificador de extracción.",
+ ],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [
+ "Hay un identificador de extracción pero la cuenta no ha sido seleccionada o la selccionada es inválida.",
+ ],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [
+ "No hay un identificador de extracción y ninguna cuenta a sido seleccionada o la seleccionada es inválida.",
+ ],
+ "Operation not found": ["Operación no encontrada"],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [
+ "Esta operación no es conocida por el servidor. El identificador de operación es incorrecto o el server borró la información de la operación antes de llegar hasta aquí.",
+ ],
+ "Cotinue to dashboard": ["Continuar al panel"],
+ "The Withdrawal URI is not valid": ["El URI de estracción no es válido"],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [
+ 'El servidor de bank no esta spoportado. Version soportada "%1$s", version del server "%2$s"',
+ ],
+ "Internal error, please report.": [
+ "Error interno, por favor reporte el error.",
+ ],
+ Preferences: ["Preferencias"],
+ "Welcome, %1$s": ["Bienvenido/a, %1$s"],
+ "Latest transactions": ["Últimas transacciones"],
+ Date: ["Fecha"],
+ Counterpart: ["Contraparte"],
+ Subject: ["Asunto"],
+ sent: ["enviado"],
+ received: ["recibido"],
+ "invalid value": ["valor inválido"],
+ to: ["hacia"],
+ from: ["desde"],
+ "First page": ["Primera página"],
+ Next: ["Siguiente"],
+ "History of public accounts": ["Historial de cuentas públicas"],
+ "Currently, the bank is not accepting new registrations!": [
+ "Actualmente, el banco no está aceptado nuevos registros!",
+ ],
+ "Missing name": ["Falta nombre"],
+ "Use letters and numbers only, and start with a lowercase letter": [
+ "Solo use letras y números, y comience con una letra minúscula",
+ ],
+ "Passwords don't match": ["La contraseña no coincide"],
+ "Server replied with invalid phone or email.": [
+ "El servidor repondio con teléfono o dirección de correo inválido.",
+ ],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "El registro está deshabilitado porque el banco se quedó sin crédito bonus.",
+ ],
+ "No enough permission to create that account.": [
+ "Sin permisos suficientes para crear esa cuenta.",
+ ],
+ "That account id is already taken.": [
+ "El identificador de cuenta ya está tomado.",
+ ],
+ "That username is already taken.": [
+ "El nombre de usuario ya está tomado.",
+ ],
+ "That username can't be used because is reserved.": [
+ "El nombre de usuario no puede ser usado porque esta reservado.",
+ ],
+ "Only admin is allow to set debt limit.": [
+ "Solo el administrador tiene permitido cambiar el límite de deuda.",
+ ],
+ "No information for the selected authentication channel.": [
+ "No hay información para el canal de autenticación seleccionado.",
+ ],
+ "Authentication channel is not supported.": [
+ "Canal de autenticación no esta soportado.",
+ ],
+ "Only admin can create accounts with second factor authentication.": [
+ "Solo el administrador puede crear cuentas con el segundo factor de autenticación.",
+ ],
+ "Account registration": ["Registro de cuenta"],
+ "Repeat password": ["Repita la contraseña"],
+ Name: ["Nombre"],
+ "Create a random temporary user": ["Crear un usuario aleatorio temporal"],
+ "Make a wire transfer": ["Hacer una transferencia bancaria"],
+ "Wire transfer created!": ["Transferencia bancaria creada!"],
+ Accounts: ["Cuentas"],
+ "A list of all business account in the bank.": [
+ "Una lista de todas las cuentas en el banco.",
+ ],
+ "Create account": ["Crear cuenta"],
+ Balance: ["Saldo"],
+ Actions: ["Acciones"],
+ unknown: ["desconocido"],
+ "change password": ["cambiar contraseña"],
+ remove: ["elimiar"],
+ "Select a section": ["Seleccione una sección"],
+ "Last hour": ["Última hora"],
+ "Last day": ["Último día"],
+ "Last month": ["Último mes"],
+ "Last year": ["Último año"],
+ "Last Year": ["Último Año"],
+ "Trading volume on %1$s compared to %2$s": [
+ "Vólumen de comercio en %1$s comparado con %2$s",
+ ],
+ Cashin: ["Ingreso"],
+ Cashout: ["Egreso"],
+ Payin: ["Envios de dinero"],
+ Payout: ["Recibos de dinero"],
+ "download stats as CSV": ["descargar estadísticas en CSV"],
+ "Descreased by": ["Descendiente por"],
+ "Increased by": ["Ascendente por"],
+ "Unable to create a cashout": ["Imposible crear un egreso"],
+ "The bank configuration does not support cashout operations.": [
+ "La configuración del banco no soporta operaciones de egreso.",
+ ],
+ invalid: ["inválido"],
+ "need to be higher due to fees": [
+ "necesita ser mayor debido a las comisiones",
+ ],
+ "the total transfer at destination will be zero": [
+ "el total de la transferencia en destino será cero",
+ ],
+ "Cashout created": ["Egreso creado"],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [
+ "Se detectó una petición duplicada, verifique si la operación tuvo éxito o intente otra vez.",
+ ],
+ "The conversion rate was incorrectly applied": [
+ "La tasa de conversión se aplicó incorrectamente",
+ ],
+ "The account does not have sufficient funds": [
+ "La cuenta no tiene fondos suficientes",
+ ],
+ "Cashouts are not supported": ["Egresos no están soportados"],
+ "Missing cashout URI in the profile": [
+ "Falta dirección de egreso en el perfíl",
+ ],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [
+ "El envío del mensaje de confirmación falló, intente mas tarde o contacte al administrador.",
+ ],
+ "Convertion rate": ["Tasa de conversión"],
+ Fee: ["Comisión"],
+ "To account": ["Hacia cuenta"],
+ "No cashout account": ["No hay cuenta de egreso"],
+ "Before doing a cashout you need to complete your profile": [
+ "Antes de hacer un egreso necesita completar su perfíl",
+ ],
+ "Amount to send": ["Monto a enviar"],
+ "Amount to receive": ["Monto a recibir"],
+ "Total cost": ["Costo total"],
+ "Balance left": ["Saldo remanente"],
+ "Before fee": ["Antes de comisión"],
+ "Total cashout transfer": ["Total de egreso"],
+ "No cashout channel available": ["No hay canal de egreso disponible"],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [
+ "Antes de hacer un egreso el servidor necesita proveer un segundo canal para confirmar la operación",
+ ],
+ "Second factor authentication": ["Segundo factor de autenticación"],
+ Email: ["Correo eletrónico"],
+ "add a email in your profile to enable this option": [
+ "agrege un correo en su perfíl para habilitar esta opción",
+ ],
+ SMS: ["SMS"],
+ "add a phone number in your profile to enable this option": [
+ "agregue un número de teléfono para habilitar esta opción",
+ ],
+ Details: ["Detalles"],
+ Delete: ["Borrar"],
+ Credentials: ["Credenciales"],
+ Cashouts: ["Egresos"],
+ "it doesnt have the pattern of an IBAN number": [
+ "no tiene el patrón de un número IBAN",
+ ],
+ "it doesnt have the pattern of an email": [
+ "no tiene el patrón de un correo electrónico",
+ ],
+ "should start with +": ["debería comenzar con un +"],
+ "phone number can't have other than numbers": [
+ "número de teléfono no puede tener otra cosa que numeros",
+ ],
+ "account identification in the bank": [
+ "identificador de cuenta en el banco",
+ ],
+ "name of the person owner the account": [
+ "nombre de la persona dueña de la cuenta",
+ ],
+ "Internal IBAN": ["IBAN interno"],
+ "if empty a random account number will be assigned": [
+ "si está vacío un número de cuenta aleatorio será asignado",
+ ],
+ "account identification for bank transfer": [
+ "identificador de cuenta para transferencia bancaria",
+ ],
+ Phone: ["Teléfono"],
+ "Cashout IBAN": ["IBAN de egreso"],
+ "account number where the money is going to be sent when doing cashouts":
+ [
+ "numero de cuenta donde el dinero será enviado cuando se ejecuten egresos",
+ ],
+ "Max debt": ["Máxima deuda"],
+ "how much is user able to transfer after zero balance": [
+ "cuanto tiene habilitado a transferir despues de un saldo en cero",
+ ],
+ "Is this a Taler Exchange?": ["Es un Taler Exchange?"],
+ "This server doesn't support second factor authentication.": [
+ "Este servidor no tiene soporte para segundo factor de autenticación.",
+ ],
+ "Enable second factor authentication": [
+ "Hábilitar segundo factor de autenticación",
+ ],
+ "Using email": ["Usando correo eletrónico"],
+ "Using SMS": ["Usando SMS"],
+ "Is this account public?": ["Es una cuenta pública?"],
+ "public accounts have their balance publicly accesible": [
+ "las cuentas públicas tienen su saldo accesible al público",
+ ],
+ "Account updated": ["Cuenta actualizada"],
+ "The rights to change the account are not sufficient": [
+ "Los permisos para cambiar la cuenta no son suficientes",
+ ],
+ "The username was not found": ["El nombre de usaurio no se encontró"],
+ "You can't change the legal name, please contact the your account administrator.":
+ [
+ "No puede cambiar el nombre legal, por favor contacte el administrador de la cuenta.",
+ ],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [
+ "No puede cambiar el límite de deuda, por favor contacte el administrador de la cuenta.",
+ ],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [
+ "No puede cambiar la dirección de egreso, por favor contacte al administrador de la cuenta.",
+ ],
+ "You can't change the contact data, please contact the your account administrator.":
+ [
+ "No puede cambiar los datos de contacto, por favor contacte al administrador de la cuenta.",
+ ],
+ 'Account "%1$s"': ['Cuenta "%1$s"'],
+ "Change details": ["Cambiar detalles"],
+ Update: ["Actualizar"],
+ "password doesn't match": ["la contraseña no coincide"],
+ "Password changed": ["La contraseña cambió"],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "No está autorizado a cambiar el password, quizá la sesión es invalida.",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [
+ "Se necesita el password viejo para cambiar la contraseña. Si no lo tiene contacte a su administrador.",
+ ],
+ "Your current password doesn't match, can't change to a new password.": [
+ "Su actual contraseña no coincide, no puede cambiar a una nueva contraseña.",
+ ],
+ "Update password": ["Actualizar contraseña"],
+ "New password": ["Nueva contraseña"],
+ "Type it again": ["Escribalo otra vez"],
+ "repeat the same password": ["repita la misma contraseña"],
+ "Current password": ["Contraseña actual"],
+ "your current password, for security": [
+ "su actual contraseña, por seguridad",
+ ],
+ Change: ["Cambiar"],
+ "Can't delete the account": ["No se puede eliminar la cuenta"],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [
+ "La cuenta no puede ser eliminada mientras tiene saldo. Primero aseguresé que el dueño haga un egreso completo.",
+ ],
+ "Account removed": ["Cuenta eliminada"],
+ "No enough permission to delete the account.": [
+ "No tiene permisos suficientes para eliminar la cuenta.",
+ ],
+ "The username was not found.": ["El nombr ede usuario no se encontró."],
+ "Can't delete a reserved username.": [
+ "No se puede eliminar un nombre de usuario reservado.",
+ ],
+ "Can't delete an account with balance different than zero.": [
+ "No se puede eliminar una cuenta con saldo diferente a cero.",
+ ],
+ "name doesn't match": ["el nombre no coincide"],
+ "You are going to remove the account": ["Está por eliminar la cuenta"],
+ "This step can't be undone.": ["Este paso no puede ser deshecho."],
+ 'Deleting account "%1$s"': ['Borrando cuenta "%1$s"'],
+ Verification: ["Verificación"],
+ "enter the account name that is going to be deleted": [
+ "ingrese el nombre de cuenta que será eliminado",
+ ],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [
+ 'Cuenta creada con contraseña "%1$s". El usuario debe cambiar la contraseña en el siguiente ingreso.',
+ ],
+ "Server replied that phone or email is invalid": [
+ "El servidor respondió que el teléfono o correo eletrónico es invalido",
+ ],
+ "The rights to perform the operation are not sufficient": [
+ "Los permisos para ejecutar la operación no son suficientes",
+ ],
+ "Account username is already taken": [
+ "El nombre del usuario ya está tomado",
+ ],
+ "Account id is already taken": ["El id de cuenta ya está tomado"],
+ "Bank ran out of bonus credit.": [
+ "El banco no tiene mas crédito de bonus.",
+ ],
+ "Account username can't be used because is reserved": [
+ "El nombre de usuario de la cuenta no puede userse porque está reservado",
+ ],
+ "Can't create accounts": ["No puede crear cuentas"],
+ "Only system admin can create accounts.": [
+ "Solo los administradores del sistema pueden crear cuentas.",
+ ],
+ "New business account": ["Nueva cuenta"],
+ Create: ["Crear"],
+ "Cashout not supported.": ["Egreso no soportado."],
+ "Account not found.": ["Cuenta no encontrada."],
+ "Latest cashouts": ["Últimos egresos"],
+ Created: ["Creado"],
+ Confirmed: ["Confirmado"],
+ "Total debit": ["Débito total"],
+ "Total credit": ["Crédito total"],
+ Status: ["Estado"],
+ never: ["nunca"],
+ "Cashout for account %1$s": ["Egreso para cuenta %1$s"],
+ "This cashout not found. Maybe already aborted.": [
+ "Este egreso no se encontró. Quizá fue abortado.",
+ ],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "Egreso no econtrado. También puede significar que ya ha sido abortado.",
+ ],
+ "Cashout was already confimed.": ["Egreso ya fue confirmado."],
+ "Cashout operation is not supported.": [
+ "Operación de egreso no soportada.",
+ ],
+ "The cashout operation is already aborted.": [
+ "La operación de egreso ya está abortada.",
+ ],
+ "Missing destination account.": ["Falta cuenta destino."],
+ "Too many failed attempts.": ["Demasiados intentos fallidos."],
+ "The code for this cashout is invalid.": [
+ "El código para este egreso es invalido.",
+ ],
+ "Cashout detail": ["Detalles de egreso"],
+ Debited: ["Debitado"],
+ Credited: ["Acreditado"],
+ "Enter the confirmation code": ["Ingresar el código de confirmación"],
+ Abort: ["Abortar"],
+ Confirm: ["Confirmar"],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [
+ "No autorizado para hacer la operación, quizá la sesión haya expirado or cambió la contraseña.",
+ ],
+ "The operation was rejected due to insufficient funds.": [
+ "La operación fue rechazada debido a saldo insuficiente.",
+ ],
+ "Do not show this again": ["No mostrar otra vez"],
+ Close: ["Cerrar"],
+ "On this device": ["En este dispositivo"],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [
+ 'Si esta usando un explorador web de escritorio deberías acceder ahora a tu billletera con la GNU Taler WebExtension o hacer click en el link si tu extensión tiene la configuración "Inyectar soporte para Taler" habilitada.',
+ ],
+ Start: ["Comenzar"],
+ "On a mobile phone": ["En un dispotivo mobile"],
+ "Scan the QR code with your mobile device.": [
+ "Escanear el código QR con tu dispotivo móvil.",
+ ],
+ "There is an operation already": ["Ya hay una operación"],
+ "Complete or cancel the operation in": [
+ "Completa o cancela la operación en",
+ ],
+ "Server responded with an invalid withdraw URI": [
+ "El servidor respondió con una URI de extracción inválida",
+ ],
+ "Withdraw URI: %1$s": ["URI de extracción: %1$s"],
+ "The operation was rejected due to insufficient funds": [
+ "La operación fue rechazada debido a fundos insuficientes",
+ ],
+ "Prepare your wallet": ["Prepare su billetera"],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [
+ "Despues de usar tu billetera necesitarás confirmar o cancelar la operación en este sitio.",
+ ],
+ "You need a GNU Taler Wallet": ["Necesitas una GNU Taler Wallet"],
+ "If you don't have one yet you can follow the instruction in": [
+ "Si no tienes una todavía puedes seguir las instrucciones en",
+ ],
+ "Send money": ["Enviar dinero"],
+ "to a %1$s wallet": ["a una billetera %1$s"],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "Extraer dinero digital a tu billetera móvil o extesión web",
+ ],
+ "operation ready": ["operación lista"],
+ "to another bank account": ["a otra cuenta bancaria"],
+ "Make a wire transfer to an account with known bank account number.": [
+ "Hacer una transferencia bancaria a una cuenta con un número de cuenta conocido.",
+ ],
+ "Transfer details": ["Detalles de transferencia"],
+ "This is a demo bank": ["Este es un banco de demostración"],
+ "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 %1$s.":
+ [
+ "Esta parte de la demostración muestra cómo funciona un banco que soporta Taler directamente. Además de usar tu propia cuenta de banco, también podrás ver el historial de transacciones de algunas %1$s.",
+ ],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [
+ "Esta parte de la demostración muetra como un banco que soporta Taler directamente funcionaría.",
+ ],
+ "Pending account delete operation": [
+ "Operación pendiente de eliminación de cuenta",
+ ],
+ "Pending account update operation": [
+ "Operación pendiente de actualización de cuenta",
+ ],
+ "Pending password update operation": [
+ "Operación pendiente de actualización de password",
+ ],
+ "Pending transaction operation": ["Operación pendiente de transacción"],
+ "Pending withdrawal operation": ["Operación pendiente de extracción"],
+ "Pending cashout operation": ["Operación pendiente de egreso"],
+ "You can complete or cancel the operation in": [
+ "Puedes completar o cancelar la operación en",
+ ],
+ "Download bank stats": ["Descargar estadísticas del banco"],
+ "Include hour metric": ["Incluir métrica horaria"],
+ "Include day metric": ["Incluir métrica diaria"],
+ "Include month metric": ["Incluir métrica mensual"],
+ "Include year metric": ["Incluir métrica anual"],
+ "Include table header": ["Incluir encabezado de tabla"],
+ "Add previous metric for compare": [
+ "Agregar métrica previa para comparar",
+ ],
+ "Fail on first error": ["Fallar en el primer error"],
+ Download: ["Descargar"],
+ "downloading... %1$s": ["descargando... %1$s"],
+ "Download completed": ["Descarga completada"],
+ "click here to save the file in your computer": [
+ "click aquí para guardar el archivo en su computadora",
+ ],
+ "Challenge not found.": ["Desafío no encontrado."],
+ "This user is not authorized to complete this challenge.": [
+ "Este usuario no está autorizado para completar este desafío.",
+ ],
+ "Too many attemps, try another code.": [
+ "Demasiados intentos, intente otro código.",
+ ],
+ "The confirmation code is wrong, try again.": [
+ "El código de confirmación es erroneo, intente otra vez.",
+ ],
+ "The operation expired.": ["La operación expiró."],
+ "The operation failed.": ["La operación falló."],
+ "The operation needs another confirmation to complete.": [
+ "La operación necesita otra confirmación para completar.",
+ ],
+ "Account delete": ["Eliminación de cuenta"],
+ "Account update": ["Actualización de cuenta"],
+ "Password update": ["Actualización de contraseña"],
+ "Wire transfer": ["Transferencia bancaria"],
+ Withdrawal: ["Extracción"],
+ "Confirm the operation": ["Confirmar la operación"],
+ "Send again": ["Enviar otra vez"],
+ "Send code": ["Enviar código"],
+ "Operation details": ["Detalles de operación"],
+ "Challenge details": ["Detalles del desafío"],
+ "Sent at": ["Enviado a"],
+ "To phone": ["Al teléfono"],
+ "To email": ["Al email"],
+ "Welcome to %1$s!": ["Bienvenido a %1$s!"],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "es",
+ completeness: 100,
+};
+
+strings["en"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ },
+ "Operation failed, please report": [""],
+ "Request timeout": [""],
+ "Request throttled": [""],
+ "Malformed response": [""],
+ "Network error": [""],
+ "Unexpected request error": [""],
+ "Unexpected error": [""],
+ "IBAN numbers usually have more that 4 digits": [""],
+ "IBAN numbers usually have less that 34 digits": [""],
+ "IBAN country code not found": [""],
+ "IBAN number is not valid, checksum is wrong": [""],
+ "Max withdrawal amount": [""],
+ "Show withdrawal confirmation": [""],
+ "Show demo description": [""],
+ "Show install wallet first": [""],
+ "Use fast withdrawal form": [""],
+ "Show debug info": [""],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [""],
+ "The operation id is invalid.": [""],
+ "The operation was not found.": [""],
+ "If you have a Taler wallet installed in this device": [""],
+ "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":
+ [""],
+ "this page": [""],
+ Withdraw: [""],
+ "Or if you have the wallet in another device": [""],
+ "Scan the QR below to start the withdrawal.": [""],
+ required: [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "not valid": [""],
+ "should be greater than 0": [""],
+ "balance is not enough": [""],
+ "does not follow the pattern": [""],
+ 'only "IBAN" target are supported': [""],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ "",
+ ],
+ "the amount is not valid": [""],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [""],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [""],
+ "Not enough permission to complete the operation.": [""],
+ 'The destination account "%1$s" was not found.': [""],
+ "The origin and the destination of the transfer can't be the same.": [""],
+ "Your balance is not enough.": [""],
+ 'The origin account "%1$s" was not found.': [""],
+ "Using a form": [""],
+ "Import payto:// URI": [""],
+ Recipient: [""],
+ "IBAN of the recipient's account": [""],
+ "Transfer subject": [""],
+ subject: [""],
+ "some text to identify the transfer": [""],
+ Amount: [""],
+ "amount to transfer": [""],
+ "payto URI:": [""],
+ "uniform resource identifier of the target account": [""],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [""],
+ Cancel: [""],
+ Send: [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ 'Wrong credentials for "%1$s"': [""],
+ "Account not found": [""],
+ Username: [""],
+ "username of the account": [""],
+ Password: [""],
+ "password of the account": [""],
+ Check: [""],
+ "Log in": [""],
+ Register: [""],
+ "Wire transfer completed!": [""],
+ "The withdrawal has been aborted previously and can't be confirmed": [""],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [""],
+ "Your balance is not enough for the operation.": [""],
+ "Confirm the withdrawal operation": [""],
+ "Wire transfer details": [""],
+ "Taler Exchange operator's account": [""],
+ "Taler Exchange operator's name": [""],
+ Transfer: [""],
+ "Authentication required": [""],
+ "This operation was created with other username": [""],
+ "Operation aborted": [""],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [""],
+ "You can close this page now or continue to the account page.": [""],
+ Continue: [""],
+ "Withdrawal confirmed": [""],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [""],
+ Done: [""],
+ "Operation canceled": [""],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [""],
+ "The account is selected but no withdrawal identification found.": [""],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [""],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [""],
+ "Operation not found": [""],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [""],
+ "Cotinue to dashboard": [""],
+ "The Withdrawal URI is not valid": [""],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [""],
+ "Internal error, please report.": [""],
+ Preferences: [""],
+ "Welcome, %1$s": [""],
+ "Latest transactions": [""],
+ Date: [""],
+ Counterpart: [""],
+ Subject: [""],
+ sent: [""],
+ received: [""],
+ "invalid value": [""],
+ to: [""],
+ from: [""],
+ "First page": [""],
+ Next: [""],
+ "History of public accounts": [""],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Missing name": [""],
+ "Use letters and numbers only, and start with a lowercase letter": [""],
+ "Passwords don't match": [""],
+ "Server replied with invalid phone or email.": [""],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "",
+ ],
+ "No enough permission to create that account.": [""],
+ "That account id is already taken.": [""],
+ "That username is already taken.": [""],
+ "That username can't be used because is reserved.": [""],
+ "Only admin is allow to set debt limit.": [""],
+ "No information for the selected authentication channel.": [""],
+ "Authentication channel is not supported.": [""],
+ "Only admin can create accounts with second factor authentication.": [""],
+ "Account registration": [""],
+ "Repeat password": [""],
+ Name: [""],
+ "Create a random temporary user": [""],
+ "Make a wire transfer": [""],
+ "Wire transfer created!": [""],
+ Accounts: [""],
+ "A list of all business account in the bank.": [""],
+ "Create account": [""],
+ Balance: [""],
+ Actions: [""],
+ unknown: [""],
+ "change password": [""],
+ remove: [""],
+ "Select a section": [""],
+ "Last hour": [""],
+ "Last day": [""],
+ "Last month": [""],
+ "Last year": [""],
+ "Last Year": [""],
+ "Trading volume on %1$s compared to %2$s": [""],
+ Cashin: [""],
+ Cashout: [""],
+ Payin: [""],
+ Payout: [""],
+ "download stats as CSV": [""],
+ "Descreased by": [""],
+ "Increased by": [""],
+ "Unable to create a cashout": [""],
+ "The bank configuration does not support cashout operations.": [""],
+ invalid: [""],
+ "need to be higher due to fees": [""],
+ "the total transfer at destination will be zero": [""],
+ "Cashout created": [""],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [""],
+ "The conversion rate was incorrectly applied": [""],
+ "The account does not have sufficient funds": [""],
+ "Cashouts are not supported": [""],
+ "Missing cashout URI in the profile": [""],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [""],
+ "Convertion rate": [""],
+ Fee: [""],
+ "To account": [""],
+ "No cashout account": [""],
+ "Before doing a cashout you need to complete your profile": [""],
+ "Amount to send": [""],
+ "Amount to receive": [""],
+ "Total cost": [""],
+ "Balance left": [""],
+ "Before fee": [""],
+ "Total cashout transfer": [""],
+ "No cashout channel available": [""],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [""],
+ "Second factor authentication": [""],
+ Email: [""],
+ "add a email in your profile to enable this option": [""],
+ SMS: [""],
+ "add a phone number in your profile to enable this option": [""],
+ Details: [""],
+ Delete: [""],
+ Credentials: [""],
+ Cashouts: [""],
+ "it doesnt have the pattern of an IBAN number": [""],
+ "it doesnt have the pattern of an email": [""],
+ "should start with +": [""],
+ "phone number can't have other than numbers": [""],
+ "account identification in the bank": [""],
+ "name of the person owner the account": [""],
+ "Internal IBAN": [""],
+ "if empty a random account number will be assigned": [""],
+ "account identification for bank transfer": [""],
+ Phone: [""],
+ "Cashout IBAN": [""],
+ "account number where the money is going to be sent when doing cashouts":
+ [""],
+ "Max debt": [""],
+ "how much is user able to transfer after zero balance": [""],
+ "Is this a Taler Exchange?": [""],
+ "This server doesn't support second factor authentication.": [""],
+ "Enable second factor authentication": [""],
+ "Using email": [""],
+ "Using SMS": [""],
+ "Is this account public?": [""],
+ "public accounts have their balance publicly accesible": [""],
+ "Account updated": [""],
+ "The rights to change the account are not sufficient": [""],
+ "The username was not found": [""],
+ "You can't change the legal name, please contact the your account administrator.":
+ [""],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [""],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [""],
+ "You can't change the contact data, please contact the your account administrator.":
+ [""],
+ 'Account "%1$s"': [""],
+ "Change details": [""],
+ Update: [""],
+ "password doesn't match": [""],
+ "Password changed": [""],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [""],
+ "Your current password doesn't match, can't change to a new password.": [
+ "",
+ ],
+ "Update password": [""],
+ "New password": [""],
+ "Type it again": [""],
+ "repeat the same password": [""],
+ "Current password": [""],
+ "your current password, for security": [""],
+ Change: [""],
+ "Can't delete the account": [""],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [""],
+ "Account removed": [""],
+ "No enough permission to delete the account.": [""],
+ "The username was not found.": [""],
+ "Can't delete a reserved username.": [""],
+ "Can't delete an account with balance different than zero.": [""],
+ "name doesn't match": [""],
+ "You are going to remove the account": [""],
+ "This step can't be undone.": [""],
+ 'Deleting account "%1$s"': [""],
+ Verification: [""],
+ "enter the account name that is going to be deleted": [""],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [""],
+ "Server replied that phone or email is invalid": [""],
+ "The rights to perform the operation are not sufficient": [""],
+ "Account username is already taken": [""],
+ "Account id is already taken": [""],
+ "Bank ran out of bonus credit.": [""],
+ "Account username can't be used because is reserved": [""],
+ "Can't create accounts": [""],
+ "Only system admin can create accounts.": [""],
+ "New business account": [""],
+ Create: [""],
+ "Cashout not supported.": [""],
+ "Account not found.": [""],
+ "Latest cashouts": [""],
+ Created: [""],
+ Confirmed: [""],
+ "Total debit": [""],
+ "Total credit": [""],
+ Status: [""],
+ never: [""],
+ "Cashout for account %1$s": [""],
+ "This cashout not found. Maybe already aborted.": [""],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "",
+ ],
+ "Cashout was already confimed.": [""],
+ "Cashout operation is not supported.": [""],
+ "The cashout operation is already aborted.": [""],
+ "Missing destination account.": [""],
+ "Too many failed attempts.": [""],
+ "The code for this cashout is invalid.": [""],
+ "Cashout detail": [""],
+ Debited: [""],
+ Credited: [""],
+ "Enter the confirmation code": [""],
+ Abort: [""],
+ Confirm: [""],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [""],
+ "The operation was rejected due to insufficient funds.": [""],
+ "Do not show this again": [""],
+ Close: [""],
+ "On this device": [""],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [""],
+ Start: [""],
+ "On a mobile phone": [""],
+ "Scan the QR code with your mobile device.": [""],
+ "There is an operation already": [""],
+ "Complete or cancel the operation in": [""],
+ "Server responded with an invalid withdraw URI": [""],
+ "Withdraw URI: %1$s": [""],
+ "The operation was rejected due to insufficient funds": [""],
+ "Prepare your wallet": [""],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [""],
+ "You need a GNU Taler Wallet": [""],
+ "If you don't have one yet you can follow the instruction in": [""],
+ "Send money": [""],
+ "to a %1$s wallet": [""],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "",
+ ],
+ "operation ready": [""],
+ "to another bank account": [""],
+ "Make a wire transfer to an account with known bank account number.": [
+ "",
+ ],
+ "Transfer details": [""],
+ "This is a demo bank": [""],
+ "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 %1$s.":
+ [""],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [""],
+ "Pending account delete operation": [""],
+ "Pending account update operation": [""],
+ "Pending password update operation": [""],
+ "Pending transaction operation": [""],
+ "Pending withdrawal operation": [""],
+ "Pending cashout operation": [""],
+ "You can complete or cancel the operation in": [""],
+ "Download bank stats": [""],
+ "Include hour metric": [""],
+ "Include day metric": [""],
+ "Include month metric": [""],
+ "Include year metric": [""],
+ "Include table header": [""],
+ "Add previous metric for compare": [""],
+ "Fail on first error": [""],
+ Download: [""],
+ "downloading... %1$s": [""],
+ "Download completed": [""],
+ "click here to save the file in your computer": [""],
+ "Challenge not found.": [""],
+ "This user is not authorized to complete this challenge.": [""],
+ "Too many attemps, try another code.": [""],
+ "The confirmation code is wrong, try again.": [""],
+ "The operation expired.": [""],
+ "The operation failed.": [""],
+ "The operation needs another confirmation to complete.": [""],
+ "Account delete": [""],
+ "Account update": [""],
+ "Password update": [""],
+ "Wire transfer": [""],
+ Withdrawal: [""],
+ "Confirm the operation": [""],
+ "Send again": [""],
+ "Send code": [""],
+ "Operation details": [""],
+ "Challenge details": [""],
+ "Sent at": [""],
+ "To phone": [""],
+ "To email": [""],
+ "Welcome to %1$s!": [""],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "en",
+ completeness: 100,
+};
+
+strings["de"] = {
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ },
+ "Operation failed, please report": [""],
+ "Request timeout": [""],
+ "Request throttled": [""],
+ "Malformed response": [""],
+ "Network error": [""],
+ "Unexpected request error": [""],
+ "Unexpected error": [""],
+ "IBAN numbers usually have more that 4 digits": [""],
+ "IBAN numbers usually have less that 34 digits": [""],
+ "IBAN country code not found": [""],
+ "IBAN number is not valid, checksum is wrong": [""],
+ "Max withdrawal amount": [""],
+ "Show withdrawal confirmation": [""],
+ "Show demo description": [""],
+ "Show install wallet first": [""],
+ "Use fast withdrawal form": [""],
+ "Show debug info": [""],
+ "The reserve operation has been confirmed previously and can't be aborted":
+ [""],
+ "The operation id is invalid.": [""],
+ "The operation was not found.": [""],
+ "If you have a Taler wallet installed in this device": [""],
+ "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":
+ [""],
+ "this page": [""],
+ Withdraw: [""],
+ "Or if you have the wallet in another device": [""],
+ "Scan the QR below to start the withdrawal.": [""],
+ required: [""],
+ "IBAN should have just uppercased letters and numbers": [""],
+ "not valid": [""],
+ "should be greater than 0": [""],
+ "balance is not enough": [""],
+ "does not follow the pattern": [""],
+ 'only "IBAN" target are supported': [""],
+ 'use the "amount" parameter to specify the amount to be transferred': [
+ "",
+ ],
+ "the amount is not valid": [""],
+ 'use the "message" parameter to specify a reference text for the transfer':
+ [""],
+ "The request was invalid or the payto://-URI used unacceptable features.":
+ [""],
+ "Not enough permission to complete the operation.": [""],
+ 'The destination account "%1$s" was not found.': [""],
+ "The origin and the destination of the transfer can't be the same.": [""],
+ "Your balance is not enough.": [""],
+ 'The origin account "%1$s" was not found.': [""],
+ "Using a form": [""],
+ "Import payto:// URI": [""],
+ Recipient: [""],
+ "IBAN of the recipient's account": [""],
+ "Transfer subject": [""],
+ subject: ["Verwendungszweck"],
+ "some text to identify the transfer": [""],
+ Amount: ["Betrag"],
+ "amount to transfer": ["Betrag"],
+ "payto URI:": [""],
+ "uniform resource identifier of the target account": [""],
+ "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]": [""],
+ Cancel: [""],
+ Send: [""],
+ "Missing username": [""],
+ "Missing password": [""],
+ 'Wrong credentials for "%1$s"': [""],
+ "Account not found": [""],
+ Username: [""],
+ "username of the account": [""],
+ Password: [""],
+ "password of the account": ["Buchungen auf öffentlich sichtbaren Konten"],
+ Check: [""],
+ "Log in": [""],
+ Register: [""],
+ "Wire transfer completed!": [""],
+ "The withdrawal has been aborted previously and can't be confirmed": [""],
+ "The withdrawal operation can't be confirmed before a wallet accepted the transaction.":
+ [""],
+ "Your balance is not enough for the operation.": [""],
+ "Confirm the withdrawal operation": ["Abhebung bestätigen"],
+ "Wire transfer details": [""],
+ "Taler Exchange operator's account": [""],
+ "Taler Exchange operator's name": [""],
+ Transfer: [""],
+ "Authentication required": [""],
+ "This operation was created with other username": [""],
+ "Operation aborted": [""],
+ "The wire transfer to the Taler Exchange operator's account was aborted, your balance was not affected.":
+ [""],
+ "You can close this page now or continue to the account page.": [""],
+ Continue: [""],
+ "Withdrawal confirmed": [""],
+ "The wire transfer to the Taler operator has been initiated. You will soon receive the requested amount in your Taler wallet.":
+ [""],
+ Done: [""],
+ "Operation canceled": [""],
+ "The operation is marked as 'selected' but some step in the withdrawal failed":
+ [""],
+ "The account is selected but no withdrawal identification found.": [""],
+ "There is a withdrawal identification but no account has been selected or the selected account is invalid.":
+ [""],
+ "No withdrawal ID found and no account has been selected or the selected account is invalid.":
+ [""],
+ "Operation not found": [""],
+ "This operation is not known by the server. The operation id is wrong or the server deleted the operation information before reaching here.":
+ [""],
+ "Cotinue to dashboard": [""],
+ "The Withdrawal URI is not valid": [""],
+ 'the bank backend is not supported. supported version "%1$s", server version "%2$s"':
+ [""],
+ "Internal error, please report.": [""],
+ Preferences: [""],
+ "Welcome, %1$s": [""],
+ "Latest transactions": [""],
+ Date: ["Datum"],
+ Counterpart: ["Empfänger"],
+ Subject: ["Verwendungszweck"],
+ sent: [""],
+ received: [""],
+ "invalid value": [""],
+ to: [""],
+ from: [""],
+ "First page": [""],
+ Next: [""],
+ "History of public accounts": [
+ "Buchungen auf öffentlich sichtbaren Konten",
+ ],
+ "Currently, the bank is not accepting new registrations!": [""],
+ "Missing name": [""],
+ "Use letters and numbers only, and start with a lowercase letter": [""],
+ "Passwords don't match": [""],
+ "Server replied with invalid phone or email.": [""],
+ "Registration is disabled because the bank ran out of bonus credit.": [
+ "",
+ ],
+ "No enough permission to create that account.": [""],
+ "That account id is already taken.": [""],
+ "That username is already taken.": [""],
+ "That username can't be used because is reserved.": [""],
+ "Only admin is allow to set debt limit.": [""],
+ "No information for the selected authentication channel.": [""],
+ "Authentication channel is not supported.": [""],
+ "Only admin can create accounts with second factor authentication.": [""],
+ "Account registration": [""],
+ "Repeat password": [""],
+ Name: [""],
+ "Create a random temporary user": [""],
+ "Make a wire transfer": [""],
+ "Wire transfer created!": [""],
+ Accounts: ["Betrag"],
+ "A list of all business account in the bank.": [""],
+ "Create account": [""],
+ Balance: [""],
+ Actions: [""],
+ unknown: [""],
+ "change password": [""],
+ remove: [""],
+ "Select a section": [""],
+ "Last hour": [""],
+ "Last day": [""],
+ "Last month": [""],
+ "Last year": [""],
+ "Last Year": [""],
+ "Trading volume on %1$s compared to %2$s": [""],
+ Cashin: [""],
+ Cashout: [""],
+ Payin: [""],
+ Payout: [""],
+ "download stats as CSV": [""],
+ "Descreased by": [""],
+ "Increased by": [""],
+ "Unable to create a cashout": [""],
+ "The bank configuration does not support cashout operations.": [""],
+ invalid: [""],
+ "need to be higher due to fees": [""],
+ "the total transfer at destination will be zero": [""],
+ "Cashout created": [""],
+ "Duplicated request detected, check if the operation succeded or try again.":
+ [""],
+ "The conversion rate was incorrectly applied": [""],
+ "The account does not have sufficient funds": [""],
+ "Cashouts are not supported": [""],
+ "Missing cashout URI in the profile": [""],
+ "Sending the confirmation message failed, retry later or contact the administrator.":
+ [""],
+ "Convertion rate": [""],
+ Fee: [""],
+ "To account": [""],
+ "No cashout account": [""],
+ "Before doing a cashout you need to complete your profile": [""],
+ "Amount to send": ["Betrag"],
+ "Amount to receive": [""],
+ "Total cost": [""],
+ "Balance left": [""],
+ "Before fee": [""],
+ "Total cashout transfer": [""],
+ "No cashout channel available": [""],
+ "Before doing a cashout the server need to provide an second channel to confirm the operation":
+ [""],
+ "Second factor authentication": [""],
+ Email: [""],
+ "add a email in your profile to enable this option": [""],
+ SMS: [""],
+ "add a phone number in your profile to enable this option": [""],
+ Details: [""],
+ Delete: [""],
+ Credentials: [""],
+ Cashouts: [""],
+ "it doesnt have the pattern of an IBAN number": [""],
+ "it doesnt have the pattern of an email": [""],
+ "should start with +": [""],
+ "phone number can't have other than numbers": [""],
+ "account identification in the bank": [""],
+ "name of the person owner the account": [""],
+ "Internal IBAN": [""],
+ "if empty a random account number will be assigned": [""],
+ "account identification for bank transfer": [""],
+ Phone: [""],
+ "Cashout IBAN": [""],
+ "account number where the money is going to be sent when doing cashouts":
+ [""],
+ "Max debt": [""],
+ "how much is user able to transfer after zero balance": [""],
+ "Is this a Taler Exchange?": [""],
+ "This server doesn't support second factor authentication.": [""],
+ "Enable second factor authentication": [""],
+ "Using email": [""],
+ "Using SMS": [""],
+ "Is this account public?": [""],
+ "public accounts have their balance publicly accesible": [""],
+ "Account updated": [""],
+ "The rights to change the account are not sufficient": [""],
+ "The username was not found": [""],
+ "You can't change the legal name, please contact the your account administrator.":
+ [""],
+ "You can't change the debt limit, please contact the your account administrator.":
+ [""],
+ "You can't change the cashout address, please contact the your account administrator.":
+ [""],
+ "You can't change the contact data, please contact the your account administrator.":
+ [""],
+ 'Account "%1$s"': [""],
+ "Change details": [""],
+ Update: [""],
+ "password doesn't match": [""],
+ "Password changed": [""],
+ "Not authorized to change the password, maybe the session is invalid.": [
+ "",
+ ],
+ "You need to provide the old password. If you don't have it contact your account administrator.":
+ [""],
+ "Your current password doesn't match, can't change to a new password.": [
+ "",
+ ],
+ "Update password": [""],
+ "New password": [""],
+ "Type it again": [""],
+ "repeat the same password": [""],
+ "Current password": [""],
+ "your current password, for security": [""],
+ Change: [""],
+ "Can't delete the account": [""],
+ "The account can't be delete while still holding some balance. First make sure that the owner make a complete cashout.":
+ [""],
+ "Account removed": [""],
+ "No enough permission to delete the account.": [""],
+ "The username was not found.": [""],
+ "Can't delete a reserved username.": [""],
+ "Can't delete an account with balance different than zero.": [""],
+ "name doesn't match": [""],
+ "You are going to remove the account": [""],
+ "This step can't be undone.": [""],
+ 'Deleting account "%1$s"': [""],
+ Verification: [""],
+ "enter the account name that is going to be deleted": [""],
+ 'Account created with password "%1$s". The user must change the password on the next login.':
+ [""],
+ "Server replied that phone or email is invalid": [""],
+ "The rights to perform the operation are not sufficient": [""],
+ "Account username is already taken": [""],
+ "Account id is already taken": [""],
+ "Bank ran out of bonus credit.": [""],
+ "Account username can't be used because is reserved": [""],
+ "Can't create accounts": [""],
+ "Only system admin can create accounts.": [""],
+ "New business account": [""],
+ Create: [""],
+ "Cashout not supported.": [""],
+ "Account not found.": [""],
+ "Latest cashouts": [""],
+ Created: [""],
+ Confirmed: ["Bestätigen"],
+ "Total debit": [""],
+ "Total credit": [""],
+ Status: [""],
+ never: [""],
+ "Cashout for account %1$s": [""],
+ "This cashout not found. Maybe already aborted.": [""],
+ "Cashout not found. It may be also mean that it was already aborted.": [
+ "",
+ ],
+ "Cashout was already confimed.": [""],
+ "Cashout operation is not supported.": [""],
+ "The cashout operation is already aborted.": [""],
+ "Missing destination account.": [""],
+ "Too many failed attempts.": [""],
+ "The code for this cashout is invalid.": [""],
+ "Cashout detail": [""],
+ Debited: [""],
+ Credited: [""],
+ "Enter the confirmation code": [""],
+ Abort: [""],
+ Confirm: ["Bestätigen"],
+ "Unauthorized to make the operation, maybe the session has expired or the password changed.":
+ [""],
+ "The operation was rejected due to insufficient funds.": [""],
+ "Do not show this again": [""],
+ Close: [""],
+ "On this device": [""],
+ 'If you are using a web browser on desktop you should access your wallet with the GNU Taler WebExtension now or click the link if your WebExtension have the "Inject Taler support" option enabled.':
+ [""],
+ Start: [""],
+ "On a mobile phone": [""],
+ "Scan the QR code with your mobile device.": [""],
+ "There is an operation already": [""],
+ "Complete or cancel the operation in": ["Abhebung bestätigen"],
+ "Server responded with an invalid withdraw URI": [""],
+ "Withdraw URI: %1$s": [""],
+ "The operation was rejected due to insufficient funds": [""],
+ "Prepare your wallet": [""],
+ "After using your wallet you will need to confirm or cancel the operation on this site.":
+ [""],
+ "You need a GNU Taler Wallet": [""],
+ "If you don't have one yet you can follow the instruction in": [""],
+ "Send money": [""],
+ "to a %1$s wallet": [""],
+ "Withdraw digital money into your mobile wallet or browser extension": [
+ "",
+ ],
+ "operation ready": [""],
+ "to another bank account": [""],
+ "Make a wire transfer to an account with known bank account number.": [
+ "",
+ ],
+ "Transfer details": [""],
+ "This is a demo bank": [""],
+ "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 %1$s.":
+ [""],
+ "This part of the demo shows how a bank that supports Taler directly would work.":
+ [""],
+ "Pending account delete operation": [""],
+ "Pending account update operation": [""],
+ "Pending password update operation": [""],
+ "Pending transaction operation": [""],
+ "Pending withdrawal operation": [""],
+ "Pending cashout operation": [""],
+ "You can complete or cancel the operation in": [""],
+ "Download bank stats": [""],
+ "Include hour metric": [""],
+ "Include day metric": [""],
+ "Include month metric": [""],
+ "Include year metric": [""],
+ "Include table header": [""],
+ "Add previous metric for compare": [""],
+ "Fail on first error": [""],
+ Download: [""],
+ "downloading... %1$s": [""],
+ "Download completed": [""],
+ "click here to save the file in your computer": [""],
+ "Challenge not found.": [""],
+ "This user is not authorized to complete this challenge.": [""],
+ "Too many attemps, try another code.": [""],
+ "The confirmation code is wrong, try again.": [""],
+ "The operation expired.": [""],
+ "The operation failed.": [""],
+ "The operation needs another confirmation to complete.": [""],
+ "Account delete": [""],
+ "Account update": [""],
+ "Password update": [""],
+ "Wire transfer": [""],
+ Withdrawal: ["Abhebung bestätigen"],
+ "Confirm the operation": ["Abhebung bestätigen"],
+ "Send again": [""],
+ "Send code": [""],
+ "Operation details": [""],
+ "Challenge details": [""],
+ "Sent at": [""],
+ "To phone": [""],
+ "To email": [""],
+ "Welcome to %1$s!": [""],
+ },
+ },
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=n != 1;",
+ lang: "de",
+ completeness: 4,
+};
diff --git a/packages/bank-ui/src/i18n/uk.po b/packages/bank-ui/src/i18n/uk.po
new file mode 100644
index 000000000..a8b41e32f
--- /dev/null
+++ b/packages/bank-ui/src/i18n/uk.po
@@ -0,0 +1,1743 @@
+# 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/>
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Bank\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: 2024-03-07 07:04+0000\n"
+"Last-Translator: Tim Vutor <flukes.ostrich0p@icloud.com>\n"
+"Language-Team: Ukrainian <https://weblate.taler.net/projects/gnu-taler/"
+"taler-bank-spa/uk/>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/utils.ts:137
+#, c-format, fuzzy
+msgid "Operation failed, please report"
+msgstr "Помилка операції, повідомте"
+
+#: src/utils.ts:156
+#, c-format
+msgid "Request timeout"
+msgstr "Тайм-аут запиту"
+
+#: src/utils.ts:165
+#, c-format, fuzzy
+msgid "Request throttled"
+msgstr "Запит до сервера перервано"
+
+#: src/utils.ts:174
+#, c-format
+msgid "Malformed response"
+msgstr "Некоректна відповідь"
+
+#: src/utils.ts:183
+#, c-format
+msgid "Network error"
+msgstr "Мережева помилка"
+
+#: src/utils.ts:192
+#, c-format
+msgid "Unexpected request error"
+msgstr "Неочікувана помилка запиту"
+
+#: src/utils.ts:201
+#, c-format
+msgid "Unexpected error"
+msgstr "Неочікувана помилка"
+
+#: src/utils.ts:377
+#, c-format, fuzzy
+msgid "IBAN numbers usually have more that 4 digits"
+msgstr "Номера IBAN зазвичай мають більше 4ьох цифр"
+
+#: src/utils.ts:379
+#, c-format, fuzzy
+msgid "IBAN numbers usually have less that 34 digits"
+msgstr "Номера IBAN зазвичай мають менше 34ьох цифр"
+
+#: src/utils.ts:387
+#, c-format, fuzzy
+msgid "IBAN country code not found"
+msgstr "Код країни IBAN не знайдено"
+
+#: src/utils.ts:401
+#, c-format, fuzzy
+msgid "IBAN number is not valid, checksum is wrong"
+msgstr "Номер IBAN невірний, контрольна сума не сходиться"
+
+#: src/context/config.ts:136
+#, c-format
+msgid ""
+"the bank backend is not supported. supported version \"%1$s\", server version "
+"\"%2$s\""
+msgstr ""
+
+#: src/hooks/preferences.ts:55
+#, c-format
+msgid "Max withdrawal amount"
+msgstr "Максимальна сумма для виведення"
+
+#: src/hooks/preferences.ts:57
+#, c-format
+msgid "Show withdrawal confirmation"
+msgstr "Показати підтвердження виводу"
+
+#: src/hooks/preferences.ts:59
+#, c-format
+msgid "Show demo description"
+msgstr "Показати демо опис"
+
+#: src/hooks/preferences.ts:61
+#, c-format
+msgid "Show install wallet first"
+msgstr ""
+
+#: src/hooks/preferences.ts:63
+#, c-format
+msgid "Use fast withdrawal form"
+msgstr ""
+
+#: src/hooks/preferences.ts:65
+#, c-format
+msgid "Show debug info"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:90
+#, c-format
+msgid "required"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:92
+#, c-format
+msgid "IBAN should have just uppercased letters and numbers"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:98
+#, c-format
+msgid "not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:100
+#, c-format
+msgid "should be greater than 0"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:102
+#, c-format
+msgid "balance is not enough"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:112
+#, c-format
+msgid "does not follow the pattern"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:114
+#, c-format
+msgid "only \"IBAN\" target are supported"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:116
+#, c-format
+msgid "use the \"amount\" parameter to specify the amount to be transferred"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:118
+#, c-format
+msgid "the amount is not valid"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:120
+#, c-format
+msgid "use the \"message\" parameter to specify a reference text for the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:160
+#, c-format
+msgid "The request was invalid or the payto://-URI used unacceptable features."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:167
+#, c-format
+msgid "Not enough permission to complete the operation."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:174
+#, c-format
+msgid "The destination account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:181
+#, c-format
+msgid "The origin and the destination of the transfer can't be the same."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:188
+#, c-format
+msgid "Your balance is not enough."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:195
+#, c-format
+msgid "The origin account \"%1$s\" was not found."
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:212
+#, c-format
+msgid "Wire transfer created!"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:270
+#, c-format
+msgid "Using a form"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:310
+#, c-format
+msgid "Import payto:// URI"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:335
+#, c-format
+msgid "Recipient"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:359
+#, c-format
+msgid "IBAN of the recipient's account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:369
+#, c-format
+msgid "Transfer subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:377
+#, c-format
+msgid "subject"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:390
+#, c-format
+msgid "some text to identify the transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:400
+#, c-format
+msgid "Amount"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:415
+#, c-format
+msgid "amount to transfer"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:425
+#, c-format
+msgid "payto URI:"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:436
+#, c-format
+msgid "uniform resource identifier of the target account"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:437
+#, c-format
+msgid "payto://iban/[receiver-iban]?message=[subject]&amount=[%1$s:X.Y]"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:457
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/pages/PaytoWireTransferForm.tsx:471
+#, c-format
+msgid "Send"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:71
+#, c-format
+msgid "Missing username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:75
+#, c-format
+msgid "Missing password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:104
+#, c-format
+msgid "Wrong credentials for \"%1$s\""
+msgstr ""
+
+#: src/pages/LoginForm.tsx:111
+#, c-format
+msgid "Account not found"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:142
+#, c-format
+msgid "Username"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:156
+#, c-format
+msgid "username of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:175
+#, c-format
+msgid "Password"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:188
+#, c-format
+msgid "password of the account"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:223
+#, c-format
+msgid "Check"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:237
+#, c-format
+msgid "Log in"
+msgstr ""
+
+#: src/pages/LoginForm.tsx:249
+#, c-format
+msgid "Register"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:52
+#, c-format
+msgid "Latest transactions"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:63
+#, c-format
+msgid "Date"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:71
+#, c-format
+msgid "Counterpart"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:75
+#, c-format
+msgid "Subject"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:111
+#, c-format
+msgid "sent"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:112
+#, c-format
+msgid "received"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:127
+#, c-format
+msgid "invalid value"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "to"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:136
+#, c-format
+msgid "from"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:202
+#, c-format
+msgid "First page"
+msgstr ""
+
+#: src/components/Transactions/views.tsx:209
+#, c-format
+msgid "Next"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:86
+#, c-format
+msgid "Wire transfer completed!"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:93
+#, c-format
+msgid "The withdrawal has been aborted previously and can't be confirmed"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:100
+#, c-format
+msgid ""
+"The withdrawal operation can't be confirmed before a wallet accepted the "
+"transaction."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:107
+#, c-format
+msgid "The operation id is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:114
+#, c-format
+msgid "The operation was not found."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:121
+#, c-format
+msgid "Your balance is not enough for the operation."
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:155
+#, c-format
+msgid "The reserve operation has been confirmed previously and can't be aborted"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:186
+#, c-format
+msgid "Confirm the withdrawal operation"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:203
+#, c-format
+msgid "Wire transfer details"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:217
+#, c-format
+msgid "Taler Exchange operator's account"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:228
+#, c-format
+msgid "Taler Exchange operator's name"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:317
+#, c-format
+msgid "Transfer"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:342
+#, c-format
+msgid "Authentication required"
+msgstr ""
+
+#: src/pages/WithdrawalConfirmationQuestion.tsx:352
+#, c-format
+msgid "This operation was created with other username"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:209
+#, c-format
+msgid ""
+"Unauthorized to make the operation, maybe the session has expired or the "
+"password changed."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:218
+#, c-format
+msgid "The operation was rejected due to insufficient funds."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:268
+#, c-format
+msgid "Withdrawal confirmed"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:272
+#, c-format
+msgid ""
+"The wire transfer to the Taler operator has been initiated. You will soon "
+"receive the requested amount in your Taler wallet."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:287
+#, c-format
+msgid "Do not show this again"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:319
+#, c-format
+msgid "Close"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:399
+#, c-format
+msgid "On this device"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:404
+#, c-format
+msgid ""
+"If you are using a web browser on desktop you should access your wallet with the "
+"GNU Taler WebExtension now or click the link if your WebExtension have the "
+"\"Inject Taler support\" option enabled."
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:417
+#, c-format
+msgid "Start"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:426
+#, c-format
+msgid "On a mobile phone"
+msgstr ""
+
+#: src/pages/OperationState/views.tsx:431
+#, c-format
+msgid "Scan the QR code with your mobile device."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:73
+#, c-format
+msgid "There is an operation already"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:75
+#, c-format
+msgid "Complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:84
+#, c-format
+msgid "this page"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:101
+#, c-format
+msgid "invalid"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:116
+#, c-format
+msgid "Server responded with an invalid withdraw URI"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:117
+#, c-format
+msgid "Withdraw URI: %1$s"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:132
+#, c-format
+msgid "The operation was rejected due to insufficient funds"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:253
+#, c-format
+msgid "Continue"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:282
+#, c-format
+msgid "Prepare your wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:285
+#, c-format
+msgid ""
+"After using your wallet you will need to confirm or cancel the operation on this "
+"site."
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:295
+#, c-format
+msgid "You need a GNU Taler Wallet"
+msgstr ""
+
+#: src/pages/WalletWithdrawForm.tsx:300
+#, c-format
+msgid "If you don't have one yet you can follow the instruction in"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:55
+#, c-format
+msgid "Send money"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:73
+#, c-format
+msgid "to a %1$s wallet"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:95
+#, c-format
+msgid "Withdraw digital money into your mobile wallet or browser extension"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:109
+#, c-format
+msgid "operation ready"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:129
+#, c-format
+msgid "to another bank account"
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:149
+#, c-format
+msgid "Make a wire transfer to an account with known bank account number."
+msgstr ""
+
+#: src/pages/PaymentOptions.tsx:171
+#, c-format
+msgid "Transfer details"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:41
+#, c-format
+msgid "This is a demo bank"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:46
+#, c-format
+msgid ""
+"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 %1$s."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:53
+#, c-format
+msgid "This part of the demo shows how a bank that supports Taler directly would work."
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:70
+#, c-format
+msgid "Pending account delete operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:72
+#, c-format
+msgid "Pending account update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:74
+#, c-format
+msgid "Pending password update operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:76
+#, c-format
+msgid "Pending transaction operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:78
+#, c-format
+msgid "Pending withdrawal operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:80
+#, c-format
+msgid "Pending cashout operation"
+msgstr ""
+
+#: src/pages/AccountPage/views.tsx:91
+#, c-format
+msgid "You can complete or cancel the operation in"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:64
+#, c-format
+msgid "Internal error, please report."
+msgstr ""
+
+#: src/pages/BankFrame.tsx:100
+#, c-format
+msgid "Preferences"
+msgstr ""
+
+#: src/pages/BankFrame.tsx:184
+#, c-format
+msgid "Welcome, %1$s"
+msgstr ""
+
+#: src/pages/WireTransfer.tsx:79
+#, c-format
+msgid "Make a wire transfer"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:72
+#, c-format
+msgid "Accounts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:75
+#, c-format
+msgid "A list of all business account in the bank."
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:86
+#, c-format
+msgid "Create account"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:106
+#, c-format
+msgid "Name"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:110
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:112
+#, c-format
+msgid "Actions"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:151
+#, c-format
+msgid "unknown"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:170
+#, c-format
+msgid "change password"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:179
+#, c-format
+msgid "cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountList.tsx:189
+#, c-format
+msgid "remove"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:168
+#, c-format
+msgid "Cashout not implemented"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:184
+#, c-format
+msgid "Select a section"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:202
+#, c-format
+msgid "Last hour"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:208
+#, c-format
+msgid "Last day"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:216
+#, c-format
+msgid "Last month"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:222
+#, c-format
+msgid "Last year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:310
+#, c-format
+msgid "Last Year"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:325
+#, c-format
+msgid "Trading volume on %1$s compared to %2$s"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:342
+#, c-format
+msgid "Cashin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:352
+#, c-format
+msgid "Cashout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:364
+#, c-format
+msgid "Payin"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:374
+#, c-format
+msgid "Payout"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:388
+#, c-format
+msgid "download stats as CSV"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:494
+#, c-format
+msgid "Decreased by"
+msgstr ""
+
+#: src/pages/admin/AdminHome.tsx:498
+#, c-format
+msgid "Increased by"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:89
+#, c-format
+msgid "Download bank stats"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:110
+#, c-format
+msgid "Include hour metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:143
+#, c-format
+msgid "Include day metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:173
+#, c-format
+msgid "Include month metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:206
+#, c-format
+msgid "Include year metric"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:239
+#, c-format
+msgid "Include table header"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:272
+#, c-format
+msgid "Add previous metric for compare"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:307
+#, c-format
+msgid "Fail on first error"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:364
+#, c-format
+msgid "Download"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:381
+#, c-format
+msgid "downloading... %1$s"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:399
+#, c-format
+msgid "Download completed"
+msgstr ""
+
+#: src/pages/DownloadStats.tsx:400
+#, c-format
+msgid "click here to save the file in your computer"
+msgstr ""
+
+#: src/pages/PublicHistoriesPage.tsx:78
+#, c-format
+msgid "History of public accounts"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:48
+#, c-format
+msgid "Currently, the bank is not accepting new registrations!"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:87
+#, c-format
+msgid "Missing name"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:91
+#, c-format
+msgid "Use letters and numbers only, and start with a lowercase letter"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:107
+#, c-format
+msgid "Passwords don't match"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:130
+#, c-format
+msgid "Server replied with invalid phone or email."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:137
+#, c-format
+msgid "Registration is disabled because the bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:144
+#, c-format
+msgid "No enough permission to create that account."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:151
+#, c-format
+msgid "That account id is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:158
+#, c-format
+msgid "That username is already taken."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:165
+#, c-format
+msgid "That username can't be used because is reserved."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:172
+#, c-format
+msgid "Only admin is allow to set debt limit."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:179
+#, c-format
+msgid "No information for the selected authentication channel."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:186
+#, c-format
+msgid "Authentication channel is not supported."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:193
+#, c-format
+msgid "Only admin can create accounts with second factor authentication."
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:233
+#, c-format
+msgid "Account registration"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:315
+#, c-format
+msgid "Repeat password"
+msgstr ""
+
+#: src/pages/RegistrationPage.tsx:457
+#, c-format
+msgid "Create a random temporary user"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:110
+#, c-format
+msgid "If you have a Taler wallet installed in this device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:116
+#, c-format
+msgid ""
+"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"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:143
+#, c-format
+msgid "Withdraw"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:152
+#, c-format
+msgid "Or if you have the wallet in another device"
+msgstr ""
+
+#: src/pages/QrCodeSection.tsx:157
+#, c-format
+msgid "Scan the QR below to start the withdrawal."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:79
+#, c-format
+msgid "Operation aborted"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:82
+#, c-format
+msgid ""
+"The wire transfer to the Taler Exchange operator's account was aborted, your "
+"balance was not affected."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:88
+#, c-format
+msgid "You can close this page now or continue to the account page."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:147
+#, c-format
+msgid "Done"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:158
+#, c-format
+msgid "Operation canceled"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:173
+#, c-format
+msgid "The operation is marked as 'selected' but some step in the withdrawal failed"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:175
+#, c-format
+msgid "The account is selected but no withdrawal identification found."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:188
+#, c-format
+msgid ""
+"There is a withdrawal identification but no account has been selected or the "
+"selected account is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:202
+#, c-format
+msgid ""
+"No withdrawal ID found and no account has been selected or the selected account "
+"is invalid."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:259
+#, c-format
+msgid "Operation not found"
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:263
+#, c-format
+msgid ""
+"This operation is not known by the server. The operation id is wrong or the "
+"server deleted the operation information before reaching here."
+msgstr ""
+
+#: src/pages/WithdrawalQRCode.tsx:278
+#, c-format
+msgid "Cotinue to dashboard"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:98
+#, c-format
+msgid "Cashout not found. It may be also mean that it was already aborted."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:136
+#, c-format
+msgid "Challenge not found."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:143
+#, c-format
+msgid "This user is not authorized to complete this challenge."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:150
+#, c-format
+msgid "Too many attempts, try another code."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:157
+#, c-format
+msgid "The confirmation code is wrong, try again."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:164
+#, c-format
+msgid "The operation expired."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:197
+#, c-format
+msgid "The operation failed."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:212
+#, c-format
+msgid "The operation needs another confirmation to complete."
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:224
+#, c-format
+msgid "Account delete"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:226
+#, c-format
+msgid "Account update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:228
+#, c-format
+msgid "Password update"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:230
+#, c-format
+msgid "Wire transfer"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:232
+#, c-format
+msgid "Withdrawal"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:248
+#, c-format
+msgid "Confirm the operation"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:271
+#, c-format
+msgid "Enter the confirmation code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:313
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:348
+#, c-format
+msgid "Send again"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:359
+#, c-format
+msgid "Send code"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:369
+#, c-format
+msgid "Operation details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:529
+#, c-format
+msgid "Challenge details"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:536
+#, c-format
+msgid "Sent at"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:551
+#, c-format
+msgid "To phone"
+msgstr ""
+
+#: src/pages/SolveChallengePage.tsx:553
+#, c-format
+msgid "To email"
+msgstr ""
+
+#: src/pages/WithdrawalOperationPage.tsx:49
+#, c-format
+msgid "The Withdrawal URI is not valid"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:100
+#, c-format
+msgid "Latest cashouts"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:111
+#, c-format
+msgid "Created"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:115
+#, c-format
+msgid "Total debit"
+msgstr ""
+
+#: src/components/Cashouts/views.tsx:119
+#, c-format
+msgid "Total credit"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:70
+#, c-format
+msgid "Details"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:74
+#, c-format
+msgid "Delete"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:78
+#, c-format
+msgid "Credentials"
+msgstr ""
+
+#: src/pages/ProfileNavigation.tsx:82
+#, c-format
+msgid "Cashouts"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:95
+#, c-format
+msgid "Unable to create a cashout"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:96
+#, c-format
+msgid "The bank configuration does not support cashout operations."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:223
+#, c-format
+msgid "need to be higher due to fees"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:225
+#, c-format
+msgid "the total transfer at destination will be zero"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:250
+#, c-format
+msgid "Cashout created"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:272
+#, c-format
+msgid "Duplicated request detected, check if the operation succeeded or try again."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:279
+#, c-format
+msgid "The conversion rate was incorrectly applied"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:286
+#, c-format
+msgid "The account does not have sufficient funds"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:293
+#, c-format
+msgid "Cashouts are not supported"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:300
+#, c-format
+msgid "Missing cashout URI in the profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:307
+#, c-format
+msgid ""
+"Sending the confirmation message failed, retry later or contact the "
+"administrator."
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:339
+#, c-format
+msgid "Conversion rate"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:360
+#, c-format
+msgid "Fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:374
+#, c-format
+msgid "To account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:381
+#, c-format
+msgid "No cashout account"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:382
+#, c-format
+msgid "Before doing a cashout you need to complete your profile"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:440
+#, c-format
+msgid "Amount to send"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:441
+#, c-format
+msgid "Amount to receive"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:490
+#, c-format
+msgid "Total cost"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:505
+#, c-format
+msgid "Balance left"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:520
+#, c-format
+msgid "Before fee"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:533
+#, c-format
+msgid "Total cashout transfer"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:553
+#, c-format
+msgid "No cashout channel available"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:555
+#, c-format
+msgid ""
+"Before doing a cashout the server need to provide an second channel to confirm "
+"the operation"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:567
+#, c-format
+msgid "Second factor authentication"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:598
+#, c-format
+msgid "Email"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:600
+#, c-format
+msgid "add a email in your profile to enable this option"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:646
+#, c-format
+msgid "SMS"
+msgstr ""
+
+#: src/pages/business/CreateCashout.tsx:648
+#, c-format
+msgid "add a phone number in your profile to enable this option"
+msgstr ""
+
+#: src/pages/account/CashoutListForAccount.tsx:52
+#, c-format
+msgid "Cashout for account %1$s"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:165
+#, c-format
+msgid "it doesn't have the pattern of an IBAN number"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:185
+#, c-format
+msgid "it doesn't have the pattern of an email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:190
+#, c-format
+msgid "should start with +"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:192
+#, c-format
+msgid "phone number can't have other than numbers"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:329
+#, c-format
+msgid "account identification in the bank"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:365
+#, c-format
+msgid "name of the person owner the account"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:374
+#, c-format
+msgid "Internal IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:377
+#, c-format
+msgid "if empty a random account number will be assigned"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:378
+#, c-format
+msgid "account identification for bank transfer"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:423
+#, c-format
+msgid "Phone"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:451
+#, c-format
+msgid "Cashout IBAN"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:452
+#, c-format
+msgid "account number where the money is going to be sent when doing cashouts"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:470
+#, c-format
+msgid "Max debt"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:494
+#, c-format
+msgid "how much is user able to transfer after zero balance"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:508
+#, c-format
+msgid "Is this a Taler Exchange?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:549
+#, c-format
+msgid "This server doesn't support second factor authentication."
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:560
+#, c-format
+msgid "Enable second factor authentication"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:596
+#, c-format
+msgid "Using email"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:654
+#, c-format
+msgid "Using SMS"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:691
+#, c-format
+msgid "Is this account public?"
+msgstr ""
+
+#: src/pages/admin/AccountForm.tsx:719
+#, c-format
+msgid "public accounts have their balance publicly accessible"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:100
+#, c-format
+msgid "Account updated"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:107
+#, c-format
+msgid "The rights to change the account are not sufficient"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:114
+#, c-format
+msgid "The username was not found"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:121
+#, c-format
+msgid "You can't change the legal name, please contact the your account administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:128
+#, c-format
+msgid "You can't change the debt limit, please contact the your account administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:135
+#, c-format
+msgid ""
+"You can't change the cashout address, please contact the your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:177
+#, c-format
+msgid "Account \"%1$s\""
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:190
+#, c-format
+msgid "Change details"
+msgstr ""
+
+#: src/pages/account/ShowAccountDetails.tsx:235
+#, c-format
+msgid "Update"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:78
+#, c-format
+msgid "password doesn't match"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:95
+#, c-format
+msgid "Password changed"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:102
+#, c-format
+msgid "Not authorized to change the password, maybe the session is invalid."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:112
+#, c-format
+msgid ""
+"You need to provide the old password. If you don't have it contact your account "
+"administrator."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:117
+#, c-format
+msgid "Your current password doesn't match, can't change to a new password."
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:149
+#, c-format
+msgid "Update password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:167
+#, c-format
+msgid "New password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:195
+#, c-format
+msgid "Type it again"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:217
+#, c-format
+msgid "repeat the same password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:227
+#, c-format
+msgid "Current password"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:248
+#, c-format
+msgid "your current password, for security"
+msgstr ""
+
+#: src/pages/account/UpdateAccountPassword.tsx:272
+#, c-format
+msgid "Change"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:74
+#, c-format
+msgid ""
+"Account created with password \"%1$s\". The user must change the password on the "
+"next login."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:83
+#, c-format
+msgid "Server replied that phone or email is invalid"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:90
+#, c-format
+msgid "The rights to perform the operation are not sufficient"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:97
+#, c-format
+msgid "Account username is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:104
+#, c-format
+msgid "Account id is already taken"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:111
+#, c-format
+msgid "Bank ran out of bonus credit."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:118
+#, c-format
+msgid "Account username can't be used because is reserved"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:160
+#, c-format
+msgid "Can't create accounts"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:161
+#, c-format
+msgid "Only system admin can create accounts."
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:183
+#, c-format
+msgid "New business account"
+msgstr ""
+
+#: src/pages/admin/CreateNewAccount.tsx:209
+#, c-format
+msgid "Create"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:94
+#, c-format
+msgid "Can't delete the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:95
+#, c-format
+msgid ""
+"The account can't be delete while still holding some balance. First make sure "
+"that the owner make a complete cashout."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:117
+#, c-format
+msgid "Account removed"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:124
+#, c-format
+msgid "No enough permission to delete the account."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:131
+#, c-format
+msgid "The username was not found."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:138
+#, c-format
+msgid "Can't delete a reserved username."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:145
+#, c-format
+msgid "Can't delete an account with balance different than zero."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:170
+#, c-format
+msgid "name doesn't match"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:180
+#, c-format
+msgid "You are going to remove the account"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:182
+#, c-format
+msgid "This step can't be undone."
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:188
+#, c-format
+msgid "Deleting account \"%1$s\""
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:206
+#, c-format
+msgid "Verification"
+msgstr ""
+
+#: src/pages/admin/RemoveAccount.tsx:231
+#, c-format
+msgid "enter the account name that is going to be deleted"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:49
+#, c-format
+msgid "cashout id should be a number"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:65
+#, c-format
+msgid "This cashout not found. Maybe already aborted."
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:106
+#, c-format
+msgid "Cashout detail"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:139
+#, c-format
+msgid "Debited"
+msgstr ""
+
+#: src/pages/business/ShowCashoutDetails.tsx:154
+#, c-format
+msgid "Credited"
+msgstr ""
+
+#: src/Routing.tsx:140
+#, c-format
+msgid "Welcome to %1$s!"
+msgstr ""
diff --git a/packages/bank-ui/src/index.html b/packages/bank-ui/src/index.html
new file mode 100644
index 000000000..0789ecf89
--- /dev/null
+++ b/packages/bank-ui/src/index.html
@@ -0,0 +1,41 @@
+<!--
+ This file is part of GNU Taler
+ (C) 2021--2022 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
+-->
+<!doctype html>
+<html lang="en" class="h-full bg-gray-100">
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
+ <meta name="taler-support" content="uri,api" />
+ <meta name="mobile-web-app-capable" content="yes" />
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link
+ rel="icon"
+ href="data:;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///////////////////////////////////////////////////////////////////////////////////////////////////7//v38//78/P/+/fz//vz7///+/v/+/f3//vz7///+/v/+/fz//v38///////////////////////+/v3///7+/////////////////////////////////////////////////////////v3//v79///////+/v3///////r28v/ct5//06SG/9Gffv/Xqo7/7N/V/9e2nf/bsJb/6uDW/9Sskf/euKH/+/j2///////+/v3//////+3azv+/eE3/2rWd/9Kkhv/Vr5T/48i2/8J+VP/Qn3//3ryn/795Tf/WrpP/2LCW/8B6T//w4Nb///////Pn4P+/d0v/9u3n/+7d0v/EhV7//v///+HDr//fxLD/zph2/+TJt//8/Pv/woBX//Lm3f/y5dz/v3hN//bu6f/JjGn/4sW0///////Df1j/8OLZ//v6+P+/elH/+vj1//jy7f+/elL//////+zYzP/Eg13//////967p//MlHT/wn5X///////v4Nb/yY1s///////jw7H/06KG////////////z5t9/+fNvf//////x4pn//Pp4v/8+vn/w39X/8WEX///////5s/A/9CbfP//////27Oc/9y2n////////////9itlf/gu6f//////86Vdf/r2Mz//////8SCXP/Df1j//////+7d0v/KkG7//////+HBrf/VpYr////////////RnoH/5sq6///////Ii2n/8ubf//39/P/Cf1j/xohk/+bNvv//////wn5W//Tq4//58/D/wHxV//7+/f/59fH/v3xU//39/P/w4Nf/xIFb///////hw7H/yo9t/+/f1f/AeU3/+/n2/+nSxP/FhmD//////9qzm//Upon/4MSx/96+qf//////xINc/+3bz//48e3/v3hN//Pn3///////6M+//752S//gw6//06aK/8J+VP/kzLr/zZd1/8OCWv/q18r/17KZ/9Ooi//fv6r/v3dK/+vWyP///////v39///////27un/1aeK/9Opjv/m1cf/1KCC/9a0nP/n08T/0Jx8/82YdP/QnHz/16yR//jx7P///////v39///////+/f3///7+///////+//7//v7+///////+/v7//v/+/////////////////////////v7//v79///////////////////+/v/+/Pv//v39///+/v/+/Pv///7+//7+/f/+/Pv//v39//79/P/+/Pv///7+////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
+ />
+ <link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
+ <title>Bank</title>
+ <!-- Entry point for the bank SPA. -->
+ <script type="module" src="index.js"></script>
+ <link rel="stylesheet" href="index.css" />
+ </head>
+
+ <body class="h-full">
+ <div id="app"></div>
+ </body>
+</html>
diff --git a/packages/bank-ui/src/index.tsx b/packages/bank-ui/src/index.tsx
new file mode 100644
index 000000000..f559288a3
--- /dev/null
+++ b/packages/bank-ui/src/index.tsx
@@ -0,0 +1,27 @@
+/*
+ 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 { App } from "./app.js";
+import { h, render } from "preact";
+import "./scss/main.css";
+
+const app = document.getElementById("app");
+
+if (app) {
+ render(<App />, app);
+} else {
+ console.error("HTML element with id 'app' not found.");
+}
diff --git a/packages/bank-ui/src/manifest.json b/packages/bank-ui/src/manifest.json
new file mode 100644
index 000000000..8790b10c9
--- /dev/null
+++ b/packages/bank-ui/src/manifest.json
@@ -0,0 +1,21 @@
+{
+ "name": "taler-bank",
+ "short_name": "taler-bank",
+ "start_url": "/",
+ "display": "standalone",
+ "orientation": "portrait",
+ "background_color": "#fff",
+ "theme_color": "#673ab8",
+ "icons": [
+ {
+ "src": "/assets/icons/android-chrome-192x192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "/assets/icons/android-chrome-512x512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ]
+}
diff --git a/packages/bank-ui/src/pages/AccountPage/index.ts b/packages/bank-ui/src/pages/AccountPage/index.ts
new file mode 100644
index 000000000..8a9471ef4
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/index.ts
@@ -0,0 +1,135 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+
+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;
+ balance: 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/bank-ui/src/pages/AccountPage/state.ts b/packages/bank-ui/src/pages/AccountPage/state.ts
new file mode 100644
index 000000000..f8b91a2ce
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/state.ts
@@ -0,0 +1,122 @@
+/*
+ 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;
+
+ const positiveBalance = balanceIsDebit
+ ? Amounts.zeroOfAmount(balance)
+ : balance;
+
+ return {
+ status: "ready",
+ onOperationCreated,
+ error: undefined,
+ tab,
+ routeCashout,
+ routeOperationDetails,
+ routeCreateWireTransfer,
+ routePublicAccounts,
+ routeSolveSecondFactor,
+ onAuthorizationRequired,
+ onClose,
+ routeClose,
+ routeChargeWallet,
+ routeWireTransfer,
+ account,
+ limit,
+ balance: positiveBalance,
+ };
+}
diff --git a/packages/bank-ui/src/pages/AccountPage/stories.tsx b/packages/bank-ui/src/pages/AccountPage/stories.tsx
new file mode 100644
index 000000000..fe09a4f89
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/stories.tsx
@@ -0,0 +1,29 @@
+/*
+ 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/bank-ui/src/pages/AccountPage/test.ts b/packages/bank-ui/src/pages/AccountPage/test.ts
new file mode 100644
index 000000000..14c8be948
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/test.ts
@@ -0,0 +1,31 @@
+/*
+ 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/bank-ui/src/pages/AccountPage/views.tsx b/packages/bank-ui/src/pages/AccountPage/views.tsx
new file mode 100644
index 000000000..42892f536
--- /dev/null
+++ b/packages/bank-ui/src/pages/AccountPage/views.tsx
@@ -0,0 +1,156 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+
+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,
+ balance,
+ 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}
+ balance={balance}
+ routeClose={routeClose}
+ onClose={onClose}
+ onOperationCreated={onOperationCreated}
+ onAuthorizationRequired={onAuthorizationRequired}
+ />
+ <Transactions
+ account={account}
+ routeCreateWireTransfer={routeCreateWireTransfer}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/BankFrame.stories.tsx b/packages/bank-ui/src/pages/BankFrame.stories.tsx
new file mode 100644
index 000000000..c874ac4ca
--- /dev/null
+++ b/packages/bank-ui/src/pages/BankFrame.stories.tsx
@@ -0,0 +1,29 @@
+/*
+ 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/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx
new file mode 100644
index 000000000..db757ee07
--- /dev/null
+++ b/packages/bank-ui/src/pages/BankFrame.tsx
@@ -0,0 +1,368 @@
+/*
+ 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,
+ ObservabilityEventType,
+ TalerError,
+ TranslatedString,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Footer,
+ Header,
+ Loading,
+ RouteDefinition,
+ ToastBanner,
+ notifyError,
+ notifyException,
+ useBankCoreApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { useEffect, useErrorBoundary, useState } from "preact/hooks";
+import { privatePages } from "../Routing.js";
+import { useSettingsContext } from "../context/settings.js";
+import { useAccountDetails } from "../hooks/account.js";
+import { useBankState } from "../hooks/bank-state.js";
+import {
+ getAllBooleanPreferences,
+ getLabelForPreferences,
+ usePreferences,
+} from "../hooks/preferences.js";
+import { useSessionState } from "../hooks/session.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({})}
+ notificationURL={
+ preferences.showDebugInfo
+ ? privatePages.notifications.url({})
+ : undefined
+ }
+ 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-4">
+ {getAllBooleanPreferences().map((set) => {
+ const isOn: boolean = !!preferences[set];
+ return (
+ <li key={set} class="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 top-14 w-full">
+ <div class="mx-auto w-4/5">
+ <ToastBanner />
+ {/* <Attention type="success" title={"hola" as TranslatedString} onClose={() => { }} /> */}
+ </div>
+ </div>
+
+ <main class="-mt-32 flex-1">
+ {account && routeAccountDetails && (
+ <header class="py-6 bg-indigo-600">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+ <h1 class=" flex flex-wrap items-center justify-between sm:flex-nowrap">
+ <span class="text-2xl font-bold tracking-tight text-white">
+ <WelcomeAccount
+ account={account}
+ routeAccountDetails={routeAccountDetails}
+ />
+ </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-4 sm:px-6 lg:px-8">
+ <div class="rounded-lg bg-white px-5 py-6 shadow sm:px-6">
+ {children}
+ </div>
+ </div>
+ </main>
+
+ <AppActivity />
+
+ <Footer
+ testingUrlKey="corebank-api-base-url"
+ GIT_HASH={GIT_HASH}
+ VERSION={VERSION}
+ />
+ </div>
+ );
+}
+
+function Wait({ class: clazz }: { class?: string }): VNode {
+ return (
+ <Fragment>
+ <style>{`
+ .animated-loader {
+ display: inline-block;
+ --b: 5px;
+ border-radius: 50%;
+ aspect-ratio: 1;
+ padding: 1px;
+ background: conic-gradient(#0000 10%,#4f46e5) content-box;
+ -webkit-mask:
+ repeating-conic-gradient(#0000 0deg,#000 1deg 20deg,#0000 21deg 36deg),
+ radial-gradient(farthest-side,#0000 calc(100% - var(--b) - 1px),#000 calc(100% - var(--b)));
+ -webkit-mask-composite: destination-in;
+ mask-composite: intersect;
+ animation:spinning-loader 1s infinite steps(10);
+ }
+ @keyframes spinning-loader {to{transform: rotate(1turn)}}
+ `}</style>
+ <div class={`animated-loader ${clazz}`} />
+ </Fragment>
+ );
+}
+
+function AppActivity(): VNode {
+ const [lastEvent, setLastEvent] = useState<{
+ url: string;
+ id: string;
+ when: AbsoluteTime;
+ }>();
+ const [status, setStatus] = useState<"ok" | "fail">();
+ const d = useBankCoreApiContext();
+ const onBackendActivity = !d ? undefined : d.onActivity;
+ const cancelRequest = !d ? undefined : d.cancelRequest;
+ const [pref] = usePreferences();
+ useEffect(() => {
+ // console.log("ASDASDS", onBackendActivity)
+ if (!pref.showDebugInfo) return;
+ if (!onBackendActivity) return;
+ return onBackendActivity((ev) => {
+ switch (ev.type) {
+ case ObservabilityEventType.HttpFetchStart: {
+ setLastEvent(ev);
+ setStatus(undefined);
+ return;
+ }
+ case ObservabilityEventType.HttpFetchFinishError: {
+ setStatus("fail");
+ return;
+ }
+ case ObservabilityEventType.HttpFetchFinishSuccess: {
+ setStatus("ok");
+ return;
+ }
+ /**
+ * all of this are ignored
+ */
+ case ObservabilityEventType.DbQueryStart:
+ case ObservabilityEventType.DbQueryFinishSuccess:
+ case ObservabilityEventType.DbQueryFinishError:
+ case ObservabilityEventType.RequestStart:
+ case ObservabilityEventType.RequestFinishSuccess:
+ case ObservabilityEventType.RequestFinishError:
+ case ObservabilityEventType.TaskStart:
+ case ObservabilityEventType.TaskStop:
+ case ObservabilityEventType.TaskReset:
+ case ObservabilityEventType.ShepherdTaskResult:
+ case ObservabilityEventType.DeclareTaskDependency:
+ case ObservabilityEventType.CryptoStart:
+ case ObservabilityEventType.CryptoFinishSuccess:
+ case ObservabilityEventType.CryptoFinishError:
+ case ObservabilityEventType.Message:
+ return;
+ default: {
+ assertUnreachable(ev);
+ }
+ }
+ });
+ });
+ if (!pref.showDebugInfo || !lastEvent) return <Fragment />;
+ return (
+ <div
+ data-status={status}
+ class="fixed z-20 bottom-0 w-full ease-in-out delay-1000 transition-transform data-[status=ok]:scale-y-0"
+ >
+ <div
+ data-status={status}
+ class="mx-auto w-4/5 center flex p-1 bg-gray-300 m-1 data-[status=fail]:bg-red-200 data-[status=ok]:bg-green-200 "
+ >
+ {!status ? <Wait class="w-6 h-6" /> : <div class="w-6 h-6" />}
+
+ <p class="ml-2 my-auto text-sm text-gray-500">{lastEvent.url}</p>
+ {!status ? (
+ <button
+ onClick={() => {
+ if (cancelRequest) cancelRequest(lastEvent.id);
+ }}
+ >
+ cancel
+ </button>
+ ) : undefined}
+ </div>
+ </div>
+ );
+}
+
+function WelcomeAccount({
+ account,
+ routeAccountDetails,
+}: {
+ account: string;
+ routeAccountDetails: RouteDefinition;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ 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/bank-ui/src/pages/LoginForm.tsx b/packages/bank-ui/src/pages/LoginForm.tsx
new file mode 100644
index 000000000..600025400
--- /dev/null
+++ b/packages/bank-ui/src/pages/LoginForm.tsx
@@ -0,0 +1,230 @@
+/*
+ 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, createRFC8959AccessTokenEncoded, createRFC8959AccessTokenPlain } 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 "@gnu-taler/web-util/browser";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { undefinedIfEmpty } from "../utils.js";
+import { doAutoFocus } from "./PaytoWireTransferForm.js";
+import { USERNAME_REGEX } from "./RegistrationPage.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 {
+ lib: { auth: 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, numbers or any of these characters: - . _ ~`
+ : undefined,
+ password: !password ? i18n.str`Missing password` : undefined,
+ });
+
+ async function doLogout() {
+ session.logOut();
+ }
+
+ const loginHandler =
+ !username || !password
+ ? undefined
+ : withErrorHandler(
+ async () =>
+ authenticator(username).createAccessTokenBasic(username, password, {
+ scope: "readwrite",
+ duration: { d_us: "forever" },
+ refreshable: true,
+ }),
+ (result) => {
+ session.logIn({ username, token: createRFC8959AccessTokenEncoded(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/bank-ui/src/pages/OperationState/index.ts b/packages/bank-ui/src/pages/OperationState/index.ts
new file mode 100644
index 000000000..38f698a04
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/index.ts
@@ -0,0 +1,157 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+
+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/bank-ui/src/pages/OperationState/state.ts b/packages/bank-ui/src/pages/OperationState/state.ts
new file mode 100644
index 000000000..19c097d18
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/state.ts
@@ -0,0 +1,234 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+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 {
+ lib: { 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().href,
+ 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/bank-ui/src/pages/OperationState/stories.tsx b/packages/bank-ui/src/pages/OperationState/stories.tsx
new file mode 100644
index 000000000..82253b82c
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/stories.tsx
@@ -0,0 +1,29 @@
+/*
+ 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/bank-ui/src/pages/OperationState/test.ts b/packages/bank-ui/src/pages/OperationState/test.ts
new file mode 100644
index 000000000..d47cb64a2
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/test.ts
@@ -0,0 +1,31 @@
+/*
+ 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/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx
new file mode 100644
index 000000000..62308eca6
--- /dev/null
+++ b/packages/bank-ui/src/pages/OperationState/views.tsx
@@ -0,0 +1,447 @@
+/*
+ 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,
+ useTalerWalletIntegrationAPI,
+ 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";
+
+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,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Your balance is not enough.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: hasError.detail.hint as TranslatedString,
+ debug: hasError.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: hasError.detail.hint as TranslatedString,
+ debug: hasError.detail,
+ when: AbsoluteTime.now(),
+ });
+ 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/bank-ui/src/pages/PaymentOptions.stories.tsx b/packages/bank-ui/src/pages/PaymentOptions.stories.tsx
new file mode 100644
index 000000000..78af886a8
--- /dev/null
+++ b/packages/bank-ui/src/pages/PaymentOptions.stories.tsx
@@ -0,0 +1,35 @@
+/*
+ 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/bank-ui/src/pages/PaymentOptions.tsx b/packages/bank-ui/src/pages/PaymentOptions.tsx
new file mode 100644
index 000000000..386fe31bc
--- /dev/null
+++ b/packages/bank-ui/src/pages/PaymentOptions.tsx
@@ -0,0 +1,239 @@
+/*
+ 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 { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useEffect } from "preact/hooks";
+import { useWithdrawalDetails } from "../hooks/account.js";
+import { useBankState } from "../hooks/bank-state.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
+import { WalletWithdrawForm } from "./WalletWithdrawForm.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,
+ balance,
+ onOperationCreated,
+ onClose,
+ routeOperationDetails,
+ onAuthorizationRequired,
+}: {
+ limit: AmountJson;
+ balance: 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}
+ balance={balance}
+ onAuthorizationRequired={onAuthorizationRequired}
+ onOperationCreated={onOperationCreated}
+ onOperationAborted={onClose}
+ routeCancel={routeClose}
+ />
+ )}
+ {tab === "wire-transfer" && (
+ <PaytoWireTransferForm
+ focus
+ routeHere={routeWireTransfer}
+ limit={limit}
+ balance={balance}
+ onAuthorizationRequired={onAuthorizationRequired}
+ onSuccess={onClose}
+ routeCashout={routeCashout}
+ routeCancel={routeClose}
+ />
+ )}
+ </fieldset>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.stories.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.stories.tsx
new file mode 100644
index 000000000..61cfb5629
--- /dev/null
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.stories.tsx
@@ -0,0 +1,35 @@
+/*
+ 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/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
new file mode 100644
index 000000000..3bf891504
--- /dev/null
+++ b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -0,0 +1,924 @@
+/*
+ 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,
+ TalerCorebankApi,
+ TalerErrorCode,
+ TranslatedString,
+ assertUnreachable,
+ buildPayto,
+ parsePaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
+import {
+ InternationalizationAPI,
+ LocalNotificationBanner,
+ RouteDefinition,
+ ShowInputErrorLabel,
+ notifyInfo,
+ useBankCoreApiContext,
+ 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 { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js";
+import { useBankState } from "../hooks/bank-state.js";
+import { useSessionState } from "../hooks/session.js";
+import { undefinedIfEmpty, validateIBAN, validateTalerBank } from "../utils.js";
+
+interface Props {
+ 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;
+ balance: AmountJson;
+}
+
+export function PaytoWireTransferForm({
+ focus,
+ withAccount,
+ withSubject,
+ withAmount,
+ onSuccess,
+ routeCancel,
+ routeCashout,
+ routeHere,
+ onAuthorizationRequired,
+ limit,
+}: Props): VNode {
+ const [inputType, setInputType] = useState<"form" | "payto" | "qr">("form");
+ const isRawPayto = inputType !== "form";
+
+ const { state: credentials } = useSessionState();
+ const {
+ lib: { 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;
+ let acName: string | undefined;
+ 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);
+ acName = !p.isKnown
+ ? undefined
+ : p.targetType === "iban"
+ ? p.iban
+ : p.targetType === "bitcoin"
+ ? p.address
+ : p.targetType === "x-taler-bank"
+ ? p.account
+ : assertUnreachable(p);
+ } else {
+ if (!account || !subject) return;
+ let payto;
+ acName = account;
+ 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 function createTransactionHandleError() {
+ const request: TalerCorebankApi.CreateTransactionRequest = {
+ payto_uri: puri,
+ amount: sAmount,
+ };
+ const check = IdempotencyRetry.tryFiveTimes();
+ const resp = await api.createTransaction(credentials, request, check);
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_ADMIN_CREDITOR:
+ return notify({
+ type: "error",
+ title: i18n.str`Bank administrator can't be the transfer creditor.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
+ return notify({
+ type: "error",
+ title: i18n.str`The destination account "${
+ acName ?? puri
+ }" was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: {
+ return notify({
+ type: "error",
+ title: i18n.str`Tried to create the transaction ${check.maxTries} times with different UID but failed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "create-transaction",
+ id: String(resp.body.challenge_id),
+ location: routeHere.url({
+ account: account ?? "",
+ amount,
+ subject,
+ }),
+ 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-6 my-4 md:grid-cols-3 bg-gray-100 px-4 pb-4 rounded-lg">
+ <div>
+ <fieldset class="px-2 grid grid-cols-1 gap-y-4 sm:gap-x-4">
+ <legend class="sr-only">
+ <i18n.Translate>Input wire transfer detail</i18n.Translate>
+ </legend>
+ <div class="-space-y-px rounded-md ">
+ <label
+ data-checked={inputType === "form"}
+ class="group rounded-tl-md rounded-tr-md relative flex cursor-pointer border p-4 focus:outline-none bg-white data-[checked=true]:z-10 data-[checked=true]:border-indigo-200 data-[checked=true]:bg-indigo-50"
+ >
+ <input
+ type="radio"
+ name="input-type"
+ 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);
+ }
+ }
+ setInputType("form");
+ }}
+ checked={inputType === "form"}
+ value="form"
+ class="mt-0.5 h-4 w-4 shrink-0 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-600 active:ring-2 active:ring-offset-2 active:ring-indigo-600"
+ />
+ <span class="ml-3 flex flex-col">
+ {/* <!-- Checked: "text-indigo-900", Not Checked: "text-gray-900" --> */}
+ <span
+ data-checked={inputType === "form"}
+ class="block text-sm font-medium data-[checked=true]:text-indigo-900"
+ >
+ <i18n.Translate>Using a form</i18n.Translate>
+ </span>
+ </span>
+ </label>
+ {sendingToFixedAccount ? undefined : (
+ <Fragment>
+ <label
+ data-checked={inputType === "payto"}
+ class="relative flex cursor-pointer border p-4 focus:outline-none bg-white data-[checked=true]:z-10 data-[checked=true]:border-indigo-200 data-[checked=true]:bg-indigo-50"
+ >
+ <input
+ type="radio"
+ name="input-type"
+ 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));
+ }
+ setInputType("payto");
+ }}
+ checked={inputType === "payto"}
+ value="payto"
+ class="mt-0.5 h-4 w-4 shrink-0 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-600 active:ring-2 active:ring-offset-2 active:ring-indigo-600"
+ />
+ <span class="ml-3 flex flex-col">
+ <span
+ data-checked={inputType === "payto"}
+ class="block font-medium data-[checked=true]:text-indigo-900"
+ >
+ payto:// URI
+ </span>
+ <span
+ data-checked={inputType === "payto"}
+ class="block text-sm text-gray-500 data-[checked=true]:text-indigo-600"
+ >
+ <i18n.Translate>
+ A special URI that indicate the transfer amount and
+ account target.
+ </i18n.Translate>
+ </span>
+ </span>
+ </label>
+ {
+ //FIXME: add QR support
+ false && (
+ <label
+ data-checked={inputType === "qr"}
+ class="rounded-bl-md rounded-br-md relative flex cursor-pointer border p-4 focus:outline-none bg-white data-[checked=true]:z-10 data-[checked=true]:border-indigo-200 data-[checked=true]:bg-indigo-50"
+ >
+ <input
+ type="radio"
+ name="input-type"
+ onChange={() => {
+ setInputType("qr");
+ }}
+ checked={inputType === "qr"}
+ value="qr"
+ class="mt-0.5 h-4 w-4 shrink-0 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-600 active:ring-2 active:ring-offset-2 active:ring-indigo-600"
+ />
+ <span class="ml-3 flex flex-col">
+ <span
+ data-checked={inputType === "qr"}
+ class="block font-medium data-[checked=true]:text-indigo-900"
+ >
+ <i18n.Translate>QR code</i18n.Translate>
+ </span>
+ <span
+ data-checked={inputType === "qr"}
+ class="block text-sm text-gray-500 data-[checked=true]:text-indigo-600"
+ >
+ <i18n.Translate>
+ If you have a camera in this device you can import a
+ payto:// URI from a QR code.
+ </i18n.Translate>
+ </span>
+ </span>
+ </label>
+ )
+ }
+ </Fragment>
+ )}
+ </div>
+ {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}
+ </fieldset>
+ </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"
+ required
+ label={i18n.str`Recipient`}
+ help={i18n.str`Id of the recipient's account`}
+ error={errorsWire?.account}
+ onChange={setAccount}
+ value={account}
+ placeholder={i18n.str`username`}
+ focus={focus}
+ disabled={sendingToFixedAccount}
+ />
+ );
+ }
+ case "iban": {
+ return (
+ <TextField
+ id="iban"
+ required
+ label={i18n.str`Recipient`}
+ help={i18n.str`IBAN of the recipient's account`}
+ placeholder={"CC0123456789" as TranslatedString}
+ error={errorsWire?.account}
+ onChange={(v) => setAccount(v.toUpperCase())}
+ value={account}
+ focus={focus}
+ disabled={sendingToFixedAccount}
+ />
+ );
+ }
+ 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`}
+ <b style={{ color: "red" }}> *</b>
+ </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`}
+ <b style={{ color: "red" }}> *</b>
+ </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:`}
+ <b style={{ color: "red" }}> *</b>
+ </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;
+ required?: boolean;
+ 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,
+ required,
+ value,
+ error,
+}: PaytoFieldProps): VNode {
+ return (
+ <div class="sm:col-span-5">
+ <label for={id} class="block text-sm font-medium leading-6 text-gray-900">
+ {label}
+ {required && <b style={{ color: "red" }}> *</b>}
+ </label>
+ <div class="mt-2">
+ <Wrapper withIcon={rightIcons !== undefined}>
+ <input
+ ref={focus ? doAutoFocus : undefined}
+ type="text"
+ class="block w-full disabled:bg-gray-200 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name={id}
+ id={id}
+ disabled={disabled}
+ value={value ?? ""}
+ placeholder={placeholder}
+ autocomplete="off"
+ required
+ onInput={(e): void => {
+ onChange(e.currentTarget.value);
+ }}
+ />
+ {rightIcons}
+ </Wrapper>
+ <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+ </div>
+ {help && <p class="mt-2 text-sm text-gray-500">{help}</p>}
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/ProfileNavigation.tsx b/packages/bank-ui/src/pages/ProfileNavigation.tsx
new file mode 100644
index 000000000..3e81e307c
--- /dev/null
+++ b/packages/bank-ui/src/pages/ProfileNavigation.tsx
@@ -0,0 +1,202 @@
+/*
+ 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 {
+ useNavigationContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+
+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/bank-ui/src/pages/PublicHistoriesPage.tsx b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
new file mode 100644
index 000000000..80ae28dde
--- /dev/null
+++ b/packages/bank-ui/src/pages/PublicHistoriesPage.tsx
@@ -0,0 +1,98 @@
+/*
+ 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.body.length > 0
+ ? result.body[0].username
+ : undefined;
+
+ const [showAccount, setShowAccount] = useState(firstAccount);
+
+ if (!result) {
+ return <Loading />;
+ }
+ if (result instanceof TalerError) {
+ return <Loading />;
+ }
+
+ const { body: accountList } = result;
+
+ const txs: Record<string, h.JSX.Element> = {};
+ const accountsBar = [];
+
+ // Ask story of all the public accounts.
+ for (const account of accountList) {
+ 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/bank-ui/src/pages/QrCodeSection.stories.tsx b/packages/bank-ui/src/pages/QrCodeSection.stories.tsx
new file mode 100644
index 000000000..d53d2e7b4
--- /dev/null
+++ b/packages/bank-ui/src/pages/QrCodeSection.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ 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/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx
new file mode 100644
index 000000000..359d4c18f
--- /dev/null
+++ b/packages/bank-ui/src/pages/QrCodeSection.tsx
@@ -0,0 +1,152 @@
+/*
+ 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,
+ useTalerWalletIntegrationAPI,
+ 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 "@gnu-taler/web-util/browser";
+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 {
+ lib: { 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/bank-ui/src/pages/RegistrationPage.tsx b/packages/bank-ui/src/pages/RegistrationPage.tsx
new file mode 100644
index 000000000..61939c3d6
--- /dev/null
+++ b/packages/bank-ui/src/pages/RegistrationPage.tsx
@@ -0,0 +1,423 @@
+/*
+ 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, TalerErrorCode } from "@gnu-taler/taler-util";
+import {
+ LocalNotificationBanner,
+ RouteDefinition,
+ ShowInputErrorLabel,
+ useBankCoreApiContext,
+ useLocalNotification,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useSettingsContext } from "../context/settings.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}
+ />
+ );
+}
+
+// eslint-disable-next-line no-useless-escape
+export const USERNAME_REGEX = /^[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 {
+ lib: { 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, numbers or any of these characters: - . _ ~`
+ : 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(undefined, {
+ 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_MIN_CASHOUT:
+ return i18n.str`Only the administrator can change the minimum cashout 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/bank-ui/src/pages/ShowNotifications.tsx b/packages/bank-ui/src/pages/ShowNotifications.tsx
new file mode 100644
index 000000000..fe041fb19
--- /dev/null
+++ b/packages/bank-ui/src/pages/ShowNotifications.tsx
@@ -0,0 +1,55 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { useNotifications } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { Time } from "../components/Time.js";
+
+export function ShowNotifications(): VNode {
+ const ns = useNotifications();
+ if (!ns.length) {
+ return <div>no notifications</div>;
+ }
+ return (
+ <div>
+ <p>Notifications</p>
+ <table>
+ <thead></thead>
+ <tbody>
+ {ns.map((n, idx) => {
+ return (
+ <tr key={idx}>
+ <td>
+ <Time
+ timestamp={n.message.when}
+ format="dd/MM/yyyy HH:mm:ss"
+ />
+ </td>
+ <td>{n.message.title}</td>
+ <td>
+ {n.message.type === "error"
+ ? n.message.description
+ : undefined}
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {/* <ToastBanner all /> */}
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/SolveChallengePage.tsx b/packages/bank-ui/src/pages/SolveChallengePage.tsx
new file mode 100644
index 000000000..624890468
--- /dev/null
+++ b/packages/bank-ui/src/pages/SolveChallengePage.tsx
@@ -0,0 +1,793 @@
+/*
+ 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,
+ useNavigationContext,
+ 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 { Time } from "../components/Time.js";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useWithdrawalDetails } from "../hooks/account.js";
+import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js";
+import { useConversionInfo } from "../hooks/regional.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { undefinedIfEmpty } from "../utils.js";
+import { RenderAmount } from "./PaytoWireTransferForm.js";
+import { OperationNotFound } from "./WithdrawalQRCode.js";
+import { IdempotencyRetry } from "../../../taler-util/lib/http-client/utils.js";
+
+const TAN_PREFIX = "T-";
+const TAN_REGEX = /^([Tt](-)?)?[0-9]*$/;
+export function SolveChallengePage({
+ onChallengeCompleted,
+ routeClose,
+}: {
+ onChallengeCompleted: () => void;
+ routeClose: RouteDefinition;
+}): VNode {
+ const {
+ lib: { 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`
+ : !TAN_REGEX.test(code)
+ ? i18n.str`Confirmation codes are numerical, possibly beginning with 'T-.'`
+ : 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ async function completeChallenge() {
+ if (!creds || !code) return;
+ const tan = code.toUpperCase().startsWith(TAN_PREFIX)
+ ? code.substring(TAN_PREFIX.length)
+ : code;
+ await handleError(async () => {
+ {
+ const resp = await api.confirmChallenge(creds, ch.id, { tan });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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, undefined, 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,
+ when: AbsoluteTime.now(),
+ });
+ }
+ // 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.`,
+ when: AbsoluteTime.now(),
+ });
+ }
+ updateBankState("currentChallenge", undefined);
+ return onChallengeCompleted();
+ }
+ });
+ }
+
+ return (
+ <Fragment>
+ <LocalNotificationBanner notification={notification} />
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 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>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ This operation is protected with second factor authentication. In
+ order to complete it we need to verify your identity using the
+ authentication channel you provided.
+ </i18n.Translate>
+ </p>
+ </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
+ onPaste={(e) => {
+ e.preventDefault();
+ const pasted = e.clipboardData?.getData("text/plain");
+ if (!pasted) return;
+ if (pasted.toUpperCase().startsWith(TAN_PREFIX)) {
+ const sub = pasted.substring(TAN_PREFIX.length);
+ setCode(sub);
+ return;
+ }
+ setCode(pasted);
+ }}
+ name="answer"
+ id="answer"
+ autocomplete="off"
+ onChange={(e): void => {
+ setCode(e.currentTarget.value);
+ }}
+ />
+ </div>
+ <ShowInputErrorLabel
+ message={errors?.code}
+ isDirty={code !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ {((ch: TalerCorebankApi.TanChannel): VNode => {
+ switch (ch) {
+ case TalerCorebankApi.TanChannel.SMS:
+ return (
+ <i18n.Translate>
+ You should have received a code in your phone.
+ </i18n.Translate>
+ );
+ case TalerCorebankApi.TanChannel.EMAIL:
+ return (
+ <i18n.Translate>
+ You should have received a code in your email.
+ </i18n.Translate>
+ );
+ default:
+ assertUnreachable(ch);
+ }
+ })(ch.info.tan_channel)}
+ </p>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ The confirmation code starts with "{TAN_PREFIX}" followed
+ by numbers.
+ </i18n.Translate>
+ </p>
+ </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 } = useTranslationContext();
+ const { config } = useBankCoreApiContext();
+
+ const firstTime = AbsoluteTime.isNever(challenge.sent);
+ useEffect(() => {
+ if (firstTime) {
+ onStart();
+ }
+ }, []);
+
+ const subtitle = ((op): TranslatedString => {
+ switch (op) {
+ case "delete-account":
+ return i18n.str`Removing account`;
+ case "update-account":
+ return i18n.str`Updating account values`;
+ case "update-password":
+ return i18n.str`Updating password`;
+ case "create-transaction":
+ return i18n.str`Making a wire transfer`;
+ case "confirm-withdrawal":
+ return i18n.str`Confirming withdrawal`;
+ case "create-cashout":
+ return i18n.str`Making a cashout`;
+ }
+ })(challenge.operation);
+
+ return (
+ <div class="px-4 mt-4 ">
+ <div class="w-full">
+ <div class="border-gray-100">
+ <h2 class="text-base font-semibold leading-10 text-gray-900">
+ <span class=" text-black font-semibold leading-6 ">
+ <i18n.Translate>Operation:</i18n.Translate>
+ </span>{" "}
+ &nbsp;
+ <span class=" text-black font-normal leading-6 ">{subtitle}</span>
+ </h2>
+ <dl class="divide-y divide-gray-100">
+ {((): VNode => {
+ switch (challenge.operation) {
+ case "delete-account":
+ 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>Type</i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ <i18n.Translate>
+ Updating account settings
+ </i18n.Translate>
+ </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>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>
+ </Fragment>
+ );
+ case "create-transaction": {
+ const payto = parsePaytoUri(challenge.request.payto_uri)!;
+ return (
+ <Fragment>
+ {challenge.request.amount && (
+ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
+ <dt class="text-sm font-medium leading-6 text-gray-900">
+ <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);
+ }
+ })()}
+ </dl>
+ {challenge.info && (
+ <h2 class="text-base font-semibold leading-7 text-gray-900 mt-4">
+ <span
+ class="text-sm text-black font-semibold leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Challenge details</i18n.Translate>
+ </span>
+ </h2>
+ )}
+ <dl class="divide-y divide-gray-100">
+ {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/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
new file mode 100644
index 000000000..a9c652643
--- /dev/null
+++ b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx
@@ -0,0 +1,404 @@
+/*
+ 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,
+ 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 "@gnu-taler/web-util/browser";
+import { useSessionState } from "../hooks/session.js";
+import { useBankState } from "../hooks/bank-state.js";
+import { usePreferences } from "../hooks/preferences.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { undefinedIfEmpty } from "../utils.js";
+import { OperationState } from "./OperationState/index.js";
+import {
+ InputAmount,
+ RenderAmount,
+ doAutoFocus,
+} from "./PaytoWireTransferForm.js";
+
+const RefAmount = forwardRef(InputAmount);
+
+function OldWithdrawalForm({
+ onOperationCreated,
+ limit,
+ balance,
+ routeCancel,
+ focus,
+ routeOperationDetails,
+}: {
+ limit: AmountJson;
+ balance: 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 {
+ lib: { bank: api },
+ config,
+ } = 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ break;
+ }
+ case HttpStatusCode.NotFound: {
+ notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ 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>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Current balance is{" "}
+ <RenderAmount
+ value={balance}
+ spec={config.currency_specification}
+ />
+ </i18n.Translate>
+ </p>
+ {Amounts.cmp(limit, balance) > 0 ? (
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Your account allows you to withdraw{" "}
+ <RenderAmount
+ value={limit}
+ spec={config.currency_specification}
+ />
+ </i18n.Translate>
+ </p>
+ ) : undefined}
+ <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,
+ balance,
+ routeCancel,
+ onAuthorizationRequired,
+ onOperationCreated,
+ onOperationAborted,
+ routeOperationDetails,
+}: {
+ limit: AmountJson;
+ balance: 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-6 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}
+ balance={balance}
+ routeCancel={routeCancel}
+ onOperationCreated={onOperationCreated}
+ />
+ ) : (
+ <OperationState
+ currency={limit.currency}
+ onAuthorizationRequired={onAuthorizationRequired}
+ routeClose={routeCancel}
+ routeHere={routeOperationDetails}
+ onAbort={onOperationAborted}
+ // route={routeCancel}
+ />
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/WireTransfer.tsx b/packages/bank-ui/src/pages/WireTransfer.tsx
new file mode 100644
index 000000000..f45390938
--- /dev/null
+++ b/packages/bank-ui/src/pages/WireTransfer.tsx
@@ -0,0 +1,119 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+
+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);
+
+ if (!balance) return <Fragment />;
+
+ const limit = balanceIsDebit
+ ? Amounts.sub(debitThreshold, balance).amount
+ : Amounts.add(balance, debitThreshold).amount;
+
+ const positiveBalance = balanceIsDebit
+ ? Amounts.zeroOfAmount(balance)
+ : balance;
+ return (
+ <div class="px-4 mt-8">
+ <div class="sm:flex sm:items-center mb-4">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Make a wire transfer</i18n.Translate>
+ </h1>
+ </div>
+ </div>
+
+ <PaytoWireTransferForm
+ withAccount={toAccount}
+ withAmount={withAmount}
+ balance={positiveBalance}
+ withSubject={withSubject}
+ routeHere={routeHere}
+ limit={limit}
+ onAuthorizationRequired={onAuthorizationRequired}
+ onSuccess={() => {
+ notifyInfo(i18n.str`Wire transfer created!`);
+ if (onSuccess) onSuccess();
+ }}
+ routeCancel={routeCancel}
+ />
+ </div>
+ );
+}
diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
new file mode 100644
index 000000000..853dd7bae
--- /dev/null
+++ b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -0,0 +1,425 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+import { useBankState } from "../hooks/bank-state.js";
+import { usePreferences } from "../hooks/preferences.js";
+import { useSessionState } from "../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+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,
+ lib: { 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return notify({
+ type: "error",
+ title: i18n.str`Your balance is not enough for the operation.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ operation: "confirm-withdrawal",
+ id: String(resp.body.challenge_id),
+ location: routeHere.url({
+ wopid: withdrawUri.withdrawalOperationId,
+ }),
+ 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`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.BadRequest:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation id is invalid.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The operation was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ 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 => {
+ if (!details.account.isKnown) {
+ 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>
+ );
+ }
+ switch (details.account.targetType) {
+ case "iban": {
+ const name =
+ details.account.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">
+ {details.account.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 name =
+ details.account.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 bank
+ hostname
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.host}
+ </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>
+ 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">
+ {details.account.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>
+ );
+ }
+ case "bitcoin": {
+ const name =
+ details.account.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 address
+ </i18n.Translate>
+ </dt>
+ <dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
+ {details.account.address}
+ </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: {
+ assertUnreachable(details.account);
+ }
+ }
+ })()}
+ <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/bank-ui/src/pages/WithdrawalOperationPage.tsx b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
new file mode 100644
index 000000000..c0c55f14b
--- /dev/null
+++ b/packages/bank-ui/src/pages/WithdrawalOperationPage.tsx
@@ -0,0 +1,73 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+import { useBankState } from "../hooks/bank-state.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+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 {
+ lib: { bank: api },
+ } = useBankCoreApiContext();
+ const uri = stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl: api.getIntegrationAPI().href,
+ 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/bank-ui/src/pages/WithdrawalQRCode.tsx b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx
new file mode 100644
index 000000000..b61f0cc8f
--- /dev/null
+++ b/packages/bank-ui/src/pages/WithdrawalQRCode.tsx
@@ -0,0 +1,310 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+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/bank-ui/src/pages/account/CashoutListForAccount.tsx b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
new file mode 100644
index 000000000..fd6379895
--- /dev/null
+++ b/packages/bank-ui/src/pages/account/CashoutListForAccount.tsx
@@ -0,0 +1,89 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+
+interface Props {
+ account: string;
+ routeClose: RouteDefinition;
+ onAuthorizationRequired: () => void;
+ onCashout: () => void;
+ routeCashoutDetails: RouteDefinition<{ cid: string }>;
+ routeMyAccountDetails: RouteDefinition;
+ routeMyAccountDelete: RouteDefinition;
+ routeMyAccountPassword: RouteDefinition;
+ routeMyAccountCashout: RouteDefinition;
+ routeCreateCashout: RouteDefinition;
+ routeConversionConfig: RouteDefinition;
+}
+
+export function CashoutListForAccount({
+ account,
+ onAuthorizationRequired,
+ onCashout,
+ 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}
+ onCashout={onCashout}
+ onAuthorizationRequired={onAuthorizationRequired}
+ account={account}
+ />
+
+ <Cashouts account={account} routeCashoutDetails={routeCashoutDetails} />
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
new file mode 100644
index 000000000..6db0e5512
--- /dev/null
+++ b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx
@@ -0,0 +1,500 @@
+/*
+ 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,
+ parsePaytoUri
+} from "@gnu-taler/taler-util";
+import {
+ CopyButton,
+ Loading,
+ LocalNotificationBanner,
+ RouteDefinition,
+ notifyInfo,
+ useBankCoreApiContext,
+ 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 { useAccountDetails } from "../../hooks/account.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { useSessionState } from "../../hooks/session.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 {
+ lib: { bank },
+ } = 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 bank.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,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The username was not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME:
+ return notify({
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ }
+ case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: {
+ return notify({
+ type: "error",
+ title: i18n.str`Only the administrator can change the minimum cashout limit.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ default:
+ assertUnreachable(resp);
+ }
+ }
+ });
+ }
+
+ const url = bank.getRevenueAPI(account);
+ url.username = account;
+ const baseURL = url.href;
+
+ const ac = parsePaytoUri(result.body.payto_uri);
+ const payto = !ac?.isKnown ? undefined : ac;
+ let accountLetter: string | undefined = undefined;
+ if (payto) {
+ switch (payto.targetType) {
+ case "iban": {
+ accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\niban=${payto.iban}\nreceiver-name=${result.body.name}\n`;
+ break;
+ }
+ case "x-taler-bank": {
+ accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naccount=${payto.account}\nhost=${payto.host}\nreceiver-name=${result.body.name}\n`;
+ break;
+ }
+ case "bitcoin": {
+ accountLetter = `account-info-url=${url.href}\naccount-type=${payto.targetType}\naddress=${payto.address}\nreceiver-name=${result.body.name}\n`;
+ break;
+ }
+ }
+ }
+
+ 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-6 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>
+ <div class="grid grid-cols-1 gap-x-8 gap-y-8 pt-6 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>Merchant integration</i18n.Translate>
+ </span>
+ </span>
+ </div>
+ </h2>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Use this information to link your Taler Merchant Backoffice
+ account with the current bank account. You can start by copying
+ the values, then go to your merchant backoffice service provider,
+ login into your account and look for the "import" button in the
+ "bank account" section.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ {payto !== undefined && (
+ <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">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="account-type"
+ >
+ {i18n.str`Account type`}
+ </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="account-type"
+ id="account-type"
+ disabled={true}
+ value={account}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Method to use for wire transfer.
+ </i18n.Translate>
+ </p>
+ </div>
+ {((payto) => {
+ switch (payto.targetType) {
+ case "iban": {
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`IBAN`}
+ </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="iban"
+ id="iban"
+ disabled={true}
+ value={payto.iban}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+ }
+ case "x-taler-bank": {
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`IBAN`}
+ </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="iban"
+ id="iban"
+ disabled={true}
+ value={payto.account}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+ }
+ case "bitcoin": {
+ return (
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`Address`}
+ </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="iban"
+ id="iban"
+ disabled={true}
+ value={"DE1231231231"}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ International Bank Account Number.
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+ }
+ }
+ })(payto)}
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`Owner's name`}
+ </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="iban"
+ id="iban"
+ disabled={true}
+ value={result.body.name}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Legal name of the person holding the account.
+ </i18n.Translate>
+ </p>
+ </div>
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="iban"
+ >
+ {i18n.str`Account info URL`}
+ </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="iban"
+ id="iban"
+ disabled={true}
+ value={baseURL}
+ autocomplete="off"
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ From where the merchant can download information about
+ incoming wire transfers to this account.
+ </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>
+ <CopyButton
+ getContent={() => accountLetter ?? ""}
+ class="flex text-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>Copy</i18n.Translate>
+ </CopyButton>
+ </div>
+ </div>
+ )}
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
new file mode 100644
index 000000000..2724fba11
--- /dev/null
+++ b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx
@@ -0,0 +1,319 @@
+/*
+ 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,
+} 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 "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+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 {
+ lib: { 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.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`Account not found`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD:
+ return notify({
+ type: "error",
+ title: i18n.str`You need to provide the old password. If you don't have it contact your account administrator.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD:
+ return notify({
+ type: "error",
+ title: i18n.str`Your current password doesn't match, can't change to a new password.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ 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-6 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`}
+ <b style={{ color: "red" }}> *</b>
+ </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`}
+ <b style={{ color: "red" }}> *</b>
+ </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`}
+ <b style={{ color: "red" }}> *</b>
+ </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/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx
new file mode 100644
index 000000000..ba5da609f
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/AccountForm.tsx
@@ -0,0 +1,859 @@
+/*
+ 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,
+ assertUnreachable,
+ buildPayto,
+ parsePaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
+import {
+ CopyButton,
+ ShowInputErrorLabel,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { ComponentChildren, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+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;
+ min_cashout?: 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, 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,
+ ),
+ min_cashout: Amounts.stringifyValue(
+ template?.min_cashout ?? `${config.currency}:0`,
+ ),
+ 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 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 editableMinCashout =
+ userIsAdmin && (purpose === "create" || purpose === "update");
+ const editableAccount = purpose === "create" && userIsAdmin;
+
+ function updateForm(newForm: typeof defaultValue): void {
+ const trimmedMinCashoutStr = newForm.min_cashout?.trim();
+ const parsedMinCashout = Amounts.parse(
+ `${config.currency}:${trimmedMinCashoutStr}`,
+ );
+ const trimmedDebitThresholdStr = newForm.debit_threshold?.trim();
+ const parsedDebitThreshold = Amounts.parse(
+ `${config.currency}:${trimmedDebitThresholdStr}`,
+ );
+
+ 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
+ : !trimmedDebitThresholdStr
+ ? undefined
+ : !parsedDebitThreshold
+ ? i18n.str`Not valid`
+ : undefined,
+ min_cashout: !editableMinCashout
+ ? undefined
+ : !trimmedMinCashoutStr
+ ? undefined
+ : !parsedMinCashout
+ ? i18n.str`Not valid`
+ : undefined,
+ name: !editableName
+ ? undefined // disabled
+ : purpose === "update" && newForm.name === undefined ? undefined // the field hasn't been changed
+ : !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 = !parsedDebitThreshold
+ ? undefined
+ : Amounts.stringify(parsedDebitThreshold);
+ const minCashout = !parsedMinCashout
+ ? undefined
+ : Amounts.stringify(parsedMinCashout);
+
+ 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,
+ min_cashout: minCashout,
+ 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,
+ min_cashout: minCashout,
+ 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}
+ />
+ )}
+
+ <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">
+ <label
+ for="minCashout"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Minimum cashout`}</label>
+ <InputAmount
+ name="minCashout"
+ left
+ currency={config.currency}
+ value={form.min_cashout ?? defaultValue.min_cashout}
+ onChange={
+ !editableMinCashout
+ ? undefined
+ : (e) => {
+ form.min_cashout = e as AmountString;
+ updateForm(structuredClone(form));
+ }
+ }
+ />
+ <ShowInputErrorLabel
+ message={
+ errors?.min_cashout ? String(errors?.min_cashout) : undefined
+ }
+ isDirty={form.min_cashout !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Custom minimum cashout amount for this account.
+ </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/bank-ui/src/pages/admin/AccountList.tsx b/packages/bank-ui/src/pages/admin/AccountList.tsx
new file mode 100644
index 000000000..6402c2bcd
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/AccountList.tsx
@@ -0,0 +1,234 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+import { useBusinessAccounts } from "../../hooks/regional.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
+
+interface Props {
+ routeCreate: RouteDefinition;
+
+ routeShowAccount: RouteDefinition<{ account: string }>;
+ routeRemoveAccount: RouteDefinition<{ account: string }>;
+ routeUpdatePasswordAccount: RouteDefinition<{ account: string }>;
+}
+
+export function AccountList({
+ routeCreate,
+ routeRemoveAccount,
+ routeShowAccount,
+ 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.type === "fail") {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized:
+ return <Fragment />;
+ default:
+ assertUnreachable(result.case);
+ }
+ }
+
+ const onGoStart = result.isFirstPage ? undefined : result.loadFirst;
+ const onGoNext = result.isLastPage ? undefined : result.loadNext;
+
+ const accounts = result.body;
+ return (
+ <Fragment>
+ <div class="px-4 sm:px-6 lg:px-8 mt-8">
+ <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>
+ </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-4 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/bank-ui/src/pages/admin/AdminHome.tsx b/packages/bank-ui/src/pages/admin/AdminHome.tsx
new file mode 100644
index 000000000..34c121235
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/AdminHome.tsx
@@ -0,0 +1,622 @@
+/*
+ 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,
+ AmountString,
+ Amounts,
+ CurrencySpecification,
+ HttpStatusCode,
+ TalerCorebankApi,
+ TalerError,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ RouteDefinition,
+ useBankCoreApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import {
+ format,
+ 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 { useConversionInfo, useLastMonitorInfo } from "../../hooks/regional.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,
+ 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}
+ routeUpdatePasswordAccount={routeUpdatePasswordAccount}
+ />
+ </Fragment>
+ );
+}
+
+function getDateForTimeframe(
+ date: AbsoluteTime,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+ locale: Locale,
+): string {
+ if (date.t_ms === "never") return "--";
+ switch (timeframe) {
+ case TalerCorebankApi.MonitorTimeframeParam.hour:
+ return `${format(date.t_ms, "HH", { locale })}hs`;
+ case TalerCorebankApi.MonitorTimeframeParam.day:
+ return format(date.t_ms, "EEEE", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.month:
+ return format(date.t_ms, "MMMM", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.year:
+ return format(date.t_ms, "yyyy", { locale });
+ case TalerCorebankApi.MonitorTimeframeParam.decade:
+ return format(date.t_ms, "yyyy", { locale });
+ }
+ assertUnreachable(timeframe);
+}
+
+export function getTimeframesForDate(
+ time: Date,
+ timeframe: TalerCorebankApi.MonitorTimeframeParam,
+): { current: AbsoluteTime; previous: AbsoluteTime } {
+ switch (timeframe) {
+ case TalerCorebankApi.MonitorTimeframeParam.hour:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { hours: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { hours: 2 }).getTime(),
+ ),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.day:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { days: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { days: 4 }).getTime(),
+ ),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.month:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { months: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { months: 2 }).getTime(),
+ ),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.year:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 1 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 2 }).getTime(),
+ ),
+ };
+ case TalerCorebankApi.MonitorTimeframeParam.decade:
+ return {
+ current: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 10 }).getTime(),
+ ),
+ previous: AbsoluteTime.fromMilliseconds(
+ sub(time, { years: 20 }).getTime(),
+ ),
+ };
+ 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") {
+ switch (resp.current.case) {
+ case HttpStatusCode.BadRequest:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the current stats failed`}
+ >
+ <i18n.Translate>The request parameters are wrong</i18n.Translate>
+ </Attention>
+ );
+ case HttpStatusCode.Unauthorized:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the current stats failed`}
+ >
+ <i18n.Translate>The user is unauthorized</i18n.Translate>
+ </Attention>
+ );
+ default: {
+ assertUnreachable(resp.current);
+ }
+ }
+ }
+ if (resp.previous.type !== "ok") {
+ switch (resp.previous.case) {
+ case HttpStatusCode.BadRequest:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the previous stats failed`}
+ >
+ <i18n.Translate>The request parameters are wrong</i18n.Translate>
+ </Attention>
+ );
+ case HttpStatusCode.Unauthorized:
+ return (
+ <Attention
+ type="warning"
+ title={i18n.str`Querying for the previous stats failed`}
+ >
+ <i18n.Translate>The user is unauthorized</i18n.Translate>
+ </Attention>
+ );
+ default: {
+ assertUnreachable(resp.previous);
+ }
+ }
+ }
+ return (
+ <div class="px-4 mt-4">
+ <div class="sm:flex sm:items-center mb-4">
+ <div class="sm:flex-auto">
+ <h1 class="text-base font-semibold leading-6 text-gray-900">
+ <i18n.Translate>Transaction volume report</i18n.Translate>
+ </h1>
+ </div>
+ </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) => {
+ setMetricType(
+ parseInt(e.currentTarget
+ .value, 10) 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 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-4">
+ <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>
+ </div>
+ );
+}
+
+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/bank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
new file mode 100644
index 000000000..68f39fb9f
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx
@@ -0,0 +1,226 @@
+/*
+ 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,
+ 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 "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+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 {
+ lib: { 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT: {
+ return notify({
+ type: "error",
+ title: i18n.str`Only the administrator can change the minimum cashout limit.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ 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-6 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/bank-ui/src/pages/admin/DownloadStats.tsx b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
new file mode 100644
index 000000000..8f6bb7c23
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/DownloadStats.tsx
@@ -0,0 +1,588 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+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 {
+ lib: { 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-6 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,
+ date: 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,
+ date: 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/bank-ui/src/pages/admin/RemoveAccount.tsx b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
new file mode 100644
index 000000000..dbeebf719
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx
@@ -0,0 +1,273 @@
+/*
+ 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 "@gnu-taler/web-util/browser";
+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 "@gnu-taler/web-util/browser";
+
+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 {
+ lib: { 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,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotFound:
+ return notify({
+ type: "error",
+ title: i18n.str`The username was not found.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return notify({
+ type: "error",
+ title: i18n.str`Can't delete a reserved username.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO:
+ return notify({
+ type: "error",
+ title: i18n.str`Can't delete an account with balance different than zero.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.Accepted: {
+ updateBankState("currentChallenge", {
+ 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-6 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/bank-ui/src/pages/index.stories.tsx b/packages/bank-ui/src/pages/index.stories.tsx
new file mode 100644
index 000000000..823def5d7
--- /dev/null
+++ b/packages/bank-ui/src/pages/index.stories.tsx
@@ -0,0 +1,20 @@
+/*
+ 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/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
new file mode 100644
index 000000000..485ef5490
--- /dev/null
+++ b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx
@@ -0,0 +1,1170 @@
+/*
+ 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,
+ 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 "@gnu-taler/web-util/browser";
+import { useSessionState } from "../../hooks/session.js";
+import {
+ TransferCalculation,
+ useCashinEstimator,
+ useCashoutEstimator,
+ useConversionInfo,
+} from "../../hooks/regional.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+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({
+ 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 function afterComponentLoads() {
+ const { i18n } = useTranslationContext();
+
+ const {
+ lib: { conversion },
+ } = 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,
+ when: AbsoluteTime.now(),
+ });
+ }
+ case HttpStatusCode.NotImplemented: {
+ return notify({
+ type: "error",
+ title: i18n.str`Conversion is disabled`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ }
+ 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-6 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={`${id}_min_amount`}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Minimum amount`}</label>
+ <InputAmount
+ name={`${id}_min_amount`}
+ left
+ currency={inputCurrency}
+ value={minimum?.value ?? ""}
+ onChange={minimum?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={minimum?.error}
+ isDirty={minimum?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Only cashout operation above this threshold will be allowed
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="px-6 pt-6">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for={`${id}_ratio`}
+ >
+ {i18n.str`Ratio`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="number"
+ class="block rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="current"
+ id={`${id}_ratio`}
+ data-error={!!ratio?.error && ratio?.value !== undefined}
+ value={ratio?.value ?? ""}
+ onChange={(e) => {
+ ratio?.onUpdate(e.currentTarget.value);
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={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={`${id}_tiny_amount`}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Rounding value`}</label>
+ <InputAmount
+ name={`${id}_tiny_amount`}
+ left
+ currency={outputCurrency}
+ value={tiny?.value ?? ""}
+ onChange={tiny?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={tiny?.error}
+ isDirty={tiny?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Smallest difference between two amounts after the ratio is
+ applied.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div class="px-6 pt-6">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for={`${id}_channel`}
+ >
+ {i18n.str`Rounding mode`}
+ </label>
+ <div class="mt-2 max-w-xl text-sm text-gray-500">
+ <div class="px-4 mt-4 grid grid-cols-1 gap-y-6">
+ <label
+ onClick={(e) => {
+ e.preventDefault();
+ rounding?.onUpdate("zero");
+ }}
+ data-selected={rounding?.value === "zero"}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="channel"
+ value="Newsletter"
+ class="sr-only"
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Zero</i18n.Translate>
+ </span>
+ <i18n.Translate>
+ Amount will be round below to the largest possible value
+ smaller than the input.
+ </i18n.Translate>
+ </span>
+ </span>
+ <svg
+ data-selected={rounding?.value === "zero"}
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </label>
+
+ <label
+ onClick={(e) => {
+ e.preventDefault();
+ rounding?.onUpdate("up");
+ }}
+ data-selected={rounding?.value === "up"}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="channel"
+ value="Existing Customers"
+ class="sr-only"
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Up</i18n.Translate>
+ </span>
+ <i18n.Translate>
+ Amount will be round up to the smallest possible value
+ larger than the input.
+ </i18n.Translate>
+ </span>
+ </span>
+ <svg
+ data-selected={rounding?.value === "up"}
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </label>
+ <label
+ onClick={(e) => {
+ e.preventDefault();
+ rounding?.onUpdate("nearest");
+ }}
+ data-selected={rounding?.value === "nearest"}
+ class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"
+ >
+ <input
+ type="radio"
+ name="channel"
+ value="Existing Customers"
+ class="sr-only"
+ />
+ <span class="flex flex-1">
+ <span class="flex flex-col">
+ <span class="block text-sm font-medium text-gray-900 ">
+ <i18n.Translate>Nearest</i18n.Translate>
+ </span>
+ <i18n.Translate>
+ Amount will be round to the closest possible value.
+ </i18n.Translate>
+ </span>
+ </span>
+ <svg
+ data-selected={rounding?.value === "nearest"}
+ class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden"
+ viewBox="0 0 20 20"
+ fill="currentColor"
+ aria-hidden="true"
+ >
+ <path
+ fill-rule="evenodd"
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
+ clip-rule="evenodd"
+ />
+ </svg>
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="px-6 pt-4">
+ <Attention title={i18n.str`Examples`}>
+ <section class="grid grid-cols-1 gap-y-3 text-gray-600">
+ <details class="group text-sm">
+ <summary class="flex cursor-pointer flex-row items-center justify-between ">
+ <i18n.Translate>
+ Rounding an amount of 1.24 with rounding value 0.1
+ </i18n.Translate>
+ <svg
+ 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={`${id}_fee`}
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Fee`}</label>
+ <InputAmount
+ name={`${id}_fee`}
+ left
+ currency={outputCurrency}
+ value={fee?.value ?? ""}
+ onChange={fee?.onUpdate}
+ />
+ <ShowInputErrorLabel
+ message={fee?.error}
+ isDirty={fee?.value !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Amount to be deducted before amount is credited.
+ </i18n.Translate>
+ </p>
+ </div>
+ </div>
+ </div>
+ </Fragment>
+ );
+}
diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
new file mode 100644
index 000000000..c51b96b8b
--- /dev/null
+++ b/packages/bank-ui/src/pages/regional/CreateCashout.tsx
@@ -0,0 +1,732 @@
+/*
+ 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 { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+import { useAccountDetails } from "../../hooks/account.js";
+import { useBankState } from "../../hooks/bank-state.js";
+import {
+ TransferCalculation,
+ useCashoutEstimator,
+ useConversionInfo,
+} from "../../hooks/regional.js";
+import { useSessionState } from "../../hooks/session.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+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;
+ onCashout: () => 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,
+ onCashout,
+ 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 {
+ lib: { bank: api },
+ config,
+ } = 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>
+ );
+ }
+
+ 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 {
+ 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 account = {
+ balance: Amounts.parseOrThrow(resultAccount.body.balance.amount),
+ balanceIsDebit:
+ resultAccount.body.balance.credit_debit_indicator == "debit",
+ debitThreshold: Amounts.parseOrThrow(resultAccount.body.debit_threshold),
+ minCashout: resultAccount.body.min_cashout === undefined ? regionalZero : Amounts.parseOrThrow(resultAccount.body.min_cashout)
+ };
+
+ 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`
+ : calculationResult === "amount-is-too-small"
+ ? i18n.str`Amount needs to be higher`
+ : Amounts.cmp(calc.debit, conversionInfo.cashout_min_amount) < 0
+ ? i18n.str`No account can't cashout less than ${
+ Amounts.stringifyValueWithSpec(
+ Amounts.parseOrThrow(conversionInfo.cashout_min_amount),
+ regional_currency_specification,
+ ).normal
+ }`
+ : Amounts.cmp(calc.debit, account.minCashout) < 0
+ ? i18n.str`Your account can't cashout less than ${
+ Amounts.stringifyValueWithSpec(
+ Amounts.parseOrThrow(account.minCashout),
+ regional_currency_specification,
+ ).normal
+ }`
+
+ : Amounts.isZero(calc.credit)
+ ? i18n.str`The total transfer at destination will be zero`
+ : undefined,
+ });
+ const trimmedAmountStr = form.amount?.trim();
+
+ async function createCashout() {
+ const request_uid = encodeCrock(getRandomBytes(32));
+ await handleError(async () => {
+ if (!creds || !form.subject) return;
+ const request = {
+ request_uid,
+ amount_credit: Amounts.stringify(calc.credit),
+ amount_debit: Amounts.stringify(calc.debit),
+ subject: form.subject,
+ };
+ const resp = await api.createCashout(creds, request);
+ if (resp.type === "ok") {
+ notifyInfo(i18n.str`Cashout created`);
+ onCashout();
+ } 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ case HttpStatusCode.NotImplemented:
+ return notify({
+ type: "error",
+ title: i18n.str`Cashout are disabled`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return notify({
+ type: "error",
+ title: i18n.str`Missing cashout URI in the profile`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+ case TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL:
+ return notify({
+ type: "error",
+ title: i18n.str`The amount is less than the minimum allowed.`,
+ description: resp.detail.hint as TranslatedString,
+ debug: resp.detail,
+ when: AbsoluteTime.now(),
+ });
+
+ 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,
+ when: AbsoluteTime.now(),
+ });
+ }
+ 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-6 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>
+ )}
+ </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/bank-ui/src/pages/regional/ShowCashoutDetails.tsx b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
new file mode 100644
index 000000000..aba00ad7a
--- /dev/null
+++ b/packages/bank-ui/src/pages/regional/ShowCashoutDetails.tsx
@@ -0,0 +1,194 @@
+/*
+ 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,
+ assertUnreachable,
+} from "@gnu-taler/taler-util";
+import {
+ Attention,
+ Loading,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+import { ErrorLoadingWithDebug } from "../../components/ErrorLoadingWithDebug.js";
+import { Time } from "../../components/Time.js";
+import { useCashoutDetails, useConversionInfo } from "../../hooks/regional.js";
+import { RouteDefinition } from "@gnu-taler/web-util/browser";
+import { RenderAmount } from "../PaytoWireTransferForm.js";
+
+interface Props {
+ id: string;
+ routeClose: RouteDefinition;
+}
+export function ShowCashoutDetails({ id, routeClose }: Props): VNode {
+ const { i18n } = 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-6 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/bank-ui/src/pages/rnd.ts b/packages/bank-ui/src/pages/rnd.ts
new file mode 100644
index 000000000..d66fb005b
--- /dev/null
+++ b/packages/bank-ui/src/pages/rnd.ts
@@ -0,0 +1,2907 @@
+/*
+ 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));
+}
diff --git a/packages/bank-ui/src/scss/main.css b/packages/bank-ui/src/scss/main.css
new file mode 100644
index 000000000..b5c61c956
--- /dev/null
+++ b/packages/bank-ui/src/scss/main.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/bank-ui/src/settings.json b/packages/bank-ui/src/settings.json
new file mode 100644
index 000000000..df5fe75ce
--- /dev/null
+++ b/packages/bank-ui/src/settings.json
@@ -0,0 +1,11 @@
+{
+ "backendBaseURL": "http://bank.taler.test:1180/",
+ "simplePasswordForRandomAccounts": true,
+ "allowRandomAccountCreation": true,
+ "bankName": "Taler DEVELOPMENT Bank",
+ "topNavSites": {
+ "Exchange": "http://Exchnage.taler.test:1180/",
+ "Bank": "http://bank-ui.taler.test:1180/",
+ "Merchant": "http://merchant.taler.test:1180/"
+ }
+}
diff --git a/packages/bank-ui/src/settings.ts b/packages/bank-ui/src/settings.ts
new file mode 100644
index 000000000..c085c7cd8
--- /dev/null
+++ b/packages/bank-ui/src/settings.ts
@@ -0,0 +1,112 @@
+/*
+ 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 {
+ Codec,
+ buildCodecForObject,
+ canonicalizeBaseUrl,
+ codecForBoolean,
+ codecForMap,
+ codecForString,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+
+export interface UiSettings {
+ // Where libeufin backend is localted
+ // default: window.origin without "webui/"
+ backendBaseURL?: string;
+ // Shows a button "create random account" in the registration form
+ // Useful for testing
+ // default: false
+ allowRandomAccountCreation?: boolean;
+ // Create all random accounts with password "123"
+ // Useful for testing
+ // default: false
+ simplePasswordForRandomAccounts?: boolean;
+ // URL where the user is going to be redirected after
+ // clicking in Taler Logo
+ // default: home page
+ iconLinkURL?: string;
+ // Mapping for every link shown in the top navitation bar
+ // - key: link label, what the user will read
+ // - value: link target, where the user is going to be redirected
+ // default: empty list
+ topNavSites?: Record<string, string>;
+}
+
+/**
+ * Global settings for the bank UI.
+ */
+const defaultSettings: UiSettings = {
+ backendBaseURL: buildDefaultBackendBaseURL(),
+ iconLinkURL: undefined,
+ simplePasswordForRandomAccounts: false,
+ allowRandomAccountCreation: false,
+ topNavSites: {},
+};
+
+const codecForUISettings = (): Codec<UiSettings> =>
+ buildCodecForObject<UiSettings>()
+ .property("backendBaseURL", codecOptional(codecForString()))
+ .property("allowRandomAccountCreation", codecOptional(codecForBoolean()))
+ .property(
+ "simplePasswordForRandomAccounts",
+ codecOptional(codecForBoolean()),
+ )
+ .property("iconLinkURL", codecOptional(codecForString()))
+ .property("topNavSites", codecOptional(codecForMap(codecForString())))
+ .build("UiSettings");
+
+function removeUndefineField<T extends object>(obj: T): T {
+ const keys = Object.keys(obj) as Array<keyof T>;
+ return keys.reduce((prev, cur) => {
+ if (typeof prev[cur] === "undefined") {
+ delete prev[cur];
+ }
+ return prev;
+ }, obj);
+}
+
+export function fetchSettings(listener: (s: UiSettings) => void): void {
+ fetch("./settings.json")
+ .then((resp) => resp.json())
+ .then((json) => codecForUISettings().decode(json))
+ .then((result) =>
+ listener({
+ ...defaultSettings,
+ ...removeUndefineField(result),
+ }),
+ )
+ .catch((e) => {
+ console.log("failed to fetch settings", e);
+ listener(defaultSettings);
+ });
+}
+
+function buildDefaultBackendBaseURL(): string | undefined {
+ if (typeof window !== "undefined") {
+ const currentLocation = new URL(
+ window.location.pathname,
+ window.location.origin,
+ ).href;
+ /**
+ * By default, bank backend serves the html content
+ * from the /webui root.
+ */
+ return canonicalizeBaseUrl(currentLocation.replace("/webui", ""));
+ }
+ throw Error("No default URL");
+}
diff --git a/packages/bank-ui/src/stories.test.ts b/packages/bank-ui/src/stories.test.ts
new file mode 100644
index 000000000..921f9f9ea
--- /dev/null
+++ b/packages/bank-ui/src/stories.test.ts
@@ -0,0 +1,83 @@
+/*
+ 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 {
+ AmountString,
+ TalerCorebankApi,
+ setupI18n,
+} from "@gnu-taler/taler-util";
+import {
+ BankApiProviderTesting,
+ parseGroupImport,
+} from "@gnu-taler/web-util/browser";
+import * as tests from "@gnu-taler/web-util/testing";
+import * as components from "./components/index.examples.js";
+import * as pages from "./pages/index.stories.js";
+
+import { ComponentChildren, VNode, h as create } from "preact";
+// import { BankCoreApiProviderTesting } from "./context/config.js";
+
+setupI18n("en", { en: {} });
+
+describe("All the examples:", () => {
+ const cms = parseGroupImport({ pages, components });
+ cms.forEach((group) => {
+ describe(`Example for group "${group.title}:"`, () => {
+ group.list.forEach((component) => {
+ describe(`Component ${component.name}:`, () => {
+ component.examples.forEach((example) => {
+ it(`should render example: ${example.name}`, () => {
+ tests.renderUI(example.render, DefaultTestingContext);
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+function DefaultTestingContext(_props: { children: ComponentChildren }): VNode {
+ const cfg: TalerCorebankApi.Config = {
+ name: "libeufin-bank",
+ allow_deletions: true,
+ bank_name: "taler bank",
+ wire_type: "wire t",
+ supported_tan_channels: [],
+ allow_registrations: true,
+ allow_conversion: true,
+ allow_edit_cashout_payto_uri: false,
+ allow_edit_name: false,
+ currency: "ASR",
+ currency_specification: {
+ name: "ARS",
+ alt_unit_names: {},
+ num_fractional_input_digits: 2,
+ num_fractional_normal_digits: 2,
+ num_fractional_trailing_zero_digits: 2,
+ },
+ default_debit_threshold: "ARS:10" as AmountString,
+ version: "1:0:0",
+ };
+ const ctx2 = create(BankApiProviderTesting, {
+ children: [],
+ value: cfg as any,
+ });
+ return ctx2;
+}
diff --git a/packages/bank-ui/src/stories.tsx b/packages/bank-ui/src/stories.tsx
new file mode 100644
index 000000000..8342a8434
--- /dev/null
+++ b/packages/bank-ui/src/stories.tsx
@@ -0,0 +1,41 @@
+/*
+ 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 { strings } from "./i18n/strings.js";
+
+import * as pages from "./pages/index.stories.js";
+import * as components from "./components/index.examples.js";
+
+import { renderStories } from "@gnu-taler/web-util/browser";
+
+function main(): void {
+ renderStories(
+ { pages, components },
+ {
+ strings,
+ },
+ );
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/bank-ui/src/utils.ts b/packages/bank-ui/src/utils.ts
new file mode 100644
index 000000000..2cc502416
--- /dev/null
+++ b/packages/bank-ui/src/utils.ts
@@ -0,0 +1,439 @@
+/*
+ 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,
+ AmountString,
+ PaytoString,
+ TalerError,
+ TalerErrorCode,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ ErrorNotification,
+ InternationalizationAPI,
+ notify,
+ notifyError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+
+/**
+ * Validate (the number part of) an amount. If needed,
+ * replace comma with a dot. Returns 'false' whenever
+ * the input is invalid, the valid amount otherwise.
+ */
+const amountRegex = /^[0-9]+(.[0-9]+)?$/;
+export function validateAmount(
+ maybeAmount: string | undefined,
+): string | undefined {
+ if (!maybeAmount || !amountRegex.test(maybeAmount)) {
+ return;
+ }
+ return maybeAmount;
+}
+
+/**
+ * Extract IBAN from a Payto URI.
+ */
+export function getIbanFromPayto(url: string): string {
+ const pathSplit = new URL(url).pathname.split("/");
+ let lastIndex = pathSplit.length - 1;
+ // Happens if the path ends with "/".
+ if (pathSplit[lastIndex] === "") lastIndex--;
+ const iban = pathSplit[lastIndex];
+ return iban;
+}
+
+export function undefinedIfEmpty<T extends object>(obj: T): T | undefined {
+ return Object.keys(obj).some(
+ (k) => (obj as Record<string, T>)[k] !== undefined,
+ )
+ ? obj
+ : undefined;
+}
+
+export type PartialButDefined<T> = {
+ [P in keyof T]: T[P] | undefined;
+};
+
+/**
+ * every non-map field can be undefined
+ */
+export type WithIntermediate<Type> = {
+ [prop in keyof Type]: Type[prop] extends PaytoString
+ ? Type[prop] | undefined
+ : Type[prop] extends AmountString
+ ? Type[prop] | undefined
+ : Type[prop] extends TranslatedString
+ ? Type[prop] | undefined
+ : Type[prop] extends object
+ ? WithIntermediate<Type[prop]>
+ : Type[prop] | undefined;
+};
+export type RecursivePartial<Type> = {
+ [P in keyof Type]?: Type[P] extends (infer U)[]
+ ? RecursivePartial<U>[]
+ : Type[P] extends object
+ ? RecursivePartial<Type[P]>
+ : Type[P];
+};
+export type ErrorMessageMappingFor<Type> = {
+ [prop in keyof Type]+?: Exclude<Type[prop], undefined> extends PaytoString // enumerate known object
+ ? TranslatedString
+ : Exclude<Type[prop], undefined> extends AmountString
+ ? TranslatedString
+ : Exclude<Type[prop], undefined> extends TranslatedString
+ ? TranslatedString
+ : // arrays: every element
+ Exclude<Type[prop], undefined> extends (infer U)[]
+ ? ErrorMessageMappingFor<U>[]
+ : // map: every field
+ Exclude<Type[prop], undefined> extends object
+ ? ErrorMessageMappingFor<Type[prop]>
+ : TranslatedString;
+};
+
+export enum TanChannel {
+ SMS = "sms",
+ EMAIL = "email",
+}
+export enum CashoutStatus {
+ // The payment was initiated after a valid
+ // TAN was received by the bank.
+ CONFIRMED = "confirmed",
+
+ // The cashout was created and now waits
+ // for the TAN by the author.
+ PENDING = "pending",
+}
+
+
+export const PAGINATED_LIST_SIZE = 5;
+// when doing paginated request, ask for one more
+// and use it to know if there are more to request
+export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1;
+
+type Translator = ReturnType<typeof useTranslationContext>["i18n"];
+
+export async function withRuntimeErrorHandling<T>(
+ i18n: Translator,
+ cb: () => Promise<T>,
+): Promise<void> {
+ try {
+ await cb();
+ } catch (error: unknown) {
+ if (error instanceof TalerError) {
+ notify(buildRequestErrorMessage(i18n, error));
+ } else {
+ notifyError(
+ i18n.str`Operation failed, please report`,
+ (error instanceof Error
+ ? error.message
+ : JSON.stringify(error)) as TranslatedString,
+ );
+ }
+ }
+}
+
+export function buildRequestErrorMessage(
+ i18n: Translator,
+ cause: TalerError,
+): ErrorNotification {
+ let result: ErrorNotification;
+ switch (cause.errorDetail.code) {
+ case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: {
+ result = {
+ type: "error",
+ title: i18n.str`Request timeout`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: {
+ result = {
+ type: "error",
+ title: i18n.str`Request throttled`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: {
+ result = {
+ type: "error",
+ title: i18n.str`Malformed response`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_NETWORK_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Network error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected request error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ default: {
+ result = {
+ type: "error",
+ title: i18n.str`Unexpected error`,
+ description: cause.message as TranslatedString,
+ debug: JSON.stringify(cause.errorDetail, undefined, 2),
+ when: AbsoluteTime.now(),
+ };
+ break;
+ }
+ }
+ return result;
+}
+
+export const COUNTRY_TABLE = {
+ AE: "U.A.E.",
+ AF: "Afghanistan",
+ AL: "Albania",
+ AM: "Armenia",
+ AN: "Netherlands Antilles",
+ AR: "Argentina",
+ AT: "Austria",
+ AU: "Australia",
+ AZ: "Azerbaijan",
+ BA: "Bosnia and Herzegovina",
+ BD: "Bangladesh",
+ BE: "Belgium",
+ BG: "Bulgaria",
+ BH: "Bahrain",
+ BN: "Brunei Darussalam",
+ BO: "Bolivia",
+ BR: "Brazil",
+ BT: "Bhutan",
+ BY: "Belarus",
+ BZ: "Belize",
+ CA: "Canada",
+ CG: "Congo",
+ CH: "Switzerland",
+ CI: "Cote d'Ivoire",
+ CL: "Chile",
+ CM: "Cameroon",
+ CN: "People's Republic of China",
+ CO: "Colombia",
+ CR: "Costa Rica",
+ CS: "Serbia and Montenegro",
+ CZ: "Czech Republic",
+ DE: "Germany",
+ DK: "Denmark",
+ DO: "Dominican Republic",
+ DZ: "Algeria",
+ EC: "Ecuador",
+ EE: "Estonia",
+ EG: "Egypt",
+ ER: "Eritrea",
+ ES: "Spain",
+ ET: "Ethiopia",
+ FI: "Finland",
+ FO: "Faroe Islands",
+ FR: "France",
+ GB: "United Kingdom",
+ GD: "Caribbean",
+ GE: "Georgia",
+ GL: "Greenland",
+ GR: "Greece",
+ GT: "Guatemala",
+ HK: "Hong Kong",
+ // HK: "Hong Kong S.A.R.",
+ HN: "Honduras",
+ HR: "Croatia",
+ HT: "Haiti",
+ HU: "Hungary",
+ ID: "Indonesia",
+ IE: "Ireland",
+ IL: "Israel",
+ IN: "India",
+ IQ: "Iraq",
+ IR: "Iran",
+ IS: "Iceland",
+ IT: "Italy",
+ JM: "Jamaica",
+ JO: "Jordan",
+ JP: "Japan",
+ KE: "Kenya",
+ KG: "Kyrgyzstan",
+ KH: "Cambodia",
+ KR: "South Korea",
+ KW: "Kuwait",
+ KZ: "Kazakhstan",
+ LA: "Laos",
+ LB: "Lebanon",
+ LI: "Liechtenstein",
+ LK: "Sri Lanka",
+ LT: "Lithuania",
+ LU: "Luxembourg",
+ LV: "Latvia",
+ LY: "Libya",
+ MA: "Morocco",
+ MC: "Principality of Monaco",
+ MD: "Moldava",
+ // MD: "Moldova",
+ ME: "Montenegro",
+ MK: "Former Yugoslav Republic of Macedonia",
+ ML: "Mali",
+ MM: "Myanmar",
+ MN: "Mongolia",
+ MO: "Macau S.A.R.",
+ MT: "Malta",
+ MV: "Maldives",
+ MX: "Mexico",
+ MY: "Malaysia",
+ NG: "Nigeria",
+ NI: "Nicaragua",
+ NL: "Netherlands",
+ NO: "Norway",
+ NP: "Nepal",
+ NZ: "New Zealand",
+ OM: "Oman",
+ PA: "Panama",
+ PE: "Peru",
+ PH: "Philippines",
+ PK: "Islamic Republic of Pakistan",
+ PL: "Poland",
+ PR: "Puerto Rico",
+ PT: "Portugal",
+ PY: "Paraguay",
+ QA: "Qatar",
+ RE: "Reunion",
+ RO: "Romania",
+ RS: "Serbia",
+ RU: "Russia",
+ RW: "Rwanda",
+ SA: "Saudi Arabia",
+ SE: "Sweden",
+ SG: "Singapore",
+ SI: "Slovenia",
+ SK: "Slovak",
+ SN: "Senegal",
+ SO: "Somalia",
+ SR: "Suriname",
+ SV: "El Salvador",
+ SY: "Syria",
+ TH: "Thailand",
+ TJ: "Tajikistan",
+ TM: "Turkmenistan",
+ TN: "Tunisia",
+ TR: "Turkey",
+ TT: "Trinidad and Tobago",
+ TW: "Taiwan",
+ TZ: "Tanzania",
+ UA: "Ukraine",
+ US: "United States",
+ UY: "Uruguay",
+ VA: "Vatican",
+ VE: "Venezuela",
+ VN: "Viet Nam",
+ YE: "Yemen",
+ ZA: "South Africa",
+ ZW: "Zimbabwe",
+};
+
+/**
+ * An IBAN is validated by converting it into an integer and performing a
+ * basic mod-97 operation (as described in ISO 7064) on it.
+ * If the IBAN is valid, the remainder equals 1.
+ *
+ * The algorithm of IBAN validation is as follows:
+ * 1.- Check that the total IBAN length is correct as per the country. If not, the IBAN is invalid
+ * 2.- Move the four initial characters to the end of the string
+ * 3.- Replace each letter in the string with two digits, thereby expanding the string, where A = 10, B = 11, ..., Z = 35
+ * 4.- Interpret the string as a decimal integer and compute the remainder of that number on division by 97
+ *
+ * If the remainder is 1, the check digit test is passed and the IBAN might be valid.
+ *
+ */
+const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
+export function validateIBAN(
+ account: string,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+ if (!IBAN_REGEX.test(account)) {
+ return i18n.str`IBAN only have uppercased letters and numbers`;
+ }
+ // Check total length
+ if (account.length < 4) return i18n.str`IBAN numbers have more that 4 digits`;
+ if (account.length > 34)
+ return i18n.str`IBAN numbers have less that 34 digits`;
+
+ const A_code = "A".charCodeAt(0);
+ const Z_code = "Z".charCodeAt(0);
+ const IBAN = account.toUpperCase();
+ // check supported country
+ const code = IBAN.substring(0, 2);
+ const found = code in COUNTRY_TABLE;
+ if (!found) return i18n.str`IBAN country code not found`;
+
+ // 2.- Move the four initial characters to the end of the string
+ const step2 = IBAN.substring(4) + account.substring(0, 4);
+ const step3 = Array.from(step2)
+ .map((letter) => {
+ const code = letter.charCodeAt(0);
+ if (code < A_code || code > Z_code) return letter;
+ return `${letter.charCodeAt(0) - "A".charCodeAt(0) + 10}`;
+ })
+ .join("");
+
+ const checksum = calculate_iban_checksum(step3);
+ if (checksum !== 1)
+ return i18n.str`IBAN number is not valid, checksum is wrong`;
+ return undefined;
+}
+
+function calculate_iban_checksum(str: string): number {
+ const numberStr = str.substring(0, 5);
+ const rest = str.substring(5);
+ const number = parseInt(numberStr, 10);
+ const result = number % 97;
+ if (rest.length > 0) {
+ return calculate_iban_checksum(`${result}${rest}`);
+ }
+ return result;
+}
+
+const USERNAME_REGEX = /^[A-Za-z][A-Za-z0-9]*$/;
+
+export function validateTalerBank(
+ account: string,
+ i18n: InternationalizationAPI,
+): TranslatedString | undefined {
+ if (!USERNAME_REGEX.test(account)) {
+ return i18n.str`Account only have letters and numbers`;
+ }
+ return undefined;
+}