aboutsummaryrefslogtreecommitdiff
path: root/packages/demobank-ui/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'packages/demobank-ui/src/pages')
-rw-r--r--packages/demobank-ui/src/pages/AccountPage.tsx283
-rw-r--r--packages/demobank-ui/src/pages/AdminPage.tsx707
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx42
-rw-r--r--packages/demobank-ui/src/pages/HomePage.tsx149
-rw-r--r--packages/demobank-ui/src/pages/LoginForm.tsx188
-rw-r--r--packages/demobank-ui/src/pages/PaymentOptions.tsx33
-rw-r--r--packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx317
-rw-r--r--packages/demobank-ui/src/pages/PublicHistoriesPage.tsx93
-rw-r--r--packages/demobank-ui/src/pages/QrCodeSection.tsx9
-rw-r--r--packages/demobank-ui/src/pages/RegistrationPage.tsx176
-rw-r--r--packages/demobank-ui/src/pages/Routing.tsx84
-rw-r--r--packages/demobank-ui/src/pages/WalletWithdrawForm.tsx259
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx466
-rw-r--r--packages/demobank-ui/src/pages/WithdrawalQRCode.tsx111
14 files changed, 1787 insertions, 1130 deletions
diff --git a/packages/demobank-ui/src/pages/AccountPage.tsx b/packages/demobank-ui/src/pages/AccountPage.tsx
index 8d29bd933..769e85804 100644
--- a/packages/demobank-ui/src/pages/AccountPage.tsx
+++ b/packages/demobank-ui/src/pages/AccountPage.tsx
@@ -14,206 +14,52 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, HttpStatusCode, Logger } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { useEffect } from "preact/hooks";
-import useSWR, { SWRConfig, useSWRConfig } from "swr";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import { BackendInfo } from "../hooks/backend.js";
-import { bankUiSettings } from "../settings.js";
-import { getIbanFromPayto, prepareHeaders } from "../utils.js";
-import { BankFrame } from "./BankFrame.js";
-import { LoginForm } from "./LoginForm.js";
-import { PaymentOptions } from "./PaymentOptions.js";
+import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
+import {
+ HttpResponsePaginated,
+ useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Cashouts } from "../components/Cashouts/index.js";
import { Transactions } from "../components/Transactions/index.js";
-import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
-
-export function AccountPage(): VNode {
- const backend = useBackendContext();
- const { i18n } = useTranslationContext();
-
- if (backend.state.status === "loggedOut") {
- return (
- <BankFrame>
- <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
- <LoginForm />
- </BankFrame>
- );
- }
-
- return (
- <SWRWithCredentials info={backend.state}>
- <Account accountLabel={backend.state.username} />
- </SWRWithCredentials>
- );
-}
-
-/**
- * Factor out login credentials.
- */
-function SWRWithCredentials({
- children,
- info,
-}: {
- children: ComponentChildren;
- info: BackendInfo;
-}): VNode {
- const { username, password, url: backendUrl } = info;
- const headers = prepareHeaders(username, password);
- return (
- <SWRConfig
- value={{
- fetcher: (url: string) => {
- return fetch(new URL(url, backendUrl).href, { headers }).then((r) => {
- if (!r.ok) throw { status: r.status, json: r.json() };
+import { useAccountDetails } from "../hooks/access.js";
+import { PaymentOptions } from "./PaymentOptions.js";
- return r.json();
- });
- },
- }}
- >
- {children as any}
- </SWRConfig>
- );
+interface Props {
+ account: string;
+ onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
}
-
-const logger = new Logger("AccountPage");
-
/**
- * Show only the account's balance. NOTE: the backend state
- * is mostly needed to provide the user's credentials to POST
- * to the bank.
+ * Query account information and show QR code if there is pending withdrawal
*/
-function Account({ accountLabel }: { accountLabel: string }): VNode {
- const { cache } = useSWRConfig();
-
- // Getting the bank account balance:
- const endpoint = `access-api/accounts/${accountLabel}`;
- const { data, error, mutate } = useSWR(endpoint, {
- // refreshInterval: 0,
- // revalidateIfStale: false,
- // revalidateOnMount: false,
- // revalidateOnFocus: false,
- // revalidateOnReconnect: false,
- });
- const backend = useBackendContext();
- const { pageState, pageStateSetter: setPageState } = usePageContext();
- const { withdrawalId, talerWithdrawUri, timestamp } = pageState;
+export function AccountPage({ account, onLoadNotOk }: Props): VNode {
+ const result = useAccountDetails(account);
const { i18n } = useTranslationContext();
- useEffect(() => {
- mutate();
- }, [timestamp]);
- /**
- * This part shows a list of transactions: with 5 elements by
- * default and offers a "load more" button.
- */
- // const [txPageNumber, setTxPageNumber] = useTransactionPageNumber();
- // const txsPages = [];
- // for (let i = 0; i <= txPageNumber; i++) {
- // txsPages.push(<Transactions accountLabel={accountLabel} pageNumber={i} />);
- // }
-
- if (typeof error !== "undefined") {
- logger.error("account error", error, endpoint);
- /**
- * FIXME: to minimize the code, try only one invocation
- * of pageStateSetter, after having decided the error
- * message in the case-branch.
- */
- switch (error.status) {
- case 404: {
- backend.clear();
- setPageState((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Username or account label '${accountLabel}' not found. Won't login.`,
- },
- }));
-
- /**
- * 404 should never stick to the cache, because they
- * taint successful future registrations. How? After
- * registering, the user gets navigated to this page,
- * therefore a previous 404 on this SWR key (the requested
- * resource) would still appear as valid and cause this
- * page not to be shown! A typical case is an attempted
- * login of a unregistered user X, and then a registration
- * attempt of the same user X: in this case, the failed
- * login would cache a 404 error to X's profile, resulting
- * in the legitimate request after the registration to still
- * be flagged as 404. Clearing the cache should prevent
- * this. */
- (cache as any).clear();
- return <p>Profile not found...</p>;
- }
- case HttpStatusCode.Unauthorized:
- case HttpStatusCode.Forbidden: {
- backend.clear();
- setPageState((prevState: PageStateType) => ({
- ...prevState,
- error: {
- title: i18n.str`Wrong credentials given.`,
- },
- }));
- return <p>Wrong credentials...</p>;
- }
- default: {
- backend.clear();
- setPageState((prevState: PageStateType) => ({
- ...prevState,
- error: {
- title: i18n.str`Account information could not be retrieved.`,
- debug: JSON.stringify(error),
- },
- }));
- return <p>Unknown problem...</p>;
- }
- }
+ if (!result.ok) {
+ return onLoadNotOk(result);
}
- const balance = !data ? undefined : Amounts.parse(data.balance.amount);
- const errorParsingBalance = data && !balance;
- const accountNumber = !data ? undefined : getIbanFromPayto(data.paytoUri);
- const balanceIsDebit = data && data.balance.credit_debit_indicator == "debit";
- /**
- * This block shows the withdrawal QR code.
- *
- * A withdrawal operation replaces everything in the page and
- * (ToDo:) starts polling the backend until either the wallet
- * selected a exchange and reserve public key, or a error / abort
- * happened.
- *
- * After reaching one of the above states, the user should be
- * brought to this ("Account") page where they get informed about
- * the outcome.
- */
- if (talerWithdrawUri && withdrawalId) {
- logger.trace("Bank created a new Taler withdrawal");
+ const { data } = result;
+ const balance = Amounts.parse(data.balance.amount);
+ const errorParsingBalance = !balance;
+ const payto = parsePaytoUri(data.paytoUri);
+ if (!payto || !payto.isKnown || payto.targetType !== "iban") {
return (
- <BankFrame>
- <WithdrawalQRCode
- withdrawalId={withdrawalId}
- talerWithdrawUri={talerWithdrawUri}
- />
- </BankFrame>
+ <div>Payto from server is not valid &quot;{data.paytoUri}&quot;</div>
);
}
- const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance);
+ const accountNumber = payto.iban;
+ const balanceIsDebit = data.balance.credit_debit_indicator == "debit";
return (
- <BankFrame>
+ <Fragment>
<div>
<h1 class="nav welcome-text">
<i18n.Translate>
Welcome,
- {accountNumber
- ? `${accountLabel} (${accountNumber})`
- : accountLabel}
- !
+ {accountNumber ? `${account} (${accountNumber})` : account}!
</i18n.Translate>
</h1>
</div>
@@ -239,7 +85,10 @@ function Account({ accountLabel }: { accountLabel: string }): VNode {
) : (
<div class="large-amount amount">
{balanceIsDebit ? <b>-</b> : null}
- <span class="value">{`${balanceValue}`}</span>&nbsp;
+ <span class="value">{`${Amounts.stringifyValue(
+ balance,
+ )}`}</span>
+ &nbsp;
<span class="currency">{`${balance.currency}`}</span>
</div>
)}
@@ -248,34 +97,56 @@ function Account({ accountLabel }: { accountLabel: string }): VNode {
<section id="payments">
<div class="payments">
<h2>{i18n.str`Payments`}</h2>
- <PaymentOptions currency={balance?.currency} />
+ <PaymentOptions currency={balance.currency} />
</div>
</section>
</Fragment>
)}
- <section id="main">
- <article>
- <h2>{i18n.str`Latest transactions:`}</h2>
- <Transactions
- balanceValue={balanceValue}
- pageNumber={0}
- accountLabel={accountLabel}
- />
- </article>
+
+ <section style={{ marginTop: "2em" }}>
+ <Moves account={account} />
</section>
- </BankFrame>
+ </Fragment>
);
}
-// function useTransactionPageNumber(): [number, StateUpdater<number>] {
-// const ret = useNotNullLocalStorage("transaction-page", "0");
-// const retObj = JSON.parse(ret[0]);
-// const retSetter: StateUpdater<number> = function (val) {
-// const newVal =
-// val instanceof Function
-// ? JSON.stringify(val(retObj))
-// : JSON.stringify(val);
-// ret[1](newVal);
-// };
-// return [retObj, retSetter];
-// }
+function Moves({ account }: { account: string }): VNode {
+ const [tab, setTab] = useState<"transactions" | "cashouts">("transactions");
+ const { i18n } = useTranslationContext();
+ return (
+ <article>
+ <div class="payments">
+ <div class="tab">
+ <button
+ class={tab === "transactions" ? "tablinks active" : "tablinks"}
+ onClick={(): void => {
+ setTab("transactions");
+ }}
+ >
+ {i18n.str`Transactions`}
+ </button>
+ <button
+ class={tab === "cashouts" ? "tablinks active" : "tablinks"}
+ onClick={(): void => {
+ setTab("cashouts");
+ }}
+ >
+ {i18n.str`Cashouts`}
+ </button>
+ </div>
+ {tab === "transactions" && (
+ <div class="active">
+ <h3>{i18n.str`Latest transactions`}</h3>
+ <Transactions account={account} />
+ </div>
+ )}
+ {tab === "cashouts" && (
+ <div class="active">
+ <h3>{i18n.str`Latest cashouts`}</h3>
+ <Cashouts account={account} />
+ </div>
+ )}
+ </div>
+ </article>
+ );
+}
diff --git a/packages/demobank-ui/src/pages/AdminPage.tsx b/packages/demobank-ui/src/pages/AdminPage.tsx
new file mode 100644
index 000000000..9efd37f12
--- /dev/null
+++ b/packages/demobank-ui/src/pages/AdminPage.tsx
@@ -0,0 +1,707 @@
+/*
+ This file is part of GNU Taler
+ (C) 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/>
+ */
+
+import { parsePaytoUri, TranslatedString } from "@gnu-taler/taler-util";
+import {
+ HttpResponsePaginated,
+ RequestError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { ErrorMessage, usePageContext } from "../context/pageState.js";
+import {
+ useAccountDetails,
+ useAccounts,
+ useAdminAccountAPI,
+} from "../hooks/circuit.js";
+import {
+ PartialButDefined,
+ undefinedIfEmpty,
+ WithIntermediate,
+} from "../utils.js";
+import { ErrorBanner } from "./BankFrame.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+
+const charset =
+ "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+const upperIdx = charset.indexOf("A");
+
+function randomPassword(): string {
+ const random = Array.from({ length: 16 }).map(() => {
+ return charset.charCodeAt(Math.random() * charset.length);
+ });
+ // first char can't be upper
+ const charIdx = charset.indexOf(String.fromCharCode(random[0]));
+ random[0] =
+ charIdx > upperIdx ? charset.charCodeAt(charIdx - upperIdx) : random[0];
+ return String.fromCharCode(...random);
+}
+
+interface Props {
+ onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+}
+/**
+ * Query account information and show QR code if there is pending withdrawal
+ */
+export function AdminPage({ onLoadNotOk }: Props): VNode {
+ const [account, setAccount] = useState<string | undefined>();
+ const [showDetails, setShowDetails] = useState<string | undefined>();
+ const [updatePassword, setUpdatePassword] = useState<string | undefined>();
+ const [createAccount, setCreateAccount] = useState(false);
+ const { pageStateSetter } = usePageContext();
+
+ function showInfoMessage(info: TranslatedString): void {
+ pageStateSetter((prev) => ({
+ ...prev,
+ info,
+ }));
+ }
+
+ const result = useAccounts({ account });
+ const { i18n } = useTranslationContext();
+
+ if (result.loading) return <div />;
+ if (!result.ok) {
+ return onLoadNotOk(result);
+ }
+
+ const { customers } = result.data;
+
+ if (showDetails) {
+ return (
+ <ShowAccountDetails
+ account={showDetails}
+ onLoadNotOk={onLoadNotOk}
+ onUpdateSuccess={() => {
+ showInfoMessage(i18n.str`Account updated`);
+ setShowDetails(undefined);
+ }}
+ onClear={() => {
+ setShowDetails(undefined);
+ }}
+ />
+ );
+ }
+ if (updatePassword) {
+ return (
+ <UpdateAccountPassword
+ account={updatePassword}
+ onLoadNotOk={onLoadNotOk}
+ onUpdateSuccess={() => {
+ showInfoMessage(i18n.str`Password changed`);
+ setUpdatePassword(undefined);
+ }}
+ onClear={() => {
+ setUpdatePassword(undefined);
+ }}
+ />
+ );
+ }
+ if (createAccount) {
+ return (
+ <CreateNewAccount
+ onClose={() => setCreateAccount(false)}
+ onCreateSuccess={(password) => {
+ showInfoMessage(
+ i18n.str`Account created with password "${password}"`,
+ );
+ setCreateAccount(false);
+ }}
+ />
+ );
+ }
+ return (
+ <Fragment>
+ <div>
+ <h1 class="nav welcome-text">
+ <i18n.Translate>Admin panel</i18n.Translate>
+ </h1>
+ </div>
+
+ <p>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div></div>
+ <div>
+ <input
+ class="pure-button pure-button-primary content"
+ type="submit"
+ value={i18n.str`Create account`}
+ onClick={async (e) => {
+ e.preventDefault();
+
+ setCreateAccount(true);
+ }}
+ />
+ </div>
+ </div>
+ </p>
+
+ <section id="main">
+ <article>
+ <h2>{i18n.str`Accounts:`}</h2>
+ <div class="results">
+ <table class="pure-table pure-table-striped">
+ <thead>
+ <tr>
+ <th>{i18n.str`Username`}</th>
+ <th>{i18n.str`Name`}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ {customers.map((item, idx) => {
+ return (
+ <tr key={idx}>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setShowDetails(item.username);
+ }}
+ >
+ {item.username}
+ </a>
+ </td>
+ <td>{item.name}</td>
+ <td>
+ <a
+ href="#"
+ onClick={(e) => {
+ e.preventDefault();
+ setUpdatePassword(item.username);
+ }}
+ >
+ change password
+ </a>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ </article>
+ </section>
+ </Fragment>
+ );
+}
+
+const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
+const EMAIL_REGEX =
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/;
+
+function initializeFromTemplate(
+ account: SandboxBackend.Circuit.CircuitAccountData | undefined,
+): WithIntermediate<SandboxBackend.Circuit.CircuitAccountData> {
+ const emptyAccount = {
+ cashout_address: undefined,
+ iban: undefined,
+ name: undefined,
+ username: undefined,
+ contact_data: undefined,
+ };
+ const emptyContact = {
+ email: undefined,
+ phone: undefined,
+ };
+
+ const initial: PartialButDefined<SandboxBackend.Circuit.CircuitAccountData> =
+ structuredClone(account) ?? emptyAccount;
+ if (typeof initial.contact_data === "undefined") {
+ initial.contact_data = emptyContact;
+ }
+ initial.contact_data.email;
+ return initial as any;
+}
+
+function UpdateAccountPassword({
+ account,
+ onClear,
+ onUpdateSuccess,
+ onLoadNotOk,
+}: {
+ onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+ onClear: () => void;
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useAccountDetails(account);
+ const { changePassword } = useAdminAccountAPI();
+ const [password, setPassword] = useState<string | undefined>();
+ const [repeat, setRepeat] = useState<string | undefined>();
+ const [error, saveError] = useState<ErrorMessage | undefined>();
+
+ if (result.clientError) {
+ if (result.isNotfound) return <div>account not found</div>;
+ }
+ if (!result.ok) {
+ return onLoadNotOk(result);
+ }
+
+ const errors = undefinedIfEmpty({
+ password: !password ? i18n.str`required` : undefined,
+ repeat: !repeat
+ ? i18n.str`required`
+ : password !== repeat
+ ? i18n.str`password doesn't match`
+ : undefined,
+ });
+
+ return (
+ <div>
+ <div>
+ <h1 class="nav welcome-text">
+ <i18n.Translate>Admin panel</i18n.Translate>
+ </h1>
+ </div>
+ {error && (
+ <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+ )}
+
+ <form class="pure-form">
+ <fieldset>
+ <label for="username">{i18n.str`Username`}</label>
+ <input name="username" type="text" readOnly value={account} />
+ </fieldset>
+ <fieldset>
+ <label>{i18n.str`Password`}</label>
+ <input
+ type="password"
+ value={password ?? ""}
+ onChange={(e) => {
+ setPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ </fieldset>
+ <fieldset>
+ <label>{i18n.str`Repeast password`}</label>
+ <input
+ type="password"
+ value={repeat ?? ""}
+ onChange={(e) => {
+ setRepeat(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.repeat}
+ isDirty={repeat !== undefined}
+ />
+ </fieldset>
+ </form>
+ <p>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div>
+ <input
+ class="pure-button"
+ type="submit"
+ value={i18n.str`Close`}
+ onClick={async (e) => {
+ e.preventDefault();
+ onClear();
+ }}
+ />
+ </div>
+ <div>
+ <input
+ id="select-exchange"
+ class="pure-button pure-button-primary content"
+ disabled={!!errors}
+ type="submit"
+ value={i18n.str`Confirm`}
+ onClick={async (e) => {
+ e.preventDefault();
+ if (!!errors || !password) return;
+ try {
+ const r = await changePassword(account, {
+ new_password: password,
+ });
+ onUpdateSuccess();
+ } catch (error) {
+ handleError(error, saveError, i18n);
+ }
+ }}
+ />
+ </div>
+ </div>
+ </p>
+ </div>
+ );
+}
+
+function CreateNewAccount({
+ onClose,
+ onCreateSuccess,
+}: {
+ onClose: () => void;
+ onCreateSuccess: (password: string) => void;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const { createAccount } = useAdminAccountAPI();
+ const [submitAccount, setSubmitAccount] = useState<
+ SandboxBackend.Circuit.CircuitAccountData | undefined
+ >();
+ const [error, saveError] = useState<ErrorMessage | undefined>();
+ return (
+ <div>
+ <div>
+ <h1 class="nav welcome-text">
+ <i18n.Translate>Admin panel</i18n.Translate>
+ </h1>
+ </div>
+ {error && (
+ <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+ )}
+
+ <AccountForm
+ template={undefined}
+ purpose="create"
+ onChange={(a) => setSubmitAccount(a)}
+ />
+
+ <p>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div>
+ <input
+ class="pure-button"
+ type="submit"
+ value={i18n.str`Close`}
+ onClick={async (e) => {
+ e.preventDefault();
+ onClose();
+ }}
+ />
+ </div>
+ <div>
+ <input
+ id="select-exchange"
+ class="pure-button pure-button-primary content"
+ disabled={!submitAccount}
+ type="submit"
+ value={i18n.str`Confirm`}
+ onClick={async (e) => {
+ e.preventDefault();
+
+ if (!submitAccount) return;
+ try {
+ const account: SandboxBackend.Circuit.CircuitAccountRequest =
+ {
+ cashout_address: submitAccount.cashout_address,
+ contact_data: submitAccount.contact_data,
+ internal_iban: submitAccount.iban,
+ name: submitAccount.name,
+ username: submitAccount.username,
+ password: randomPassword(),
+ };
+
+ await createAccount(account);
+ onCreateSuccess(account.password);
+ } catch (error) {
+ handleError(error, saveError, i18n);
+ }
+ }}
+ />
+ </div>
+ </div>
+ </p>
+ </div>
+ );
+}
+
+function ShowAccountDetails({
+ account,
+ onClear,
+ onUpdateSuccess,
+ onLoadNotOk,
+}: {
+ onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+ onClear: () => void;
+ onUpdateSuccess: () => void;
+ account: string;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useAccountDetails(account);
+ const { updateAccount } = useAdminAccountAPI();
+ const [update, setUpdate] = useState(false);
+ const [submitAccount, setSubmitAccount] = useState<
+ SandboxBackend.Circuit.CircuitAccountData | undefined
+ >();
+ const [error, saveError] = useState<ErrorMessage | undefined>();
+
+ if (result.clientError) {
+ if (result.isNotfound) return <div>account not found</div>;
+ }
+ if (!result.ok) {
+ return onLoadNotOk(result);
+ }
+
+ return (
+ <div>
+ <div>
+ <h1 class="nav welcome-text">
+ <i18n.Translate>Admin panel</i18n.Translate>
+ </h1>
+ </div>
+ {error && (
+ <ErrorBanner error={error} onClear={() => saveError(undefined)} />
+ )}
+ <AccountForm
+ template={result.data}
+ purpose={update ? "update" : "show"}
+ onChange={(a) => setSubmitAccount(a)}
+ />
+
+ <p>
+ <div style={{ display: "flex", justifyContent: "space-between" }}>
+ <div>
+ <input
+ class="pure-button"
+ type="submit"
+ value={i18n.str`Close`}
+ onClick={async (e) => {
+ e.preventDefault();
+ onClear();
+ }}
+ />
+ </div>
+ <div>
+ <input
+ id="select-exchange"
+ class="pure-button pure-button-primary content"
+ disabled={update && !submitAccount}
+ type="submit"
+ value={update ? i18n.str`Confirm` : i18n.str`Update`}
+ onClick={async (e) => {
+ e.preventDefault();
+
+ if (!update) {
+ setUpdate(true);
+ } else {
+ if (!submitAccount) return;
+ try {
+ await updateAccount(account, {
+ cashout_address: submitAccount.cashout_address,
+ contact_data: submitAccount.contact_data,
+ });
+ onUpdateSuccess();
+ } catch (error) {
+ handleError(error, saveError, i18n);
+ }
+ }
+ }}
+ />
+ </div>
+ </div>
+ </p>
+ </div>
+ );
+}
+
+/**
+ * Create valid account object to update or create
+ * Take template as initial values for the form
+ * Purpose indicate if all field al read only (show), part of them (update)
+ * or none (create)
+ * @param param0
+ * @returns
+ */
+function AccountForm({
+ template,
+ purpose,
+ onChange,
+}: {
+ template: SandboxBackend.Circuit.CircuitAccountData | undefined;
+ onChange: (a: SandboxBackend.Circuit.CircuitAccountData | undefined) => void;
+ purpose: "create" | "update" | "show";
+}): VNode {
+ const initial = initializeFromTemplate(template);
+ const [form, setForm] = useState(initial);
+ const [errors, setErrors] = useState<typeof initial | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ function updateForm(newForm: typeof initial): void {
+ const parsed = !newForm.cashout_address
+ ? undefined
+ : parsePaytoUri(newForm.cashout_address);
+
+ const validationResult = undefinedIfEmpty<typeof initial>({
+ cashout_address: !newForm.cashout_address
+ ? i18n.str`required`
+ : !parsed
+ ? i18n.str`does not follow the pattern`
+ : !parsed.isKnown || parsed.targetType !== "iban"
+ ? i18n.str`only "IBAN" target are supported`
+ : !IBAN_REGEX.test(parsed.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : undefined,
+ contact_data: {
+ email: !newForm.contact_data.email
+ ? undefined
+ : !EMAIL_REGEX.test(newForm.contact_data.email)
+ ? i18n.str`it should be an email`
+ : undefined,
+ phone: !newForm.contact_data.phone
+ ? undefined
+ : !newForm.contact_data.phone.startsWith("+")
+ ? i18n.str`should start with +`
+ : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone)
+ ? i18n.str`phone number can't have other than numbers`
+ : undefined,
+ },
+ iban: !newForm.iban
+ ? i18n.str`required`
+ : !IBAN_REGEX.test(newForm.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : undefined,
+ name: !newForm.name ? i18n.str`required` : undefined,
+ username: !newForm.username ? i18n.str`required` : undefined,
+ });
+
+ setErrors(validationResult);
+ setForm(newForm);
+ onChange(validationResult === undefined ? undefined : (newForm as any));
+ }
+
+ return (
+ <form class="pure-form">
+ <fieldset>
+ <label for="username">{i18n.str`Username`}</label>
+ <input
+ name="username"
+ type="text"
+ disabled={purpose !== "create"}
+ value={form.username}
+ onChange={(e) => {
+ form.username = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.username}
+ isDirty={form.username !== undefined}
+ />
+ </fieldset>
+ <fieldset>
+ <label>{i18n.str`Name`}</label>
+ <input
+ disabled={purpose !== "create"}
+ value={form.name ?? ""}
+ onChange={(e) => {
+ form.name = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.name}
+ isDirty={form.name !== undefined}
+ />
+ </fieldset>
+ <fieldset>
+ <label>{i18n.str`IBAN`}</label>
+ <input
+ disabled={purpose !== "create"}
+ value={form.iban ?? ""}
+ onChange={(e) => {
+ form.iban = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.iban}
+ isDirty={form.iban !== undefined}
+ />
+ </fieldset>
+ <fieldset>
+ <label>{i18n.str`Email`}</label>
+ <input
+ disabled={purpose === "show"}
+ value={form.contact_data.email ?? ""}
+ onChange={(e) => {
+ form.contact_data.email = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.contact_data.email}
+ isDirty={form.contact_data.email !== undefined}
+ />
+ </fieldset>
+ <fieldset>
+ <label>{i18n.str`Phone`}</label>
+ <input
+ disabled={purpose === "show"}
+ value={form.contact_data.phone ?? ""}
+ onChange={(e) => {
+ form.contact_data.phone = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.contact_data.phone}
+ isDirty={form.contact_data?.phone !== undefined}
+ />
+ </fieldset>
+ <fieldset>
+ <label>{i18n.str`Cashout address`}</label>
+ <input
+ disabled={purpose === "show"}
+ value={form.cashout_address ?? ""}
+ onChange={(e) => {
+ form.cashout_address = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.cashout_address}
+ isDirty={form.cashout_address !== undefined}
+ />
+ </fieldset>
+ </form>
+ );
+}
+
+function handleError(
+ error: unknown,
+ saveError: (e: ErrorMessage) => void,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): void {
+ if (error instanceof RequestError) {
+ const payload = error.info.error as SandboxBackend.SandboxError;
+ saveError({
+ title: error.info.serverError
+ ? i18n.str`Server had an error`
+ : i18n.str`Server didn't accept the request`,
+ description: payload.error.description,
+ });
+ } else if (error instanceof Error) {
+ saveError({
+ title: i18n.str`Could not update account`,
+ description: error.message,
+ });
+ } else {
+ saveError({
+ title: i18n.str`Error, please report`,
+ debug: JSON.stringify(error),
+ });
+ }
+}
diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx
index e36629e2a..ed36daa21 100644
--- a/packages/demobank-ui/src/pages/BankFrame.tsx
+++ b/packages/demobank-ui/src/pages/BankFrame.tsx
@@ -19,7 +19,11 @@ import { ComponentChildren, Fragment, h, VNode } from "preact";
import talerLogo from "../assets/logo-white.svg";
import { LangSelectorLikePy as LangSelector } from "../components/LangSelector.js";
import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import {
+ ErrorMessage,
+ PageStateType,
+ usePageContext,
+} from "../context/pageState.js";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { bankUiSettings } from "../settings.js";
@@ -42,7 +46,7 @@ export function BankFrame({
onClick={() => {
pageStateSetter((prevState: PageStateType) => {
const { talerWithdrawUri, withdrawalId, ...rest } = prevState;
- backend.clear();
+ backend.logOut();
return {
...rest,
withdrawalInProgress: false,
@@ -107,7 +111,14 @@ export function BankFrame({
</nav>
</div>
<section id="main" class="content">
- <ErrorBanner />
+ {pageState.error && (
+ <ErrorBanner
+ error={pageState.error}
+ onClear={() => {
+ pageStateSetter((prev) => ({ ...prev, error: undefined }));
+ }}
+ />
+ )}
<StatusBanner />
{backend.state.status === "loggedIn" ? logOut : null}
{children}
@@ -136,33 +147,34 @@ function maybeDemoContent(content: VNode): VNode {
return <Fragment />;
}
-function ErrorBanner(): VNode | null {
- const { pageState, pageStateSetter } = usePageContext();
-
- if (!pageState.error) return null;
-
- const rval = (
+export function ErrorBanner({
+ error,
+ onClear,
+}: {
+ error: ErrorMessage;
+ onClear: () => void;
+}): VNode | null {
+ return (
<div class="informational informational-fail" style={{ marginTop: 8 }}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<p>
- <b>{pageState.error.title}</b>
+ <b>{error.title}</b>
</p>
<div>
<input
type="button"
class="pure-button"
value="Clear"
- onClick={async () => {
- pageStateSetter((prev) => ({ ...prev, error: undefined }));
+ onClick={(e) => {
+ e.preventDefault();
+ onClear();
}}
/>
</div>
</div>
- <p>{pageState.error.description}</p>
+ <p>{error.description}</p>
</div>
);
- delete pageState.error;
- return rval;
}
function StatusBanner(): VNode | null {
diff --git a/packages/demobank-ui/src/pages/HomePage.tsx b/packages/demobank-ui/src/pages/HomePage.tsx
new file mode 100644
index 000000000..e60732d42
--- /dev/null
+++ b/packages/demobank-ui/src/pages/HomePage.tsx
@@ -0,0 +1,149 @@
+/*
+ This file is part of GNU Taler
+ (C) 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/>
+ */
+
+import { Logger } from "@gnu-taler/taler-util";
+import {
+ HttpResponsePaginated,
+ useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
+import { Loading } from "../components/Loading.js";
+import { useBackendContext } from "../context/backend.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
+import { AccountPage } from "./AccountPage.js";
+import { AdminPage } from "./AdminPage.js";
+import { LoginForm } from "./LoginForm.js";
+import { WithdrawalQRCode } from "./WithdrawalQRCode.js";
+
+const logger = new Logger("AccountPage");
+
+/**
+ * show content based on state:
+ * - LoginForm if the user is not logged in
+ * - qr code if withdrawal in progress
+ * - else account information
+ * Use the handler to catch error cases
+ *
+ * @param param0
+ * @returns
+ */
+export function HomePage({ onRegister }: { onRegister: () => void }): VNode {
+ const backend = useBackendContext();
+ const { pageState, pageStateSetter } = usePageContext();
+ const { i18n } = useTranslationContext();
+
+ function saveError(error: PageStateType["error"]): void {
+ pageStateSetter((prev) => ({ ...prev, error }));
+ }
+
+ function saveErrorAndLogout(error: PageStateType["error"]): void {
+ saveError(error);
+ backend.logOut();
+ }
+
+ function clearCurrentWithdrawal(): void {
+ pageStateSetter((prevState: PageStateType) => {
+ return {
+ ...prevState,
+ withdrawalId: undefined,
+ talerWithdrawUri: undefined,
+ withdrawalInProgress: false,
+ };
+ });
+ }
+
+ if (backend.state.status === "loggedOut") {
+ return <LoginForm onRegister={onRegister} />;
+ }
+
+ const { withdrawalId, talerWithdrawUri } = pageState;
+
+ if (talerWithdrawUri && withdrawalId) {
+ return (
+ <WithdrawalQRCode
+ account={backend.state.username}
+ withdrawalId={withdrawalId}
+ talerWithdrawUri={talerWithdrawUri}
+ onAbort={clearCurrentWithdrawal}
+ onLoadNotOk={handleNotOkResult(
+ backend.state.username,
+ saveError,
+ i18n,
+ onRegister,
+ )}
+ />
+ );
+ }
+
+ if (backend.state.isUserAdministrator) {
+ return (
+ <AdminPage
+ onLoadNotOk={handleNotOkResult(
+ backend.state.username,
+ saveErrorAndLogout,
+ i18n,
+ onRegister,
+ )}
+ />
+ );
+ }
+
+ return (
+ <AccountPage
+ account={backend.state.username}
+ onLoadNotOk={handleNotOkResult(
+ backend.state.username,
+ saveErrorAndLogout,
+ i18n,
+ onRegister,
+ )}
+ />
+ );
+}
+
+function handleNotOkResult(
+ account: string,
+ onErrorHandler: (state: PageStateType["error"]) => void,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+ onRegister: () => void,
+): <T, E>(result: HttpResponsePaginated<T, E>) => VNode {
+ return function handleNotOkResult2<T, E>(
+ result: HttpResponsePaginated<T, E>,
+ ): VNode {
+ if (result.clientError && result.isUnauthorized) {
+ onErrorHandler({
+ title: i18n.str`Wrong credentials for "${account}"`,
+ });
+ return <LoginForm onRegister={onRegister} />;
+ }
+ if (result.clientError && result.isNotfound) {
+ onErrorHandler({
+ title: i18n.str`Username or account label "${account}" not found`,
+ });
+ return <LoginForm onRegister={onRegister} />;
+ }
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ onErrorHandler({
+ title: i18n.str`The backend reported a problem: HTTP status #${result.status}`,
+ description: `Diagnostic from ${result.info?.url.href} is "${result.message}"`,
+ debug: JSON.stringify(result.error),
+ });
+ return <LoginForm onRegister={onRegister} />;
+ }
+ return <div />;
+ };
+}
diff --git a/packages/demobank-ui/src/pages/LoginForm.tsx b/packages/demobank-ui/src/pages/LoginForm.tsx
index a5d8695dc..3d4279f99 100644
--- a/packages/demobank-ui/src/pages/LoginForm.tsx
+++ b/packages/demobank-ui/src/pages/LoginForm.tsx
@@ -14,21 +14,19 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { h, VNode } from "preact";
-import { route } from "preact-router";
+import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
-import { BackendStateHandler } from "../hooks/backend.js";
import { bankUiSettings } from "../settings.js";
-import { getBankBackendBaseUrl, undefinedIfEmpty } from "../utils.js";
+import { undefinedIfEmpty } from "../utils.js";
+import { PASSWORD_REGEX, USERNAME_REGEX } from "./RegistrationPage.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-import { USERNAME_REGEX, PASSWORD_REGEX } from "./RegistrationPage.js";
/**
* Collect and submit login data.
*/
-export function LoginForm(): VNode {
+export function LoginForm({ onRegister }: { onRegister: () => void }): VNode {
const backend = useBackendContext();
const [username, setUsername] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
@@ -52,107 +50,93 @@ export function LoginForm(): VNode {
});
return (
- <div class="login-div">
- <form
- class="login-form"
- noValidate
- onSubmit={(e) => {
- e.preventDefault();
- }}
- autoCapitalize="none"
- autoCorrect="off"
- >
- <div class="pure-form">
- <h2>{i18n.str`Please login!`}</h2>
- <p class="unameFieldLabel loginFieldLabel formFieldLabel">
- <label for="username">{i18n.str`Username:`}</label>
- </p>
- <input
- ref={ref}
- autoFocus
- type="text"
- name="username"
- id="username"
- value={username ?? ""}
- placeholder="Username"
- required
- onInput={(e): void => {
- setUsername(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.username}
- isDirty={username !== undefined}
- />
- <p class="passFieldLabel loginFieldLabel formFieldLabel">
- <label for="password">{i18n.str`Password:`}</label>
- </p>
- <input
- type="password"
- name="password"
- id="password"
- value={password ?? ""}
- placeholder="Password"
- required
- onInput={(e): void => {
- setPassword(e.currentTarget.value);
- }}
- />
- <ShowInputErrorLabel
- message={errors?.password}
- isDirty={password !== undefined}
- />
- <br />
- <button
- type="submit"
- class="pure-button pure-button-primary"
- disabled={!!errors}
- onClick={(e) => {
- e.preventDefault();
- if (!username || !password) return;
- loginCall({ username, password }, backend);
- setUsername(undefined);
- setPassword(undefined);
- }}
- >
- {i18n.str`Login`}
- </button>
+ <Fragment>
+ <h1 class="nav">{i18n.str`Welcome to ${bankUiSettings.bankName}!`}</h1>
- {bankUiSettings.allowRegistrations ? (
+ <div class="login-div">
+ <form
+ class="login-form"
+ noValidate
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ autoCapitalize="none"
+ autoCorrect="off"
+ >
+ <div class="pure-form">
+ <h2>{i18n.str`Please login!`}</h2>
+ <p class="unameFieldLabel loginFieldLabel formFieldLabel">
+ <label for="username">{i18n.str`Username:`}</label>
+ </p>
+ <input
+ ref={ref}
+ autoFocus
+ type="text"
+ name="username"
+ id="username"
+ value={username ?? ""}
+ placeholder="Username"
+ autocomplete="username"
+ required
+ onInput={(e): void => {
+ setUsername(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.username}
+ isDirty={username !== undefined}
+ />
+ <p class="passFieldLabel loginFieldLabel formFieldLabel">
+ <label for="password">{i18n.str`Password:`}</label>
+ </p>
+ <input
+ type="password"
+ name="password"
+ id="password"
+ autocomplete="current-password"
+ value={password ?? ""}
+ placeholder="Password"
+ required
+ onInput={(e): void => {
+ setPassword(e.currentTarget.value);
+ }}
+ />
+ <ShowInputErrorLabel
+ message={errors?.password}
+ isDirty={password !== undefined}
+ />
+ <br />
<button
- class="pure-button pure-button-secondary btn-cancel"
+ type="submit"
+ class="pure-button pure-button-primary"
+ disabled={!!errors}
onClick={(e) => {
e.preventDefault();
- route("/register");
+ if (!username || !password) return;
+ backend.logIn({ username, password });
+ setUsername(undefined);
+ setPassword(undefined);
}}
>
- {i18n.str`Register`}
+ {i18n.str`Login`}
</button>
- ) : (
- <div />
- )}
- </div>
- </form>
- </div>
- );
-}
-
-async function loginCall(
- req: { username: string; password: string },
- /**
- * FIXME: figure out if the two following
- * functions can be retrieved from the state.
- */
- backend: BackendStateHandler,
-): Promise<void> {
- /**
- * Optimistically setting the state as 'logged in', and
- * let the Account component request the balance to check
- * whether the credentials are valid. */
- backend.save({
- url: getBankBackendBaseUrl(),
- username: req.username,
- password: req.password,
- });
+ {bankUiSettings.allowRegistrations ? (
+ <button
+ class="pure-button pure-button-secondary btn-cancel"
+ onClick={(e) => {
+ e.preventDefault();
+ onRegister();
+ }}
+ >
+ {i18n.str`Register`}
+ </button>
+ ) : (
+ <div />
+ )}
+ </div>
+ </form>
+ </div>
+ </Fragment>
+ );
}
diff --git a/packages/demobank-ui/src/pages/PaymentOptions.tsx b/packages/demobank-ui/src/pages/PaymentOptions.tsx
index ae876d556..dd04ed6e2 100644
--- a/packages/demobank-ui/src/pages/PaymentOptions.tsx
+++ b/packages/demobank-ui/src/pages/PaymentOptions.tsx
@@ -19,17 +19,22 @@ import { useState } from "preact/hooks";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
/**
* Let the user choose a payment option,
* then specify the details trigger the action.
*/
-export function PaymentOptions({ currency }: { currency?: string }): VNode {
+export function PaymentOptions({ currency }: { currency: string }): VNode {
const { i18n } = useTranslationContext();
+ const { pageStateSetter } = usePageContext();
const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
"charge-wallet",
);
+ function saveError(error: PageStateType["error"]): void {
+ pageStateSetter((prev) => ({ ...prev, error }));
+ }
return (
<article>
@@ -55,13 +60,35 @@ export function PaymentOptions({ currency }: { currency?: string }): VNode {
{tab === "charge-wallet" && (
<div id="charge-wallet" class="tabcontent active">
<h3>{i18n.str`Obtain digital cash`}</h3>
- <WalletWithdrawForm focus currency={currency} />
+ <WalletWithdrawForm
+ focus
+ currency={currency}
+ onSuccess={(data) => {
+ pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+ withdrawalInProgress: true,
+ talerWithdrawUri: data.taler_withdraw_uri,
+ withdrawalId: data.withdrawal_id,
+ }));
+ }}
+ onError={saveError}
+ />
</div>
)}
{tab === "wire-transfer" && (
<div id="wire-transfer" class="tabcontent active">
<h3>{i18n.str`Transfer to bank account`}</h3>
- <PaytoWireTransferForm focus currency={currency} />
+ <PaytoWireTransferForm
+ focus
+ currency={currency}
+ onSuccess={() => {
+ pageStateSetter((prevState: PageStateType) => ({
+ ...prevState,
+ info: i18n.str`Wire transfer created!`,
+ }));
+ }}
+ onError={saveError}
+ />
</div>
)}
</div>
diff --git a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
index 46b006880..d859b1cc7 100644
--- a/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
+++ b/packages/demobank-ui/src/pages/PaytoWireTransferForm.tsx
@@ -14,64 +14,81 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Amounts, Logger, parsePaytoUri } from "@gnu-taler/taler-util";
-import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser";
-import { h, VNode } from "preact";
-import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import {
+ Amounts,
+ buildPayto,
+ Logger,
+ parsePaytoUri,
+ stringifyPaytoUri,
+} from "@gnu-taler/taler-util";
import {
InternationalizationAPI,
+ RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
+import { useAccessAPI } from "../hooks/access.js";
import { BackendState } from "../hooks/backend.js";
-import { prepareHeaders, undefinedIfEmpty } from "../utils.js";
+import { undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("PaytoWireTransferForm");
export function PaytoWireTransferForm({
focus,
+ onError,
+ onSuccess,
currency,
}: {
focus?: boolean;
- currency?: string;
+ onError: (e: PageStateType["error"]) => void;
+ onSuccess: () => void;
+ currency: string;
}): VNode {
const backend = useBackendContext();
- const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
+ // const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
- const [submitData, submitDataSetter] = useWireTransferRequestType();
+ const [isRawPayto, setIsRawPayto] = useState(false);
+ // const [submitData, submitDataSetter] = useWireTransferRequestType();
+ const [iban, setIban] = useState<string | undefined>(undefined);
+ const [subject, setSubject] = useState<string | undefined>(undefined);
+ const [amount, setAmount] = useState<string | undefined>(undefined);
const [rawPaytoInput, rawPaytoInputSetter] = useState<string | undefined>(
undefined,
);
const { i18n } = useTranslationContext();
const ibanRegex = "^[A-Z][A-Z][0-9]+$";
- let transactionData: TransactionRequestType;
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
- }, [focus, pageState.isRawPayto]);
+ }, [focus, isRawPayto]);
let parsedAmount = undefined;
+ const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/;
const errorsWire = undefinedIfEmpty({
- iban: !submitData?.iban
+ iban: !iban
? i18n.str`Missing IBAN`
- : !/^[A-Z0-9]*$/.test(submitData.iban)
+ : !IBAN_REGEX.test(iban)
? i18n.str`IBAN should have just uppercased letters and numbers`
: undefined,
- subject: !submitData?.subject ? i18n.str`Missing subject` : undefined,
- amount: !submitData?.amount
+ subject: !subject ? i18n.str`Missing subject` : undefined,
+ amount: !amount
? i18n.str`Missing amount`
- : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
+ : !(parsedAmount = Amounts.parse(`${currency}:${amount}`))
? i18n.str`Amount is not valid`
: Amounts.isZero(parsedAmount)
? i18n.str`Should be greater than 0`
: undefined,
});
- if (!pageState.isRawPayto)
+ const { createTransaction } = useAccessAPI();
+
+ if (!isRawPayto)
return (
<div>
<form
@@ -90,21 +107,18 @@ export function PaytoWireTransferForm({
type="text"
id="iban"
name="iban"
- value={submitData?.iban ?? ""}
+ value={iban ?? ""}
placeholder="CC0123456789"
required
pattern={ibanRegex}
onInput={(e): void => {
- submitDataSetter((submitData) => ({
- ...submitData,
- iban: e.currentTarget.value,
- }));
+ setIban(e.currentTarget.value);
}}
/>
<br />
<ShowInputErrorLabel
message={errorsWire?.iban}
- isDirty={submitData?.iban !== undefined}
+ isDirty={iban !== undefined}
/>
<br />
<label for="subject">{i18n.str`Transfer subject:`}</label>&nbsp;
@@ -113,19 +127,16 @@ export function PaytoWireTransferForm({
name="subject"
id="subject"
placeholder="subject"
- value={submitData?.subject ?? ""}
+ value={subject ?? ""}
required
onInput={(e): void => {
- submitDataSetter((submitData) => ({
- ...submitData,
- subject: e.currentTarget.value,
- }));
+ setSubject(e.currentTarget.value);
}}
/>
<br />
<ShowInputErrorLabel
message={errorsWire?.subject}
- isDirty={submitData?.subject !== undefined}
+ isDirty={subject !== undefined}
/>
<br />
<label for="amount">{i18n.str`Amount:`}</label>&nbsp;
@@ -146,18 +157,15 @@ export function PaytoWireTransferForm({
id="amount"
placeholder="amount"
required
- value={submitData?.amount ?? ""}
+ value={amount ?? ""}
onInput={(e): void => {
- submitDataSetter((submitData) => ({
- ...submitData,
- amount: e.currentTarget.value,
- }));
+ setAmount(e.currentTarget.value);
}}
/>
</div>
<ShowInputErrorLabel
message={errorsWire?.amount}
- isDirty={submitData?.amount !== undefined}
+ isDirty={amount !== undefined}
/>
</p>
@@ -169,43 +177,28 @@ export function PaytoWireTransferForm({
value="Send"
onClick={async (e) => {
e.preventDefault();
- if (
- typeof submitData === "undefined" ||
- typeof submitData.iban === "undefined" ||
- submitData.iban === "" ||
- typeof submitData.subject === "undefined" ||
- submitData.subject === "" ||
- typeof submitData.amount === "undefined" ||
- submitData.amount === ""
- ) {
- logger.error("Not all the fields were given.");
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Field(s) missing.`,
- },
- }));
+ if (!(iban && subject && amount)) {
return;
}
- transactionData = {
- paytoUri: `payto://iban/${
- submitData.iban
- }?message=${encodeURIComponent(submitData.subject)}`,
- amount: `${currency}:${submitData.amount}`,
- };
- return await createTransactionCall(
- transactionData,
- backend.state,
- pageStateSetter,
- () =>
- submitDataSetter((p) => ({
- amount: undefined,
- iban: undefined,
- subject: undefined,
- })),
- i18n,
- );
+ const ibanPayto = buildPayto("iban", iban, undefined);
+ ibanPayto.params.message = encodeURIComponent(subject);
+ const paytoUri = stringifyPaytoUri(ibanPayto);
+
+ await createTransaction({
+ paytoUri,
+ amount: `${currency}:${amount}`,
+ });
+ // return await createTransactionCall(
+ // transactionData,
+ // backend.state,
+ // pageStateSetter,
+ // () => {
+ // setAmount(undefined);
+ // setIban(undefined);
+ // setSubject(undefined);
+ // },
+ // i18n,
+ // );
}}
/>
<input
@@ -214,11 +207,9 @@ export function PaytoWireTransferForm({
value="Clear"
onClick={async (e) => {
e.preventDefault();
- submitDataSetter((p) => ({
- amount: undefined,
- iban: undefined,
- subject: undefined,
- }));
+ setAmount(undefined);
+ setIban(undefined);
+ setSubject(undefined);
}}
/>
</p>
@@ -227,11 +218,7 @@ export function PaytoWireTransferForm({
<a
href="/account"
onClick={() => {
- logger.trace("switch to raw payto form");
- pageStateSetter((prevState) => ({
- ...prevState,
- isRawPayto: true,
- }));
+ setIsRawPayto(true);
}}
>
{i18n.str`Want to try the raw payto://-format?`}
@@ -240,11 +227,23 @@ export function PaytoWireTransferForm({
</div>
);
+ const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput);
+
const errorsPayto = undefinedIfEmpty({
rawPaytoInput: !rawPaytoInput
- ? i18n.str`Missing payto address`
- : !parsePaytoUri(rawPaytoInput)
- ? i18n.str`Payto does not follow the pattern`
+ ? i18n.str`required`
+ : !parsed
+ ? i18n.str`does not follow the pattern`
+ : !parsed.params.amount
+ ? i18n.str`use the "amount" parameter to specify the amount to be transferred`
+ : Amounts.parse(parsed.params.amount) === undefined
+ ? i18n.str`the amount is not valid`
+ : !parsed.params.message
+ ? i18n.str`use the "message" parameter to specify a reference text for the transfer`
+ : !parsed.isKnown || parsed.targetType !== "iban"
+ ? i18n.str`only "IBAN" target are supported`
+ : !IBAN_REGEX.test(parsed.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
: undefined,
});
@@ -296,25 +295,29 @@ export function PaytoWireTransferForm({
disabled={!!errorsPayto}
value={i18n.str`Send`}
onClick={async () => {
- // empty string evaluates to false.
if (!rawPaytoInput) {
logger.error("Didn't get any raw Payto string!");
return;
}
- transactionData = { paytoUri: rawPaytoInput };
- if (
- typeof transactionData.paytoUri === "undefined" ||
- transactionData.paytoUri.length === 0
- )
- return;
- return await createTransactionCall(
- transactionData,
- backend.state,
- pageStateSetter,
- () => rawPaytoInputSetter(undefined),
- i18n,
- );
+ try {
+ await createTransaction({
+ paytoUri: rawPaytoInput,
+ });
+ onSuccess();
+ rawPaytoInputSetter(undefined);
+ } catch (error) {
+ if (error instanceof RequestError) {
+ const errorData: SandboxBackend.SandboxError =
+ error.info.error;
+
+ onError({
+ title: i18n.str`Transfer creation gave response error`,
+ description: errorData.error.description,
+ debug: JSON.stringify(errorData),
+ });
+ }
+ }
}}
/>
</p>
@@ -322,11 +325,7 @@ export function PaytoWireTransferForm({
<a
href="/account"
onClick={() => {
- logger.trace("switch to wire-transfer-form");
- pageStateSetter((prevState) => ({
- ...prevState,
- isRawPayto: false,
- }));
+ setIsRawPayto(false);
}}
>
{i18n.str`Use wire-transfer form?`}
@@ -336,115 +335,3 @@ export function PaytoWireTransferForm({
</div>
);
}
-
-/**
- * Stores in the state a object representing a wire transfer,
- * in order to avoid losing the handle of the data entered by
- * the user in <input> fields. FIXME: name not matching the
- * purpose, as this is not a HTTP request body but rather the
- * state of the <input>-elements.
- */
-type WireTransferRequestTypeOpt = WireTransferRequestType | undefined;
-function useWireTransferRequestType(
- state?: WireTransferRequestType,
-): [WireTransferRequestTypeOpt, StateUpdater<WireTransferRequestTypeOpt>] {
- const ret = useLocalStorage(
- "wire-transfer-request-state",
- JSON.stringify(state),
- );
- const retObj: WireTransferRequestTypeOpt = ret[0]
- ? JSON.parse(ret[0])
- : ret[0];
- const retSetter: StateUpdater<WireTransferRequestTypeOpt> = function (val) {
- const newVal =
- val instanceof Function
- ? JSON.stringify(val(retObj))
- : JSON.stringify(val);
- ret[1](newVal);
- };
- return [retObj, retSetter];
-}
-
-/**
- * This function creates a new transaction. It reads a Payto
- * address entered by the user and POSTs it to the bank. No
- * sanity-check of the input happens before the POST as this is
- * already conducted by the backend.
- */
-async function createTransactionCall(
- req: TransactionRequestType,
- backendState: BackendState,
- pageStateSetter: StateUpdater<PageStateType>,
- /**
- * Optional since the raw payto form doesn't have
- * a stateful management of the input data yet.
- */
- cleanUpForm: () => void,
- i18n: InternationalizationAPI,
-): Promise<void> {
- if (backendState.status === "loggedOut") {
- logger.error("No credentials found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: i18n.str`No credentials found.`,
- },
- }));
- return;
- }
- let res: Response;
- try {
- const { username, password } = backendState;
- const headers = prepareHeaders(username, password);
- const url = new URL(
- `access-api/accounts/${backendState.username}/transactions`,
- backendState.url,
- );
- res = await fetch(url.href, {
- method: "POST",
- headers,
- body: JSON.stringify(req),
- });
- } catch (error) {
- logger.error("Could not POST transaction request to the bank", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Could not create the wire transfer`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- // POST happened, status not sure yet.
- if (!res.ok) {
- const response = await res.json();
- logger.error(
- `Transfer creation gave response error: ${response} (${res.status})`,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Transfer creation gave response error`,
- description: response.error.description,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
- // status is 200 OK here, tell the user.
- logger.trace("Wire transfer created!");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- info: i18n.str`Wire transfer created!`,
- }));
-
- // Only at this point the input data can
- // be discarded.
- cleanUpForm();
-}
diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
index 7bf5c41c7..54a77b42a 100644
--- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
+++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx
@@ -15,91 +15,42 @@
*/
import { Logger } from "@gnu-taler/taler-util";
-import { useLocalStorage } from "@gnu-taler/web-util/lib/index.browser";
-import { ComponentChildren, Fragment, h, VNode } from "preact";
-import { route } from "preact-router";
+import {
+ HttpResponsePaginated,
+ useLocalStorage,
+ useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
+import { Fragment, h, VNode } from "preact";
import { StateUpdater } from "preact/hooks";
-import useSWR, { SWRConfig } from "swr";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
-import { getBankBackendBaseUrl } from "../utils.js";
-import { BankFrame } from "./BankFrame.js";
import { Transactions } from "../components/Transactions/index.js";
+import { usePublicAccounts } from "../hooks/access.js";
const logger = new Logger("PublicHistoriesPage");
-export function PublicHistoriesPage(): VNode {
- return (
- <SWRWithoutCredentials baseUrl={getBankBackendBaseUrl()}>
- <BankFrame>
- <PublicHistories />
- </BankFrame>
- </SWRWithoutCredentials>
- );
-}
-
-function SWRWithoutCredentials({
- baseUrl,
- children,
-}: {
- children: ComponentChildren;
- baseUrl: string;
-}): VNode {
- logger.trace("Base URL", baseUrl);
- return (
- <SWRConfig
- value={{
- fetcher: (url: string) =>
- fetch(baseUrl + url || "").then((r) => {
- if (!r.ok) throw { status: r.status, json: r.json() };
+// export function PublicHistoriesPage2(): VNode {
+// return (
+// <BankFrame>
+// <PublicHistories />
+// </BankFrame>
+// );
+// }
- return r.json();
- }),
- }}
- >
- {children as any}
- </SWRConfig>
- );
+interface Props {
+ onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
}
/**
* Show histories of public accounts.
*/
-function PublicHistories(): VNode {
- const { pageState, pageStateSetter } = usePageContext();
+export function PublicHistoriesPage({ onLoadNotOk }: Props): VNode {
const [showAccount, setShowAccount] = useShowPublicAccount();
- const { data, error } = useSWR("access-api/public-accounts");
const { i18n } = useTranslationContext();
- if (typeof error !== "undefined") {
- switch (error.status) {
- case 404:
- logger.error("public accounts: 404", error);
- route("/account");
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
+ const result = usePublicAccounts();
+ if (!result.ok) return onLoadNotOk(result);
- error: {
- title: i18n.str`List of public accounts was not found.`,
- debug: JSON.stringify(error),
- },
- }));
- break;
- default:
- logger.error("public accounts: non-404 error", error);
- route("/account");
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
+ const { data } = result;
- error: {
- title: i18n.str`List of public accounts could not be retrieved.`,
- debug: JSON.stringify(error),
- },
- }));
- break;
- }
- }
- if (!data) return <p>Waiting public accounts list...</p>;
const txs: Record<string, h.JSX.Element> = {};
const accountsBar = [];
@@ -133,9 +84,7 @@ function PublicHistories(): VNode {
</a>
</li>,
);
- txs[account.accountLabel] = (
- <Transactions accountLabel={account.accountLabel} pageNumber={0} />
- );
+ txs[account.accountLabel] = <Transactions account={account.accountLabel} />;
}
return (
diff --git a/packages/demobank-ui/src/pages/QrCodeSection.tsx b/packages/demobank-ui/src/pages/QrCodeSection.tsx
index e02c6efb1..708e28657 100644
--- a/packages/demobank-ui/src/pages/QrCodeSection.tsx
+++ b/packages/demobank-ui/src/pages/QrCodeSection.tsx
@@ -21,10 +21,10 @@ import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
export function QrCodeSection({
talerWithdrawUri,
- abortButton,
+ onAbort,
}: {
talerWithdrawUri: string;
- abortButton: h.JSX.Element;
+ onAbort: () => void;
}): VNode {
const { i18n } = useTranslationContext();
useEffect(() => {
@@ -62,7 +62,10 @@ export function QrCodeSection({
</i18n.Translate>
</p>
<br />
- {abortButton}
+ <a
+ class="pure-button btn-cancel"
+ onClick={onAbort}
+ >{i18n.str`Abort`}</a>
</div>
</article>
</section>
diff --git a/packages/demobank-ui/src/pages/RegistrationPage.tsx b/packages/demobank-ui/src/pages/RegistrationPage.tsx
index 29f1bf5ee..247ef8d80 100644
--- a/packages/demobank-ui/src/pages/RegistrationPage.tsx
+++ b/packages/demobank-ui/src/pages/RegistrationPage.tsx
@@ -13,38 +13,36 @@
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 { Logger } from "@gnu-taler/taler-util";
-import { Fragment, h, VNode } from "preact";
-import { route } from "preact-router";
-import { StateUpdater, useState } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import { HttpStatusCode, Logger } from "@gnu-taler/taler-util";
import {
- InternationalizationAPI,
+ RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
-import { BackendStateHandler } from "../hooks/backend.js";
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useBackendContext } from "../context/backend.js";
+import { PageStateType } from "../context/pageState.js";
+import { useTestingAPI } from "../hooks/access.js";
import { bankUiSettings } from "../settings.js";
-import { getBankBackendBaseUrl, undefinedIfEmpty } from "../utils.js";
-import { BankFrame } from "./BankFrame.js";
+import { undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("RegistrationPage");
-export function RegistrationPage(): VNode {
+export function RegistrationPage({
+ onError,
+ onComplete,
+}: {
+ onComplete: () => void;
+ onError: (e: PageStateType["error"]) => void;
+}): VNode {
const { i18n } = useTranslationContext();
if (!bankUiSettings.allowRegistrations) {
return (
- <BankFrame>
- <p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
- </BankFrame>
+ <p>{i18n.str`Currently, the bank is not accepting new registrations!`}</p>
);
}
- return (
- <BankFrame>
- <RegistrationForm />
- </BankFrame>
- );
+ return <RegistrationForm onComplete={onComplete} onError={onError} />;
}
export const USERNAME_REGEX = /^[a-z][a-zA-Z0-9]*$/;
@@ -53,13 +51,19 @@ export const PASSWORD_REGEX = /^[a-z0-9][a-zA-Z0-9]*$/;
/**
* Collect and submit registration data.
*/
-function RegistrationForm(): VNode {
+function RegistrationForm({
+ onComplete,
+ onError,
+}: {
+ onComplete: () => void;
+ onError: (e: PageStateType["error"]) => void;
+}): VNode {
const backend = useBackendContext();
- const { pageState, pageStateSetter } = usePageContext();
const [username, setUsername] = useState<string | undefined>();
const [password, setPassword] = useState<string | undefined>();
const [repeatPassword, setRepeatPassword] = useState<string | undefined>();
+ const { register } = useTestingAPI();
const { i18n } = useTranslationContext();
const errors = undefinedIfEmpty({
@@ -104,6 +108,7 @@ function RegistrationForm(): VNode {
name="register-un"
type="text"
placeholder="Username"
+ autocomplete="username"
value={username ?? ""}
onInput={(e): void => {
setUsername(e.currentTarget.value);
@@ -121,6 +126,7 @@ function RegistrationForm(): VNode {
name="register-pw"
id="register-pw"
placeholder="Password"
+ autocomplete="new-password"
value={password ?? ""}
required
onInput={(e): void => {
@@ -139,6 +145,7 @@ function RegistrationForm(): VNode {
style={{ marginBottom: 8 }}
name="register-repeat"
id="register-repeat"
+ autocomplete="new-password"
placeholder="Same password"
value={repeatPassword ?? ""}
required
@@ -155,19 +162,42 @@ function RegistrationForm(): VNode {
class="pure-button pure-button-primary btn-register"
type="submit"
disabled={!!errors}
- onClick={(e) => {
+ onClick={async (e) => {
e.preventDefault();
- if (!username || !password) return;
- registrationCall(
- { username, password },
- backend, // will store BE URL, if OK.
- pageStateSetter,
- i18n,
- );
- setUsername(undefined);
- setPassword(undefined);
- setRepeatPassword(undefined);
+ if (!username || !password) return;
+ try {
+ const credentials = { username, password };
+ await register(credentials);
+ setUsername(undefined);
+ setPassword(undefined);
+ setRepeatPassword(undefined);
+ backend.logIn(credentials);
+ onComplete();
+ } catch (error) {
+ if (error instanceof RequestError) {
+ const errorData: SandboxBackend.SandboxError =
+ error.info.error;
+ if (error.info.status === HttpStatusCode.Conflict) {
+ onError({
+ title: i18n.str`That username is already taken`,
+ description: errorData.error.description,
+ debug: JSON.stringify(error.info),
+ });
+ } else {
+ onError({
+ title: i18n.str`New registration gave response error`,
+ description: errorData.error.description,
+ debug: JSON.stringify(error.info),
+ });
+ }
+ } else if (error instanceof Error) {
+ onError({
+ title: i18n.str`Registration failed, please report`,
+ description: error.message,
+ });
+ }
+ }
}}
>
{i18n.str`Register`}
@@ -180,7 +210,7 @@ function RegistrationForm(): VNode {
setUsername(undefined);
setPassword(undefined);
setRepeatPassword(undefined);
- route("/account");
+ onComplete();
}}
>
{i18n.str`Cancel`}
@@ -192,83 +222,3 @@ function RegistrationForm(): VNode {
</Fragment>
);
}
-
-/**
- * This function requests /register.
- *
- * This function is responsible to change two states:
- * the backend's (to store the login credentials) and
- * the page's (to indicate a successful login or a problem).
- */
-async function registrationCall(
- req: { username: string; password: string },
- /**
- * FIXME: figure out if the two following
- * functions can be retrieved somewhat from
- * the state.
- */
- backend: BackendStateHandler,
- pageStateSetter: StateUpdater<PageStateType>,
- i18n: InternationalizationAPI,
-): Promise<void> {
- const url = getBankBackendBaseUrl();
-
- const headers = new Headers();
- headers.append("Content-Type", "application/json");
- const registerEndpoint = new URL("access-api/testing/register", url);
- let res: Response;
- try {
- res = await fetch(registerEndpoint.href, {
- method: "POST",
- body: JSON.stringify({
- username: req.username,
- password: req.password,
- }),
- headers,
- });
- } catch (error) {
- logger.error(
- `Could not POST new registration to the bank (${registerEndpoint.href})`,
- error,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Registration failed, please report`,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res.ok) {
- const response = await res.json();
- if (res.status === 409) {
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: i18n.str`That username is already taken`,
- debug: JSON.stringify(response),
- },
- }));
- } else {
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: i18n.str`New registration gave response error`,
- debug: JSON.stringify(response),
- },
- }));
- }
- } else {
- // registration was ok
- backend.save({
- url,
- username: req.username,
- password: req.password,
- });
- route("/account");
- }
-}
diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx
index 3c3aae0ce..a88af9b0b 100644
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ b/packages/demobank-ui/src/pages/Routing.tsx
@@ -14,21 +14,97 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import {
+ HttpResponsePaginated,
+ useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
import { createHashHistory } from "history";
import { h, VNode } from "preact";
import Router, { route, Route } from "preact-router";
import { useEffect } from "preact/hooks";
-import { AccountPage } from "./AccountPage.js";
+import { Loading } from "../components/Loading.js";
+import { PageStateType, usePageContext } from "../context/pageState.js";
+import { HomePage } from "./HomePage.js";
+import { BankFrame } from "./BankFrame.js";
import { PublicHistoriesPage } from "./PublicHistoriesPage.js";
import { RegistrationPage } from "./RegistrationPage.js";
+function handleNotOkResult(
+ safe: string,
+ saveError: (state: PageStateType["error"]) => void,
+ i18n: ReturnType<typeof useTranslationContext>["i18n"],
+): <T, E>(result: HttpResponsePaginated<T, E>) => VNode {
+ return function handleNotOkResult2<T, E>(
+ result: HttpResponsePaginated<T, E>,
+ ): VNode {
+ if (result.clientError && result.isUnauthorized) {
+ route(safe);
+ return <Loading />;
+ }
+ if (result.clientError && result.isNotfound) {
+ route(safe);
+ return (
+ <div>Page not found, you are going to be redirected to {safe}</div>
+ );
+ }
+ if (result.loading) return <Loading />;
+ if (!result.ok) {
+ saveError({
+ title: i18n.str`The backend reported a problem: HTTP status #${result.status}`,
+ description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`,
+ debug: JSON.stringify(result.error),
+ });
+ route(safe);
+ }
+ return <div />;
+ };
+}
+
export function Routing(): VNode {
const history = createHashHistory();
+ const { pageStateSetter } = usePageContext();
+
+ function saveError(error: PageStateType["error"]): void {
+ pageStateSetter((prev) => ({ ...prev, error }));
+ }
+ const { i18n } = useTranslationContext();
return (
<Router history={history}>
- <Route path="/public-accounts" component={PublicHistoriesPage} />
- <Route path="/register" component={RegistrationPage} />
- <Route path="/account" component={AccountPage} />
+ <Route
+ path="/public-accounts"
+ component={() => (
+ <BankFrame>
+ <PublicHistoriesPage
+ onLoadNotOk={handleNotOkResult("/account", saveError, i18n)}
+ />
+ </BankFrame>
+ )}
+ />
+ <Route
+ path="/register"
+ component={() => (
+ <BankFrame>
+ <RegistrationPage
+ onError={saveError}
+ onComplete={() => {
+ route("/account");
+ }}
+ />
+ </BankFrame>
+ )}
+ />
+ <Route
+ path="/account"
+ component={() => (
+ <BankFrame>
+ <HomePage
+ onRegister={() => {
+ route("/register");
+ }}
+ />
+ </BankFrame>
+ )}
+ />
<Route default component={Redirect} to="/account" />
</Router>
);
diff --git a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
index a1b616657..2b2df3baa 100644
--- a/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
+++ b/packages/demobank-ui/src/pages/WalletWithdrawForm.tsx
@@ -14,36 +14,54 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { Logger } from "@gnu-taler/taler-util";
-import { h, VNode } from "preact";
-import { StateUpdater, useEffect, useRef } from "preact/hooks";
-import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
+import { Amounts, Logger } from "@gnu-taler/taler-util";
import {
- InternationalizationAPI,
+ RequestError,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
-import { BackendState } from "../hooks/backend.js";
-import { prepareHeaders, validateAmount } from "../utils.js";
+import { h, VNode } from "preact";
+import { useEffect, useRef, useState } from "preact/hooks";
+import { PageStateType, usePageContext } from "../context/pageState.js";
+import { useAccessAPI } from "../hooks/access.js";
+import { undefinedIfEmpty } from "../utils.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("WalletWithdrawForm");
export function WalletWithdrawForm({
focus,
currency,
+ onError,
+ onSuccess,
}: {
- currency?: string;
+ currency: string;
focus?: boolean;
+ onError: (e: PageStateType["error"]) => void;
+ onSuccess: (
+ data: SandboxBackend.Access.BankAccountCreateWithdrawalResponse,
+ ) => void;
}): VNode {
- const backend = useBackendContext();
- const { pageState, pageStateSetter } = usePageContext();
+ // const backend = useBackendContext();
+ // const { pageState, pageStateSetter } = usePageContext();
const { i18n } = useTranslationContext();
- let submitAmount: string | undefined = "5.00";
+ const { createWithdrawal } = useAccessAPI();
+ const [amount, setAmount] = useState<string | undefined>("5.00");
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (focus) ref.current?.focus();
}, [focus]);
+
+ const amountFloat = amount ? parseFloat(amount) : undefined;
+ const errors = undefinedIfEmpty({
+ amount: !amountFloat
+ ? i18n.str`required`
+ : Number.isNaN(amountFloat)
+ ? i18n.str`should be a number`
+ : amountFloat < 0
+ ? i18n.str`should be positive`
+ : undefined,
+ });
return (
<form
id="reserve-form"
@@ -63,8 +81,8 @@ export function WalletWithdrawForm({
type="text"
readonly
class="currency-indicator"
- size={currency?.length ?? 5}
- maxLength={currency?.length}
+ size={currency.length}
+ maxLength={currency.length}
tabIndex={-1}
value={currency}
/>
@@ -74,14 +92,15 @@ export function WalletWithdrawForm({
ref={ref}
id="withdraw-amount"
name="withdraw-amount"
- value={submitAmount}
+ value={amount ?? ""}
onChange={(e): void => {
- // FIXME: validate using 'parseAmount()',
- // deactivate submit button as long as
- // amount is not valid
- submitAmount = e.currentTarget.value;
+ setAmount(e.currentTarget.value);
}}
/>
+ <ShowInputErrorLabel
+ message={errors?.amount}
+ isDirty={amount !== undefined}
+ />
</div>
</p>
<p>
@@ -90,22 +109,34 @@ export function WalletWithdrawForm({
id="select-exchange"
class="pure-button pure-button-primary"
type="submit"
+ disabled={!!errors}
value={i18n.str`Withdraw`}
- onClick={(e) => {
+ onClick={async (e) => {
e.preventDefault();
- submitAmount = validateAmount(submitAmount);
- /**
- * By invalid amounts, the validator prints error messages
- * on the console, and the browser colourizes the amount input
- * box to indicate a error.
- */
- if (!submitAmount && currency) return;
- createWithdrawalCall(
- `${currency}:${submitAmount}`,
- backend.state,
- pageStateSetter,
- i18n,
- );
+ if (!amountFloat) return;
+ try {
+ const result = await createWithdrawal({
+ amount: Amounts.stringify(
+ Amounts.fromFloat(amountFloat, currency),
+ ),
+ });
+
+ onSuccess(result.data);
+ } catch (error) {
+ if (error instanceof RequestError) {
+ onError({
+ title: i18n.str`Could not create withdrawal operation`,
+ description: (error as any).error.description,
+ debug: JSON.stringify(error),
+ });
+ }
+ if (error instanceof Error) {
+ onError({
+ title: i18n.str`Something when wrong trying to start the withdrawal`,
+ description: error.message,
+ });
+ }
+ }
}}
/>
</div>
@@ -114,84 +145,84 @@ export function WalletWithdrawForm({
);
}
-/**
- * This function creates a withdrawal operation via the Access API.
- *
- * After having successfully created the withdrawal operation, the
- * user should receive a QR code of the "taler://withdraw/" type and
- * supposed to scan it with their phone.
- *
- * TODO: (1) after the scan, the page should refresh itself and inform
- * the user about the operation's outcome. (2) use POST helper. */
-async function createWithdrawalCall(
- amount: string,
- backendState: BackendState,
- pageStateSetter: StateUpdater<PageStateType>,
- i18n: InternationalizationAPI,
-): Promise<void> {
- if (backendState?.status === "loggedOut") {
- logger.error("Page has a problem: no credentials found in the state.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: i18n.str`No credentials given.`,
- },
- }));
- return;
- }
-
- let res: Response;
- try {
- const { username, password } = backendState;
- const headers = prepareHeaders(username, password);
-
- // Let bank generate withdraw URI:
- const url = new URL(
- `access-api/accounts/${backendState.username}/withdrawals`,
- backendState.url,
- );
- res = await fetch(url.href, {
- method: "POST",
- headers,
- body: JSON.stringify({ amount }),
- });
- } catch (error) {
- logger.trace("Could not POST withdrawal request to the bank", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Could not create withdrawal operation`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res.ok) {
- const response = await res.json();
- logger.error(
- `Withdrawal creation gave response error: ${response} (${res.status})`,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: i18n.str`Withdrawal creation gave response error`,
- description: response.error.description,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
-
- logger.trace("Withdrawal operation created!");
- const resp = await res.json();
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
- withdrawalInProgress: true,
- talerWithdrawUri: resp.taler_withdraw_uri,
- withdrawalId: resp.withdrawal_id,
- }));
-}
+// /**
+// * This function creates a withdrawal operation via the Access API.
+// *
+// * After having successfully created the withdrawal operation, the
+// * user should receive a QR code of the "taler://withdraw/" type and
+// * supposed to scan it with their phone.
+// *
+// * TODO: (1) after the scan, the page should refresh itself and inform
+// * the user about the operation's outcome. (2) use POST helper. */
+// async function createWithdrawalCall(
+// amount: string,
+// backendState: BackendState,
+// pageStateSetter: StateUpdater<PageStateType>,
+// i18n: InternationalizationAPI,
+// ): Promise<void> {
+// if (backendState?.status === "loggedOut") {
+// logger.error("Page has a problem: no credentials found in the state.");
+// pageStateSetter((prevState) => ({
+// ...prevState,
+
+// error: {
+// title: i18n.str`No credentials given.`,
+// },
+// }));
+// return;
+// }
+
+// let res: Response;
+// try {
+// const { username, password } = backendState;
+// const headers = prepareHeaders(username, password);
+
+// // Let bank generate withdraw URI:
+// const url = new URL(
+// `access-api/accounts/${backendState.username}/withdrawals`,
+// backendState.url,
+// );
+// res = await fetch(url.href, {
+// method: "POST",
+// headers,
+// body: JSON.stringify({ amount }),
+// });
+// } catch (error) {
+// logger.trace("Could not POST withdrawal request to the bank", error);
+// pageStateSetter((prevState) => ({
+// ...prevState,
+
+// error: {
+// title: i18n.str`Could not create withdrawal operation`,
+// description: (error as any).error.description,
+// debug: JSON.stringify(error),
+// },
+// }));
+// return;
+// }
+// if (!res.ok) {
+// const response = await res.json();
+// logger.error(
+// `Withdrawal creation gave response error: ${response} (${res.status})`,
+// );
+// pageStateSetter((prevState) => ({
+// ...prevState,
+
+// error: {
+// title: i18n.str`Withdrawal creation gave response error`,
+// description: response.error.description,
+// debug: JSON.stringify(response),
+// },
+// }));
+// return;
+// }
+
+// logger.trace("Withdrawal operation created!");
+// const resp = await res.json();
+// pageStateSetter((prevState: PageStateType) => ({
+// ...prevState,
+// withdrawalInProgress: true,
+// talerWithdrawUri: resp.taler_withdraw_uri,
+// withdrawalId: resp.withdrawal_id,
+// }));
+// }
diff --git a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
index b87b77c83..4e5c621e2 100644
--- a/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalConfirmationQuestion.tsx
@@ -15,24 +15,29 @@
*/
import { Logger } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact";
-import { StateUpdater, useMemo, useState } from "preact/hooks";
+import { useMemo, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import {
- InternationalizationAPI,
- useTranslationContext,
-} from "@gnu-taler/web-util/lib/index.browser";
-import { BackendState } from "../hooks/backend.js";
-import { prepareHeaders } from "../utils.js";
+import { usePageContext } from "../context/pageState.js";
+import { useAccessAPI } from "../hooks/access.js";
+import { undefinedIfEmpty } from "../utils.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("WithdrawalConfirmationQuestion");
+interface Props {
+ account: string;
+ withdrawalId: string;
+}
/**
* Additional authentication required to complete the operation.
* Not providing a back button, only abort.
*/
-export function WithdrawalConfirmationQuestion(): VNode {
+export function WithdrawalConfirmationQuestion({
+ account,
+ withdrawalId,
+}: Props): VNode {
const { pageState, pageStateSetter } = usePageContext();
const backend = useBackendContext();
const { i18n } = useTranslationContext();
@@ -42,10 +47,20 @@ export function WithdrawalConfirmationQuestion(): VNode {
a: Math.floor(Math.random() * 10),
b: Math.floor(Math.random() * 10),
};
- }, [pageState.withdrawalId]);
+ }, []);
+ const { confirmWithdrawal, abortWithdrawal } = useAccessAPI();
const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
-
+ const answer = parseInt(captchaAnswer ?? "", 10);
+ const errors = undefinedIfEmpty({
+ answer: !captchaAnswer
+ ? i18n.str`Answer the question before continue`
+ : Number.isNaN(answer)
+ ? i18n.str`The answer should be a number`
+ : answer !== captchaNumbers.a + captchaNumbers.b
+ ? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
+ : undefined,
+ });
return (
<Fragment>
<h1 class="nav">{i18n.str`Confirm Withdrawal`}</h1>
@@ -82,33 +97,49 @@ export function WithdrawalConfirmationQuestion(): VNode {
setCaptchaAnswer(e.currentTarget.value);
}}
/>
+ <ShowInputErrorLabel
+ message={errors?.answer}
+ isDirty={captchaAnswer !== undefined}
+ />
</p>
<p>
<button
type="submit"
class="pure-button pure-button-primary btn-confirm"
+ disabled={!!errors}
onClick={async (e) => {
e.preventDefault();
- if (
- captchaAnswer ==
- (captchaNumbers.a + captchaNumbers.b).toString()
- ) {
- await confirmWithdrawalCall(
- backend.state,
- pageState.withdrawalId,
- pageStateSetter,
- i18n,
- );
- return;
+ try {
+ await confirmWithdrawal(withdrawalId);
+ pageStateSetter((prevState) => {
+ const { talerWithdrawUri, ...rest } = prevState;
+ return {
+ ...rest,
+ info: i18n.str`Withdrawal confirmed!`,
+ };
+ });
+ } catch (error) {
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ error: {
+ title: i18n.str`Could not confirm the withdrawal`,
+ description: (error as any).error.description,
+ debug: JSON.stringify(error),
+ },
+ }));
}
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`The answer "${captchaAnswer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`,
- },
- }));
- setCaptchaAnswer(undefined);
+ // if (
+ // captchaAnswer ==
+ // (captchaNumbers.a + captchaNumbers.b).toString()
+ // ) {
+ // await confirmWithdrawalCall(
+ // backend.state,
+ // pageState.withdrawalId,
+ // pageStateSetter,
+ // i18n,
+ // );
+ // return;
+ // }
}}
>
{i18n.str`Confirm`}
@@ -118,12 +149,31 @@ export function WithdrawalConfirmationQuestion(): VNode {
class="pure-button pure-button-secondary btn-cancel"
onClick={async (e) => {
e.preventDefault();
- await abortWithdrawalCall(
- backend.state,
- pageState.withdrawalId,
- pageStateSetter,
- i18n,
- );
+ try {
+ await abortWithdrawal(withdrawalId);
+ pageStateSetter((prevState) => {
+ const { talerWithdrawUri, ...rest } = prevState;
+ return {
+ ...rest,
+ info: i18n.str`Withdrawal confirmed!`,
+ };
+ });
+ } catch (error) {
+ pageStateSetter((prevState) => ({
+ ...prevState,
+ error: {
+ title: i18n.str`Could not confirm the withdrawal`,
+ description: (error as any).error.description,
+ debug: JSON.stringify(error),
+ },
+ }));
+ }
+ // await abortWithdrawalCall(
+ // backend.state,
+ // pageState.withdrawalId,
+ // pageStateSetter,
+ // i18n,
+ // );
}}
>
{i18n.str`Cancel`}
@@ -156,188 +206,188 @@ export function WithdrawalConfirmationQuestion(): VNode {
* This function will set the confirmation status in the
* 'page state' and let the related components refresh.
*/
-async function confirmWithdrawalCall(
- backendState: BackendState,
- withdrawalId: string | undefined,
- pageStateSetter: StateUpdater<PageStateType>,
- i18n: InternationalizationAPI,
-): Promise<void> {
- if (backendState.status === "loggedOut") {
- logger.error("No credentials found.");
- pageStateSetter((prevState) => ({
- ...prevState,
+// async function confirmWithdrawalCall(
+// backendState: BackendState,
+// withdrawalId: string | undefined,
+// pageStateSetter: StateUpdater<PageStateType>,
+// i18n: InternationalizationAPI,
+// ): Promise<void> {
+// if (backendState.status === "loggedOut") {
+// logger.error("No credentials found.");
+// pageStateSetter((prevState) => ({
+// ...prevState,
- error: {
- title: i18n.str`No credentials found.`,
- },
- }));
- return;
- }
- if (typeof withdrawalId === "undefined") {
- logger.error("No withdrawal ID found.");
- pageStateSetter((prevState) => ({
- ...prevState,
+// error: {
+// title: i18n.str`No credentials found.`,
+// },
+// }));
+// return;
+// }
+// if (typeof withdrawalId === "undefined") {
+// logger.error("No withdrawal ID found.");
+// pageStateSetter((prevState) => ({
+// ...prevState,
- error: {
- title: i18n.str`No withdrawal ID found.`,
- },
- }));
- return;
- }
- let res: Response;
- try {
- const { username, password } = backendState;
- const headers = prepareHeaders(username, password);
- /**
- * NOTE: tests show that when a same object is being
- * POSTed, caching might prevent same requests from being
- * made. Hence, trying to POST twice the same amount might
- * get silently ignored.
- *
- * headers.append("cache-control", "no-store");
- * headers.append("cache-control", "no-cache");
- * headers.append("pragma", "no-cache");
- * */
+// error: {
+// title: i18n.str`No withdrawal ID found.`,
+// },
+// }));
+// return;
+// }
+// let res: Response;
+// try {
+// const { username, password } = backendState;
+// const headers = prepareHeaders(username, password);
+// /**
+// * NOTE: tests show that when a same object is being
+// * POSTed, caching might prevent same requests from being
+// * made. Hence, trying to POST twice the same amount might
+// * get silently ignored.
+// *
+// * headers.append("cache-control", "no-store");
+// * headers.append("cache-control", "no-cache");
+// * headers.append("pragma", "no-cache");
+// * */
- // Backend URL must have been stored _with_ a final slash.
- const url = new URL(
- `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
- backendState.url,
- );
- res = await fetch(url.href, {
- method: "POST",
- headers,
- });
- } catch (error) {
- logger.error("Could not POST withdrawal confirmation to the bank", error);
- pageStateSetter((prevState) => ({
- ...prevState,
+// // Backend URL must have been stored _with_ a final slash.
+// const url = new URL(
+// `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/confirm`,
+// backendState.url,
+// );
+// res = await fetch(url.href, {
+// method: "POST",
+// headers,
+// });
+// } catch (error) {
+// logger.error("Could not POST withdrawal confirmation to the bank", error);
+// pageStateSetter((prevState) => ({
+// ...prevState,
- error: {
- title: i18n.str`Could not confirm the withdrawal`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res || !res.ok) {
- const response = await res.json();
- // assume not ok if res is null
- logger.error(
- `Withdrawal confirmation gave response error (${res.status})`,
- res.statusText,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
+// error: {
+// title: i18n.str`Could not confirm the withdrawal`,
+// description: (error as any).error.description,
+// debug: JSON.stringify(error),
+// },
+// }));
+// return;
+// }
+// if (!res || !res.ok) {
+// const response = await res.json();
+// // assume not ok if res is null
+// logger.error(
+// `Withdrawal confirmation gave response error (${res.status})`,
+// res.statusText,
+// );
+// pageStateSetter((prevState) => ({
+// ...prevState,
- error: {
- title: i18n.str`Withdrawal confirmation gave response error`,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
- logger.trace("Withdrawal operation confirmed!");
- pageStateSetter((prevState) => {
- const { talerWithdrawUri, ...rest } = prevState;
- return {
- ...rest,
+// error: {
+// title: i18n.str`Withdrawal confirmation gave response error`,
+// debug: JSON.stringify(response),
+// },
+// }));
+// return;
+// }
+// logger.trace("Withdrawal operation confirmed!");
+// pageStateSetter((prevState) => {
+// const { talerWithdrawUri, ...rest } = prevState;
+// return {
+// ...rest,
- info: i18n.str`Withdrawal confirmed!`,
- };
- });
-}
+// info: i18n.str`Withdrawal confirmed!`,
+// };
+// });
+// }
-/**
- * Abort a withdrawal operation via the Access API's /abort.
- */
-async function abortWithdrawalCall(
- backendState: BackendState,
- withdrawalId: string | undefined,
- pageStateSetter: StateUpdater<PageStateType>,
- i18n: InternationalizationAPI,
-): Promise<void> {
- if (backendState.status === "loggedOut") {
- logger.error("No credentials found.");
- pageStateSetter((prevState) => ({
- ...prevState,
+// /**
+// * Abort a withdrawal operation via the Access API's /abort.
+// */
+// async function abortWithdrawalCall(
+// backendState: BackendState,
+// withdrawalId: string | undefined,
+// pageStateSetter: StateUpdater<PageStateType>,
+// i18n: InternationalizationAPI,
+// ): Promise<void> {
+// if (backendState.status === "loggedOut") {
+// logger.error("No credentials found.");
+// pageStateSetter((prevState) => ({
+// ...prevState,
- error: {
- title: i18n.str`No credentials found.`,
- },
- }));
- return;
- }
- if (typeof withdrawalId === "undefined") {
- logger.error("No withdrawal ID found.");
- pageStateSetter((prevState) => ({
- ...prevState,
+// error: {
+// title: i18n.str`No credentials found.`,
+// },
+// }));
+// return;
+// }
+// if (typeof withdrawalId === "undefined") {
+// logger.error("No withdrawal ID found.");
+// pageStateSetter((prevState) => ({
+// ...prevState,
- error: {
- title: i18n.str`No withdrawal ID found.`,
- },
- }));
- return;
- }
- let res: Response;
- try {
- const { username, password } = backendState;
- const headers = prepareHeaders(username, password);
- /**
- * NOTE: tests show that when a same object is being
- * POSTed, caching might prevent same requests from being
- * made. Hence, trying to POST twice the same amount might
- * get silently ignored. Needs more observation!
- *
- * headers.append("cache-control", "no-store");
- * headers.append("cache-control", "no-cache");
- * headers.append("pragma", "no-cache");
- * */
+// error: {
+// title: i18n.str`No withdrawal ID found.`,
+// },
+// }));
+// return;
+// }
+// let res: Response;
+// try {
+// const { username, password } = backendState;
+// const headers = prepareHeaders(username, password);
+// /**
+// * NOTE: tests show that when a same object is being
+// * POSTed, caching might prevent same requests from being
+// * made. Hence, trying to POST twice the same amount might
+// * get silently ignored. Needs more observation!
+// *
+// * headers.append("cache-control", "no-store");
+// * headers.append("cache-control", "no-cache");
+// * headers.append("pragma", "no-cache");
+// * */
- // Backend URL must have been stored _with_ a final slash.
- const url = new URL(
- `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
- backendState.url,
- );
- res = await fetch(url.href, { method: "POST", headers });
- } catch (error) {
- logger.error("Could not abort the withdrawal", error);
- pageStateSetter((prevState) => ({
- ...prevState,
+// // Backend URL must have been stored _with_ a final slash.
+// const url = new URL(
+// `access-api/accounts/${backendState.username}/withdrawals/${withdrawalId}/abort`,
+// backendState.url,
+// );
+// res = await fetch(url.href, { method: "POST", headers });
+// } catch (error) {
+// logger.error("Could not abort the withdrawal", error);
+// pageStateSetter((prevState) => ({
+// ...prevState,
- error: {
- title: i18n.str`Could not abort the withdrawal.`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res.ok) {
- const response = await res.json();
- logger.error(
- `Withdrawal abort gave response error (${res.status})`,
- res.statusText,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
+// error: {
+// title: i18n.str`Could not abort the withdrawal.`,
+// description: (error as any).error.description,
+// debug: JSON.stringify(error),
+// },
+// }));
+// return;
+// }
+// if (!res.ok) {
+// const response = await res.json();
+// logger.error(
+// `Withdrawal abort gave response error (${res.status})`,
+// res.statusText,
+// );
+// pageStateSetter((prevState) => ({
+// ...prevState,
- error: {
- title: i18n.str`Withdrawal abortion failed.`,
- description: response.error.description,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
- logger.trace("Withdrawal operation aborted!");
- pageStateSetter((prevState) => {
- const { ...rest } = prevState;
- return {
- ...rest,
+// error: {
+// title: i18n.str`Withdrawal abortion failed.`,
+// description: response.error.description,
+// debug: JSON.stringify(response),
+// },
+// }));
+// return;
+// }
+// logger.trace("Withdrawal operation aborted!");
+// pageStateSetter((prevState) => {
+// const { ...rest } = prevState;
+// return {
+// ...rest,
- info: i18n.str`Withdrawal aborted!`,
- };
- });
-}
+// info: i18n.str`Withdrawal aborted!`,
+// };
+// });
+// }
diff --git a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
index 174c19288..fd91c0e1a 100644
--- a/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
+++ b/packages/demobank-ui/src/pages/WithdrawalQRCode.tsx
@@ -15,106 +15,67 @@
*/
import { Logger } from "@gnu-taler/taler-util";
+import {
+ HttpResponsePaginated,
+ useTranslationContext,
+} from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, h, VNode } from "preact";
-import useSWR from "swr";
-import { PageStateType, usePageContext } from "../context/pageState.js";
-import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
+import { Loading } from "../components/Loading.js";
+import { usePageContext } from "../context/pageState.js";
+import { useWithdrawalDetails } from "../hooks/access.js";
import { QrCodeSection } from "./QrCodeSection.js";
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
const logger = new Logger("WithdrawalQRCode");
+
+interface Props {
+ account: string;
+ withdrawalId: string;
+ talerWithdrawUri: string;
+ onAbort: () => void;
+ onLoadNotOk: <T, E>(error: HttpResponsePaginated<T, E>) => VNode;
+}
/**
* 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({
+ account,
withdrawalId,
talerWithdrawUri,
-}: {
- withdrawalId: string;
- talerWithdrawUri: string;
-}): VNode {
- // turns true when the wallet POSTed the reserve details:
- const { pageState, pageStateSetter } = usePageContext();
- const { i18n } = useTranslationContext();
- const abortButton = (
- <a
- class="pure-button btn-cancel"
- onClick={() => {
- pageStateSetter((prevState: PageStateType) => {
- return {
- ...prevState,
- withdrawalId: undefined,
- talerWithdrawUri: undefined,
- withdrawalInProgress: false,
- };
- });
- }}
- >{i18n.str`Abort`}</a>
- );
-
+ onAbort,
+ onLoadNotOk,
+}: Props): VNode {
logger.trace(`Showing withdraw URI: ${talerWithdrawUri}`);
- // waiting for the wallet:
-
- const { data, error } = useSWR(
- `integration-api/withdrawal-operation/${withdrawalId}`,
- { refreshInterval: 1000 },
- );
- if (typeof error !== "undefined") {
- logger.error(
- `withdrawal (${withdrawalId}) was never (correctly) created at the bank...`,
- error,
- );
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
-
- error: {
- title: i18n.str`withdrawal (${withdrawalId}) was never (correctly) created at the bank...`,
- },
- }));
- return (
- <Fragment>
- <br />
- <br />
- {abortButton}
- </Fragment>
- );
+ const result = useWithdrawalDetails(account, withdrawalId);
+ if (!result.ok) {
+ return onLoadNotOk(result);
}
+ const { data } = result;
- // data didn't arrive yet and wallet didn't communicate:
- if (typeof data === "undefined")
- return <p>{i18n.str`Waiting the bank to create the operation...`}</p>;
-
- /**
- * Wallet didn't communicate withdrawal details yet:
- */
logger.trace("withdrawal status", data);
- if (data.aborted)
- pageStateSetter((prevState: PageStateType) => {
- const { withdrawalId, talerWithdrawUri, ...rest } = prevState;
- return {
- ...rest,
- withdrawalInProgress: false,
-
- error: {
- title: i18n.str`This withdrawal was aborted!`,
- },
- };
- });
+ if (data.aborted) {
+ //signal that this withdrawal is aborted
+ //will redirect to account info
+ onAbort();
+ return <Loading />;
+ }
if (!data.selection_done) {
return (
- <QrCodeSection
- talerWithdrawUri={talerWithdrawUri}
- abortButton={abortButton}
- />
+ <QrCodeSection talerWithdrawUri={talerWithdrawUri} onAbort={onAbort} />
);
}
/**
* Wallet POSTed the withdrawal details! Ask the
* user to authorize the operation (here CAPTCHA).
*/
- return <WithdrawalConfirmationQuestion />;
+ return (
+ <WithdrawalConfirmationQuestion
+ account={account}
+ withdrawalId={talerWithdrawUri}
+ />
+ );
}