/* 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, Logger, 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 { useBackendContext } from "../../context/backend.js"; import { PageStateType, usePageContext } from "../../context/pageState.js"; import { useTranslationContext } from "../../context/translation.js"; import { BackendState } from "../../hooks/backend.js"; import { prepareHeaders, undefinedIfEmpty } from "../../utils.js"; import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js"; const logger = new Logger("PaytoWireTransferForm"); export function PaytoWireTransferForm({ focus, currency, }: { focus?: boolean; currency?: string; }): VNode { const backend = useBackendContext(); 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) => ({ ...submitData, iban: e.currentTarget.value, })); }} />

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

    { submitDataSetter((submitData) => ({ ...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 === "" ) { logger.error("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, backend.state, pageStateSetter, () => submitDataSetter((p) => ({ amount: undefined, iban: undefined, subject: undefined, })), ); }} /> { submitDataSetter((p) => ({ amount: undefined, iban: undefined, subject: undefined, })); }} />

{ logger.trace("switch to raw payto form"); pageStateSetter((prevState) => ({ ...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) { 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), ); }} />

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

); } /** * 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: BackendState, pageStateSetter: StateUpdater, /** * Optional since the raw payto form doesn't have * a stateful management of the input data yet. */ cleanUpForm: () => void, ): Promise { if (backendState.status === "loggedOut") { logger.error("No credentials found."); pageStateSetter((prevState) => ({ ...prevState, error: { title: "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: `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: `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: "Wire transfer created!", })); // Only at this point the input data can // be discarded. cleanUpForm(); }