diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx
index 35681a58c..f3bc3f571 100644
--- a/packages/demobank-ui/src/components/app.tsx
+++ b/packages/demobank-ui/src/components/app.tsx
@@ -3,6 +3,23 @@ import { PageStateProvider } from "../context/pageState.js";
import { TranslationProvider } from "../context/translation.js";
import { Routing } from "../pages/Routing.js";
+/**
+ * FIXME:
+ *
+ * - INPUT elements have their 'required' attribute ignored.
+ *
+ * - the page needs a "home" button that either redirects to
+ * the profile page (when the user is logged in), or to
+ * the very initial home page.
+ *
+ * - histories 'pages' are grouped in UL elements that cause
+ * the rendering to visually separate each UL. History elements
+ * should instead line up without any separation caused by
+ * a implementation detail.
+ *
+ * - Many strings need to be i18n-wrapped.
+ */
+
const App: FunctionalComponent = () => {
return (
diff --git a/packages/demobank-ui/src/pages/Routing.tsx b/packages/demobank-ui/src/pages/Routing.tsx
index 1ef042297..7f079a7de 100644
--- a/packages/demobank-ui/src/pages/Routing.tsx
+++ b/packages/demobank-ui/src/pages/Routing.tsx
@@ -18,7 +18,7 @@ import { createHashHistory } from "history";
import { h, VNode } from "preact";
import Router, { route, Route } from "preact-router";
import { useEffect } from "preact/hooks";
-import { AccountPage } from "./home/index.js";
+import { AccountPage } from "./home/AccountPage.js";
import { PublicHistoriesPage } from "./home/PublicHistoriesPage.js";
import { RegistrationPage } from "./home/RegistrationPage.js";
diff --git a/packages/demobank-ui/src/pages/home/AccountPage.tsx b/packages/demobank-ui/src/pages/home/AccountPage.tsx
new file mode 100644
index 000000000..2bc05c332
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/AccountPage.tsx
@@ -0,0 +1,289 @@
+/*
+ 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
+ */
+
+import { Amounts, HttpStatusCode } from "@gnu-taler/taler-util";
+import { hooks } from "@gnu-taler/web-util/lib/index.browser";
+import { h, Fragment, VNode } from "preact";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import useSWR, { SWRConfig, useSWRConfig } from "swr";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { useBackendState } from "../../hooks/backend.js";
+import { bankUiSettings } from "../../settings.js";
+import { getIbanFromPayto } from "../../utils.js";
+import { BankFrame } from "./BankFrame.js";
+import { LoginForm } from "./LoginForm.js";
+import { PaymentOptions } from "./PaymentOptions.js";
+import { TalerWithdrawalQRCode } from "./TalerWithdrawalQRCode.js";
+import { Transactions } from "./Transactions.js";
+
+export function AccountPage(): VNode {
+ const [backendState, backendStateSetter] = useBackendState();
+ const { i18n } = useTranslationContext();
+ const { pageState, pageStateSetter } = usePageContext();
+
+ if (!pageState.isLoggedIn) {
+ return (
+
+
{i18n.str`Welcome to ${bankUiSettings.bankName}!`}
+
+
+ );
+ }
+
+ if (typeof backendState === "undefined") {
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ isLoggedIn: false,
+ error: {
+ title: i18n.str`Page has a problem: logged in but backend state is lost.`,
+ },
+ }));
+ return
Error: waiting for details...
;
+ }
+ console.log("Showing the profile page..");
+ return (
+
+
+
+ );
+}
+
+/**
+ * Factor out login credentials.
+ */
+function SWRWithCredentials(props: any): VNode {
+ const { username, password, backendUrl } = props;
+ const headers = new Headers();
+ headers.append("Authorization", `Basic ${btoa(`${username}:${password}`)}`);
+ console.log("Likely backend base URL", backendUrl);
+ return (
+ {
+ return fetch(backendUrl + url || "", { headers }).then((r) => {
+ if (!r.ok) throw { status: r.status, json: r.json() };
+
+ return r.json();
+ });
+ },
+ }}
+ >
+ {props.children}
+
+ );
+}
+
+/**
+ * Show only the account's balance. NOTE: the backend state
+ * is mostly needed to provide the user's credentials to POST
+ * to the bank.
+ */
+function Account(Props: any): VNode {
+ const { cache } = useSWRConfig();
+ const { accountLabel, backendState } = Props;
+ // 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 { pageState, pageStateSetter: setPageState } = usePageContext();
+ const {
+ withdrawalInProgress,
+ withdrawalId,
+ isLoggedIn,
+ talerWithdrawUri,
+ timestamp,
+ } = pageState;
+ 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();
+
+ if (typeof error !== "undefined") {
+ console.log("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: {
+ setPageState((prevState: PageStateType) => ({
+ ...prevState,
+
+ isLoggedIn: false,
+ 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
;
+ }
+ default: {
+ setPageState((prevState: PageStateType) => ({
+ ...prevState,
+
+ isLoggedIn: false,
+ error: {
+ title: i18n.str`Account information could not be retrieved.`,
+ debug: JSON.stringify(error),
+ },
+ }));
+ return
Unknown problem...
;
+ }
+ }
+ }
+ const balance = !data ? undefined : Amounts.parseOrThrow(data.balance.amount);
+ 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.
+ */
+ console.log(`maybe new withdrawal ${talerWithdrawUri}`);
+ if (talerWithdrawUri) {
+ console.log("Bank created a new Taler withdrawal");
+ return (
+
+
+
+ );
+ }
+ const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance);
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+function useTransactionPageNumber(): [number, StateUpdater] {
+ const ret = hooks.useNotNullLocalStorage("transaction-page", "0");
+ const retObj = JSON.parse(ret[0]);
+ const retSetter: StateUpdater = function (val) {
+ const newVal =
+ val instanceof Function
+ ? JSON.stringify(val(retObj))
+ : JSON.stringify(val);
+ ret[1](newVal);
+ };
+ return [retObj, retSetter];
+}
diff --git a/packages/demobank-ui/src/pages/home/LoginForm.tsx b/packages/demobank-ui/src/pages/home/LoginForm.tsx
new file mode 100644
index 000000000..f60c9f600
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/LoginForm.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
+ */
+
+import { h, VNode } from "preact";
+import { route } from "preact-router";
+import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { BackendStateType, useBackendState } from "../../hooks/backend.js";
+import { bankUiSettings } from "../../settings.js";
+import { getBankBackendBaseUrl, undefinedIfEmpty } from "../../utils.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+
+/**
+ * Collect and submit login data.
+ */
+export function LoginForm(): VNode {
+ const [backendState, backendStateSetter] = useBackendState();
+ const { pageState, pageStateSetter } = usePageContext();
+ const [username, setUsername] = useState();
+ const [password, setPassword] = useState();
+ const { i18n } = useTranslationContext();
+ const ref = useRef(null);
+ useEffect(() => {
+ ref.current?.focus();
+ }, []);
+
+ const errors = undefinedIfEmpty({
+ username: !username ? i18n.str`Missing username` : undefined,
+ password: !password ? i18n.str`Missing password` : undefined,
+ });
+
+ return (
+
+
+
+ );
+}
+
+async function loginCall(
+ req: { username: string; password: string },
+ /**
+ * FIXME: figure out if the two following
+ * functions can be retrieved from the state.
+ */
+ backendStateSetter: StateUpdater,
+ pageStateSetter: StateUpdater,
+): Promise {
+ /**
+ * Optimistically setting the state as 'logged in', and
+ * let the Account component request the balance to check
+ * whether the credentials are valid. */
+ pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
+ let baseUrl = getBankBackendBaseUrl();
+ if (!baseUrl.endsWith("/")) baseUrl += "/";
+
+ backendStateSetter((prevState) => ({
+ ...prevState,
+ url: baseUrl,
+ username: req.username,
+ password: req.password,
+ }));
+}
diff --git a/packages/demobank-ui/src/pages/home/PaymentOptions.tsx b/packages/demobank-ui/src/pages/home/PaymentOptions.tsx
new file mode 100644
index 000000000..69c8d383e
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/PaymentOptions.tsx
@@ -0,0 +1,54 @@
+import { h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { useTranslationContext } from "../../context/translation.js";
+import { PaytoWireTransferForm } from "./PaytoWireTransferForm.js";
+import { WalletWithdrawForm } from "./WalletWithdrawForm.js";
+
+/**
+ * Let the user choose a payment option,
+ * then specify the details trigger the action.
+ */
+export function PaymentOptions({ currency }: { currency?: string }): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [tab, setTab] = useState<"charge-wallet" | "wire-transfer">(
+ "charge-wallet",
+ );
+
+ return (
+
+
+
+
+
+
+ {tab === "charge-wallet" && (
+
+
{i18n.str`Obtain digital cash`}
+
+
+ )}
+ {tab === "wire-transfer" && (
+
+
{i18n.str`Transfer to bank account`}
+
+
+ )}
+
+
+ );
+}
diff --git a/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx b/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx
new file mode 100644
index 000000000..45e7cf5ca
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/PaytoWireTransferForm.tsx
@@ -0,0 +1,442 @@
+/*
+ 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
+ */
+
+import { Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
+import { hooks } from "@gnu-taler/web-util/lib/index.browser";
+import { h, VNode } from "preact";
+import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { BackendStateType, useBackendState } from "../../hooks/backend.js";
+import { prepareHeaders, undefinedIfEmpty } from "../../utils.js";
+import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
+
+export function PaytoWireTransferForm({
+ focus,
+ currency,
+}: {
+ focus?: boolean;
+ currency?: string;
+}): VNode {
+ const [backendState, backendStateSetter] = useBackendState();
+ const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
+
+ const [submitData, submitDataSetter] = useWireTransferRequestType();
+
+ const [rawPaytoInput, rawPaytoInputSetter] = useState(
+ undefined,
+ );
+ const { i18n } = useTranslationContext();
+ const ibanRegex = "^[A-Z][A-Z][0-9]+$";
+ let transactionData: TransactionRequestType;
+ const ref = useRef(null);
+ useEffect(() => {
+ if (focus) ref.current?.focus();
+ }, [focus, pageState.isRawPayto]);
+
+ let parsedAmount = undefined;
+
+ const errorsWire = {
+ iban: !submitData?.iban
+ ? i18n.str`Missing IBAN`
+ : !/^[A-Z0-9]*$/.test(submitData.iban)
+ ? i18n.str`IBAN should have just uppercased letters and numbers`
+ : undefined,
+ subject: !submitData?.subject ? i18n.str`Missing subject` : undefined,
+ amount: !submitData?.amount
+ ? i18n.str`Missing amount`
+ : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
+ ? i18n.str`Amount is not valid`
+ : Amounts.isZero(parsedAmount)
+ ? i18n.str`Should be greater than 0`
+ : undefined,
+ };
+
+ if (!pageState.isRawPayto)
+ return (
+
+ );
+}
+
+/**
+ * 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 fields. FIXME: name not matching the
+ * purpose, as this is not a HTTP request body but rather the
+ * state of the -elements.
+ */
+type WireTransferRequestTypeOpt = WireTransferRequestType | undefined;
+function useWireTransferRequestType(
+ state?: WireTransferRequestType,
+): [WireTransferRequestTypeOpt, StateUpdater] {
+ const ret = hooks.useLocalStorage(
+ "wire-transfer-request-state",
+ JSON.stringify(state),
+ );
+ const retObj: WireTransferRequestTypeOpt = ret[0]
+ ? JSON.parse(ret[0])
+ : ret[0];
+ const retSetter: StateUpdater = 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: BackendStateType | undefined,
+ pageStateSetter: StateUpdater,
+ /**
+ * Optional since the raw payto form doesn't have
+ * a stateful management of the input data yet.
+ */
+ cleanUpForm: () => void,
+): Promise {
+ let res: any;
+ try {
+ res = await postToBackend(
+ `access-api/accounts/${getUsername(backendState)}/transactions`,
+ backendState,
+ JSON.stringify(req),
+ );
+ } catch (error) {
+ console.log("Could not POST transaction request to the bank", error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `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();
+ console.log(
+ `Transfer creation gave response error: ${response} (${res.status})`,
+ );
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `Transfer creation gave response error`,
+ description: response.error.description,
+ debug: JSON.stringify(response),
+ },
+ }));
+ return;
+ }
+ // status is 200 OK here, tell the user.
+ console.log("Wire transfer created!");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ info: "Wire transfer created!",
+ }));
+
+ // Only at this point the input data can
+ // be discarded.
+ cleanUpForm();
+}
+
+/**
+ * Get username from the backend state, and throw
+ * exception if not found.
+ */
+function getUsername(backendState: BackendStateType | undefined): string {
+ if (typeof backendState === "undefined")
+ throw Error("Username can't be found in a undefined backend state.");
+
+ if (!backendState.username) {
+ throw Error("No username, must login first.");
+ }
+ return backendState.username;
+}
+
+/**
+ * Helps extracting the credentials from the state
+ * and wraps the actual call to 'fetch'. Should be
+ * enclosed in a try-catch block by the caller.
+ */
+async function postToBackend(
+ uri: string,
+ backendState: BackendStateType | undefined,
+ body: string,
+): Promise {
+ if (typeof backendState === "undefined")
+ throw Error("Credentials can't be found in a undefined backend state.");
+
+ const { username, password } = backendState;
+ const headers = prepareHeaders(username, password);
+ // Backend URL must have been stored _with_ a final slash.
+ const url = new URL(uri, backendState.url);
+ return await fetch(url.href, {
+ method: "POST",
+ headers,
+ body,
+ });
+}
diff --git a/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx b/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx
new file mode 100644
index 000000000..e3d8957b8
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/TalerWithdrawalConfirmationQuestion.tsx
@@ -0,0 +1,300 @@
+import { Fragment, h, VNode } from "preact";
+import { StateUpdater } from "preact/hooks";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { BackendStateType } from "../../hooks/backend.js";
+import { prepareHeaders } from "../../utils.js";
+
+/**
+ * Additional authentication required to complete the operation.
+ * Not providing a back button, only abort.
+ */
+export function TalerWithdrawalConfirmationQuestion(Props: any): VNode {
+ const { pageState, pageStateSetter } = usePageContext();
+ const { backendState } = Props;
+ const { i18n } = useTranslationContext();
+ const captchaNumbers = {
+ a: Math.floor(Math.random() * 10),
+ b: Math.floor(Math.random() * 10),
+ };
+ let captchaAnswer = "";
+
+ return (
+
+
{i18n.str`Confirm Withdrawal`}
+
+
+
+
+
+
+ A this point, a real bank would ask for an additional
+ authentication proof (PIN/TAN, one time password, ..), instead
+ of a simple calculation.
+
+
+
+
+
+
+ );
+}
+
+/**
+ * This function confirms a withdrawal operation AFTER
+ * the wallet has given the exchange's payment details
+ * to the bank (via the Integration API). Such details
+ * can be given by scanning a QR code or by passing the
+ * raw taler://withdraw-URI to the CLI wallet.
+ *
+ * This function will set the confirmation status in the
+ * 'page state' and let the related components refresh.
+ */
+async function confirmWithdrawalCall(
+ backendState: BackendStateType | undefined,
+ withdrawalId: string | undefined,
+ pageStateSetter: StateUpdater,
+): Promise {
+ if (typeof backendState === "undefined") {
+ console.log("No credentials found.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: "No credentials found.",
+ },
+ }));
+ return;
+ }
+ if (typeof withdrawalId === "undefined") {
+ console.log("No withdrawal ID found.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: "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) {
+ console.log("Could not POST withdrawal confirmation to the bank", error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `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
+ console.log(
+ `Withdrawal confirmation gave response error (${res.status})`,
+ res.statusText,
+ );
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `Withdrawal confirmation gave response error`,
+ debug: JSON.stringify(response),
+ },
+ }));
+ return;
+ }
+ console.log("Withdrawal operation confirmed!");
+ pageStateSetter((prevState) => {
+ const { talerWithdrawUri, ...rest } = prevState;
+ return {
+ ...rest,
+
+ info: "Withdrawal confirmed!",
+ };
+ });
+}
+
+/**
+ * Abort a withdrawal operation via the Access API's /abort.
+ */
+async function abortWithdrawalCall(
+ backendState: BackendStateType | undefined,
+ withdrawalId: string | undefined,
+ pageStateSetter: StateUpdater,
+): Promise {
+ if (typeof backendState === "undefined") {
+ console.log("No credentials found.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `No credentials found.`,
+ },
+ }));
+ return;
+ }
+ if (typeof withdrawalId === "undefined") {
+ console.log("No withdrawal ID found.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `No withdrawal ID found.`,
+ },
+ }));
+ return;
+ }
+ let res: any;
+ 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) {
+ console.log("Could not abort the withdrawal", error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `Could not abort the withdrawal.`,
+ description: (error as any).error.description,
+ debug: JSON.stringify(error),
+ },
+ }));
+ return;
+ }
+ if (!res.ok) {
+ const response = await res.json();
+ console.log(
+ `Withdrawal abort gave response error (${res.status})`,
+ res.statusText,
+ );
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `Withdrawal abortion failed.`,
+ description: response.error.description,
+ debug: JSON.stringify(response),
+ },
+ }));
+ return;
+ }
+ console.log("Withdrawal operation aborted!");
+ pageStateSetter((prevState) => {
+ const { ...rest } = prevState;
+ return {
+ ...rest,
+
+ info: "Withdrawal aborted!",
+ };
+ });
+}
diff --git a/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx b/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx
new file mode 100644
index 000000000..da4ccc45e
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/TalerWithdrawalQRCode.tsx
@@ -0,0 +1,97 @@
+import { Fragment, h, VNode } from "preact";
+import useSWR from "swr";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { QrCodeSection } from "./QrCodeSection.js";
+import { TalerWithdrawalConfirmationQuestion } from "./TalerWithdrawalConfirmationQuestion.js";
+
+/**
+ * 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 TalerWithdrawalQRCode(Props: any): VNode {
+ // turns true when the wallet POSTed the reserve details:
+ const { pageState, pageStateSetter } = usePageContext();
+ const { withdrawalId, talerWithdrawUri, backendState } = Props;
+ const { i18n } = useTranslationContext();
+ const abortButton = (
+ {
+ pageStateSetter((prevState: PageStateType) => {
+ return {
+ ...prevState,
+ withdrawalId: undefined,
+ talerWithdrawUri: undefined,
+ withdrawalInProgress: false,
+ };
+ });
+ }}
+ >{i18n.str`Abort`}
+ );
+
+ console.log(`Showing withdraw URI: ${talerWithdrawUri}`);
+ // waiting for the wallet:
+
+ const { data, error } = useSWR(
+ `integration-api/withdrawal-operation/${withdrawalId}`,
+ { refreshInterval: 1000 },
+ );
+
+ if (typeof error !== "undefined") {
+ console.log(
+ `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 (
+
+
+
+ {abortButton}
+
+ );
+ }
+
+ // data didn't arrive yet and wallet didn't communicate:
+ if (typeof data === "undefined")
+ return
{i18n.str`Waiting the bank to create the operation...`}
;
+
+ /**
+ * Wallet didn't communicate withdrawal details yet:
+ */
+ console.log("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.selection_done) {
+ return (
+
+ );
+ }
+ /**
+ * Wallet POSTed the withdrawal details! Ask the
+ * user to authorize the operation (here CAPTCHA).
+ */
+ return ;
+}
diff --git a/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx b/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx
new file mode 100644
index 000000000..842f14a5f
--- /dev/null
+++ b/packages/demobank-ui/src/pages/home/WalletWithdrawForm.tsx
@@ -0,0 +1,176 @@
+/*
+ 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
+ */
+
+import { h, VNode } from "preact";
+import { StateUpdater, useEffect, useRef } from "preact/hooks";
+import { PageStateType, usePageContext } from "../../context/pageState.js";
+import { useTranslationContext } from "../../context/translation.js";
+import { BackendStateType, useBackendState } from "../../hooks/backend.js";
+import { prepareHeaders, validateAmount } from "../../utils.js";
+
+export function WalletWithdrawForm({
+ focus,
+ currency,
+}: {
+ currency?: string;
+ focus?: boolean;
+}): VNode {
+ const [backendState, backendStateSetter] = useBackendState();
+ const { pageState, pageStateSetter } = usePageContext();
+ const { i18n } = useTranslationContext();
+ let submitAmount = "5.00";
+
+ const ref = useRef(null);
+ useEffect(() => {
+ if (focus) ref.current?.focus();
+ }, [focus]);
+ return (
+
+
+ );
+}
+
+/**
+ * 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: BackendStateType | undefined,
+ pageStateSetter: StateUpdater,
+): Promise {
+ if (typeof backendState === "undefined") {
+ console.log("Page has a problem: no credentials found in the state.");
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: "No credentials given.",
+ },
+ }));
+ return;
+ }
+
+ let res: any;
+ 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) {
+ console.log("Could not POST withdrawal request to the bank", error);
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `Could not create withdrawal operation`,
+ description: (error as any).error.description,
+ debug: JSON.stringify(error),
+ },
+ }));
+ return;
+ }
+ if (!res.ok) {
+ const response = await res.json();
+ console.log(
+ `Withdrawal creation gave response error: ${response} (${res.status})`,
+ );
+ pageStateSetter((prevState) => ({
+ ...prevState,
+
+ error: {
+ title: `Withdrawal creation gave response error`,
+ description: response.error.description,
+ debug: JSON.stringify(response),
+ },
+ }));
+ return;
+ }
+
+ console.log("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/home/index.tsx b/packages/demobank-ui/src/pages/home/index.tsx
deleted file mode 100644
index ca5cae571..000000000
--- a/packages/demobank-ui/src/pages/home/index.tsx
+++ /dev/null
@@ -1,1502 +0,0 @@
-/*
- 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
- */
-
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import { h, Fragment, VNode } from "preact";
-import useSWR, { SWRConfig, useSWRConfig } from "swr";
-
-import { Amounts, HttpStatusCode, parsePaytoUri } from "@gnu-taler/taler-util";
-import { hooks } from "@gnu-taler/web-util/lib/index.browser";
-import { route } from "preact-router";
-import { StateUpdater, useEffect, useRef, useState } from "preact/hooks";
-import { PageStateType, usePageContext } from "../../context/pageState.js";
-import { useTranslationContext } from "../../context/translation.js";
-import { BackendStateType, useBackendState } from "../../hooks/backend.js";
-import { bankUiSettings } from "../../settings.js";
-import { QrCodeSection } from "./QrCodeSection.js";
-import {
- getBankBackendBaseUrl,
- getIbanFromPayto,
- undefinedIfEmpty,
- validateAmount,
-} from "../../utils.js";
-import { BankFrame } from "./BankFrame.js";
-import { Transactions } from "./Transactions.js";
-import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
-
-/**
- * FIXME:
- *
- * - INPUT elements have their 'required' attribute ignored.
- *
- * - the page needs a "home" button that either redirects to
- * the profile page (when the user is logged in), or to
- * the very initial home page.
- *
- * - histories 'pages' are grouped in UL elements that cause
- * the rendering to visually separate each UL. History elements
- * should instead line up without any separation caused by
- * a implementation detail.
- *
- * - Many strings need to be i18n-wrapped.
- */
-
-/************
- * Helpers. *
- ***********/
-
-/**
- * Get username from the backend state, and throw
- * exception if not found.
- */
-function getUsername(backendState: BackendStateType | undefined): string {
- if (typeof backendState === "undefined")
- throw Error("Username can't be found in a undefined backend state.");
-
- if (!backendState.username) {
- throw Error("No username, must login first.");
- }
- return backendState.username;
-}
-
-/**
- * Helps extracting the credentials from the state
- * and wraps the actual call to 'fetch'. Should be
- * enclosed in a try-catch block by the caller.
- */
-async function postToBackend(
- uri: string,
- backendState: BackendStateType | undefined,
- body: string,
-): Promise {
- if (typeof backendState === "undefined")
- throw Error("Credentials can't be found in a undefined backend state.");
-
- const { username, password } = backendState;
- const headers = prepareHeaders(username, password);
- // Backend URL must have been stored _with_ a final slash.
- const url = new URL(uri, backendState.url);
- return await fetch(url.href, {
- method: "POST",
- headers,
- body,
- });
-}
-
-function useTransactionPageNumber(): [number, StateUpdater] {
- const ret = hooks.useNotNullLocalStorage("transaction-page", "0");
- const retObj = JSON.parse(ret[0]);
- const retSetter: StateUpdater = function (val) {
- const newVal =
- val instanceof Function
- ? JSON.stringify(val(retObj))
- : JSON.stringify(val);
- ret[1](newVal);
- };
- return [retObj, retSetter];
-}
-
-/**
- * Craft headers with Authorization and Content-Type.
- */
-function prepareHeaders(username?: string, password?: string): Headers {
- const headers = new Headers();
- if (username && password) {
- headers.append(
- "Authorization",
- `Basic ${window.btoa(`${username}:${password}`)}`,
- );
- }
- headers.append("Content-Type", "application/json");
- return headers;
-}
-
-/*******************
- * State managers. *
- ******************/
-
-/**
- * Stores the raw Payto value entered by the user in the state.
- */
-type RawPaytoInputType = string;
-type RawPaytoInputTypeOpt = RawPaytoInputType | undefined;
-function useRawPaytoInputType(
- state?: RawPaytoInputType,
-): [RawPaytoInputTypeOpt, StateUpdater] {
- const ret = hooks.useLocalStorage("raw-payto-input-state", state);
- const retObj: RawPaytoInputTypeOpt = ret[0];
- const retSetter: StateUpdater = function (val) {
- const newVal = val instanceof Function ? val(retObj) : val;
- ret[1](newVal);
- };
- return [retObj, retSetter];
-}
-
-/**
- * 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 fields. FIXME: name not matching the
- * purpose, as this is not a HTTP request body but rather the
- * state of the -elements.
- */
-type WireTransferRequestTypeOpt = WireTransferRequestType | undefined;
-function useWireTransferRequestType(
- state?: WireTransferRequestType,
-): [WireTransferRequestTypeOpt, StateUpdater] {
- const ret = hooks.useLocalStorage(
- "wire-transfer-request-state",
- JSON.stringify(state),
- );
- const retObj: WireTransferRequestTypeOpt = ret[0]
- ? JSON.parse(ret[0])
- : ret[0];
- const retSetter: StateUpdater = function (val) {
- const newVal =
- val instanceof Function
- ? JSON.stringify(val(retObj))
- : JSON.stringify(val);
- ret[1](newVal);
- };
- return [retObj, retSetter];
-}
-
-/**
- * Request preparators.
- *
- * These functions aim at sanitizing the input received
- * from users - for example via a HTML form - and create
- * a HTTP request object out of that.
- */
-
-/******************
- * HTTP wrappers. *
- *****************/
-
-/**
- * A 'wrapper' is typically a function that prepares one
- * particular API call and updates the state accordingly. */
-
-/**
- * Abort a withdrawal operation via the Access API's /abort.
- */
-async function abortWithdrawalCall(
- backendState: BackendStateType | undefined,
- withdrawalId: string | undefined,
- pageStateSetter: StateUpdater,
-): Promise {
- if (typeof backendState === "undefined") {
- console.log("No credentials found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `No credentials found.`,
- },
- }));
- return;
- }
- if (typeof withdrawalId === "undefined") {
- console.log("No withdrawal ID found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `No withdrawal ID found.`,
- },
- }));
- return;
- }
- let res: any;
- 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) {
- console.log("Could not abort the withdrawal", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Could not abort the withdrawal.`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res.ok) {
- const response = await res.json();
- console.log(
- `Withdrawal abort gave response error (${res.status})`,
- res.statusText,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Withdrawal abortion failed.`,
- description: response.error.description,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
- console.log("Withdrawal operation aborted!");
- pageStateSetter((prevState) => {
- const { ...rest } = prevState;
- return {
- ...rest,
-
- info: "Withdrawal aborted!",
- };
- });
-}
-
-/**
- * This function confirms a withdrawal operation AFTER
- * the wallet has given the exchange's payment details
- * to the bank (via the Integration API). Such details
- * can be given by scanning a QR code or by passing the
- * raw taler://withdraw-URI to the CLI wallet.
- *
- * This function will set the confirmation status in the
- * 'page state' and let the related components refresh.
- */
-async function confirmWithdrawalCall(
- backendState: BackendStateType | undefined,
- withdrawalId: string | undefined,
- pageStateSetter: StateUpdater,
-): Promise {
- if (typeof backendState === "undefined") {
- console.log("No credentials found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: "No credentials found.",
- },
- }));
- return;
- }
- if (typeof withdrawalId === "undefined") {
- console.log("No withdrawal ID found.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: "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) {
- console.log("Could not POST withdrawal confirmation to the bank", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `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
- console.log(
- `Withdrawal confirmation gave response error (${res.status})`,
- res.statusText,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Withdrawal confirmation gave response error`,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
- console.log("Withdrawal operation confirmed!");
- pageStateSetter((prevState) => {
- const { talerWithdrawUri, ...rest } = prevState;
- return {
- ...rest,
-
- info: "Withdrawal confirmed!",
- };
- });
-}
-
-/**
- * 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: BackendStateType | undefined,
- pageStateSetter: StateUpdater,
- /**
- * Optional since the raw payto form doesn't have
- * a stateful management of the input data yet.
- */
- cleanUpForm: () => void,
-): Promise {
- let res: any;
- try {
- res = await postToBackend(
- `access-api/accounts/${getUsername(backendState)}/transactions`,
- backendState,
- JSON.stringify(req),
- );
- } catch (error) {
- console.log("Could not POST transaction request to the bank", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `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();
- console.log(
- `Transfer creation gave response error: ${response} (${res.status})`,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Transfer creation gave response error`,
- description: response.error.description,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
- // status is 200 OK here, tell the user.
- console.log("Wire transfer created!");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- info: "Wire transfer created!",
- }));
-
- // Only at this point the input data can
- // be discarded.
- cleanUpForm();
-}
-
-/**
- * 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: BackendStateType | undefined,
- pageStateSetter: StateUpdater,
-): Promise {
- if (typeof backendState === "undefined") {
- console.log("Page has a problem: no credentials found in the state.");
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: "No credentials given.",
- },
- }));
- return;
- }
-
- let res: any;
- 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) {
- console.log("Could not POST withdrawal request to the bank", error);
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Could not create withdrawal operation`,
- description: (error as any).error.description,
- debug: JSON.stringify(error),
- },
- }));
- return;
- }
- if (!res.ok) {
- const response = await res.json();
- console.log(
- `Withdrawal creation gave response error: ${response} (${res.status})`,
- );
- pageStateSetter((prevState) => ({
- ...prevState,
-
- error: {
- title: `Withdrawal creation gave response error`,
- description: response.error.description,
- debug: JSON.stringify(response),
- },
- }));
- return;
- }
-
- console.log("Withdrawal operation created!");
- const resp = await res.json();
- pageStateSetter((prevState: PageStateType) => ({
- ...prevState,
- withdrawalInProgress: true,
- talerWithdrawUri: resp.taler_withdraw_uri,
- withdrawalId: resp.withdrawal_id,
- }));
-}
-
-async function loginCall(
- req: { username: string; password: string },
- /**
- * FIXME: figure out if the two following
- * functions can be retrieved from the state.
- */
- backendStateSetter: StateUpdater,
- pageStateSetter: StateUpdater,
-): Promise {
- /**
- * Optimistically setting the state as 'logged in', and
- * let the Account component request the balance to check
- * whether the credentials are valid. */
- pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
- let baseUrl = getBankBackendBaseUrl();
- if (!baseUrl.endsWith("/")) baseUrl += "/";
-
- backendStateSetter((prevState) => ({
- ...prevState,
- url: baseUrl,
- username: req.username,
- password: req.password,
- }));
-}
-
-/**************************
- * Functional components. *
- *************************/
-
-function PaytoWireTransfer({
- focus,
- currency,
-}: {
- focus?: boolean;
- currency?: string;
-}): VNode {
- const [backendState, backendStateSetter] = useBackendState();
- const { pageState, pageStateSetter } = usePageContext(); // NOTE: used for go-back button?
-
- const [submitData, submitDataSetter] = useWireTransferRequestType();
-
- const [rawPaytoInput, rawPaytoInputSetter] = useState(
- undefined,
- );
- const { i18n } = useTranslationContext();
- const ibanRegex = "^[A-Z][A-Z][0-9]+$";
- let transactionData: TransactionRequestType;
- const ref = useRef(null);
- useEffect(() => {
- if (focus) ref.current?.focus();
- }, [focus, pageState.isRawPayto]);
-
- let parsedAmount = undefined;
-
- const errorsWire = {
- iban: !submitData?.iban
- ? i18n.str`Missing IBAN`
- : !/^[A-Z0-9]*$/.test(submitData.iban)
- ? i18n.str`IBAN should have just uppercased letters and numbers`
- : undefined,
- subject: !submitData?.subject ? i18n.str`Missing subject` : undefined,
- amount: !submitData?.amount
- ? i18n.str`Missing amount`
- : !(parsedAmount = Amounts.parse(`${currency}:${submitData.amount}`))
- ? i18n.str`Amount is not valid`
- : Amounts.isZero(parsedAmount)
- ? i18n.str`Should be greater than 0`
- : undefined,
- };
-
- if (!pageState.isRawPayto)
- return (
-
-
- A this point, a real bank would ask for an additional
- authentication proof (PIN/TAN, one time password, ..), instead
- of a simple calculation.
-
-
-
-
-
-
- );
-}
-
-/**
- * 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.
- */
-function TalerWithdrawalQRCode(Props: any): VNode {
- // turns true when the wallet POSTed the reserve details:
- const { pageState, pageStateSetter } = usePageContext();
- const { withdrawalId, talerWithdrawUri, backendState } = Props;
- const { i18n } = useTranslationContext();
- const abortButton = (
- {
- pageStateSetter((prevState: PageStateType) => {
- return {
- ...prevState,
- withdrawalId: undefined,
- talerWithdrawUri: undefined,
- withdrawalInProgress: false,
- };
- });
- }}
- >{i18n.str`Abort`}
- );
-
- console.log(`Showing withdraw URI: ${talerWithdrawUri}`);
- // waiting for the wallet:
-
- const { data, error } = useSWR(
- `integration-api/withdrawal-operation/${withdrawalId}`,
- { refreshInterval: 1000 },
- );
-
- if (typeof error !== "undefined") {
- console.log(
- `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 (
-
-
-
- {abortButton}
-
- );
- }
-
- // data didn't arrive yet and wallet didn't communicate:
- if (typeof data === "undefined")
- return
{i18n.str`Waiting the bank to create the operation...`}
- );
-}
-
-/**
- * Show only the account's balance. NOTE: the backend state
- * is mostly needed to provide the user's credentials to POST
- * to the bank.
- */
-function Account(Props: any): VNode {
- const { cache } = useSWRConfig();
- const { accountLabel, backendState } = Props;
- // 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 { pageState, pageStateSetter: setPageState } = usePageContext();
- const {
- withdrawalInProgress,
- withdrawalId,
- isLoggedIn,
- talerWithdrawUri,
- timestamp,
- } = pageState;
- 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();
-
- if (typeof error !== "undefined") {
- console.log("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: {
- setPageState((prevState: PageStateType) => ({
- ...prevState,
-
- isLoggedIn: false,
- 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
;
- }
- default: {
- setPageState((prevState: PageStateType) => ({
- ...prevState,
-
- isLoggedIn: false,
- error: {
- title: i18n.str`Account information could not be retrieved.`,
- debug: JSON.stringify(error),
- },
- }));
- return
Unknown problem...
;
- }
- }
- }
- const balance = !data ? undefined : Amounts.parseOrThrow(data.balance.amount);
- 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.
- */
- console.log(`maybe new withdrawal ${talerWithdrawUri}`);
- if (talerWithdrawUri) {
- console.log("Bank created a new Taler withdrawal");
- return (
-
-
-
- );
- }
- const balanceValue = !balance ? undefined : Amounts.stringifyValue(balance);
-
- return (
-
-