From c6f228bf142637eb72456aebabd0483d83402373 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 7 Dec 2022 12:38:50 -0300 Subject: no-fix: moved out AccountPage --- packages/demobank-ui/src/pages/home/index.tsx | 1502 ------------------------- 1 file changed, 1502 deletions(-) delete mode 100644 packages/demobank-ui/src/pages/home/index.tsx (limited to 'packages/demobank-ui/src/pages/home/index.tsx') 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 ( -
-
-

-   - { - submitDataSetter((submitData: any) => ({ - ...submitData, - iban: e.currentTarget.value, - })); - }} - /> -
- -
-   - { - submitDataSetter((submitData: any) => ({ - ...submitData, - subject: e.currentTarget.value, - })); - }} - /> -
- -
-   - -   - { - submitDataSetter((submitData: any) => ({ - ...submitData, - amount: e.currentTarget.value, - })); - }} - /> - -

- -

- { - if ( - typeof submitData === "undefined" || - typeof submitData.iban === "undefined" || - submitData.iban === "" || - typeof submitData.subject === "undefined" || - submitData.subject === "" || - typeof submitData.amount === "undefined" || - submitData.amount === "" - ) { - console.log("Not all the fields were given."); - pageStateSetter((prevState: PageStateType) => ({ - ...prevState, - - error: { - title: i18n.str`Field(s) missing.`, - }, - })); - return; - } - transactionData = { - paytoUri: `payto://iban/${ - submitData.iban - }?message=${encodeURIComponent(submitData.subject)}`, - amount: `${currency}:${submitData.amount}`, - }; - return await createTransactionCall( - transactionData, - backendState, - pageStateSetter, - () => - submitDataSetter((p) => ({ - amount: undefined, - iban: undefined, - subject: undefined, - })), - ); - }} - /> - { - submitDataSetter((p) => ({ - amount: undefined, - iban: undefined, - subject: undefined, - })); - }} - /> -

-
-

- { - console.log("switch to raw payto form"); - pageStateSetter((prevState: any) => ({ - ...prevState, - isRawPayto: true, - })); - }} - > - {i18n.str`Want to try the raw payto://-format?`} - -

-
- ); - - const errorsPayto = undefinedIfEmpty({ - rawPaytoInput: !rawPaytoInput - ? i18n.str`Missing payto address` - : !parsePaytoUri(rawPaytoInput) - ? i18n.str`Payto does not follow the pattern` - : undefined, - }); - - return ( -
-

{i18n.str`Transfer money to account identified by payto:// URI:`}

-
-

-   - { - rawPaytoInputSetter(e.currentTarget.value); - }} - /> - -
-

- Hint: - - payto://iban/[receiver-iban]?message=[subject]&amount=[{currency} - :X.Y] - -
-

-

- { - // empty string evaluates to false. - if (!rawPaytoInput) { - console.log("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, - backendState, - pageStateSetter, - () => rawPaytoInputSetter(undefined), - ); - }} - /> -

-

- { - console.log("switch to wire-transfer-form"); - pageStateSetter((prevState: any) => ({ - ...prevState, - isRawPayto: false, - })); - }} - > - {i18n.str`Use wire-transfer form?`} - -

-
-
- ); -} - -/** - * Additional authentication required to complete the operation. - * Not providing a back button, only abort. - */ -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`}

-
-
-
-
-

{i18n.str`Authorize withdrawal by solving challenge`}

-

- -   - { - captchaAnswer = e.currentTarget.value; - }} - /> -

-

- -   - -

-
-
-
-

- - 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...`}

; - - /** - * 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 ; -} - -function WalletWithdraw({ - 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 ( -
-

- -   - -   - { - // FIXME: validate using 'parseAmount()', - // deactivate submit button as long as - // amount is not valid - submitAmount = e.currentTarget.value; - }} - /> -

-

-

- { - 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}`, - backendState, - pageStateSetter, - ); - }} - /> -
-

-
- ); -} - -/** - * Let the user choose a payment option, - * then specify the details trigger the action. - */ -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`}

- -
- )} -
-
- ); -} - -/** - * Collect and submit login data. - */ -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 ( -