/* This file is part of GNU Taler (C) 2022-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ import { AbsoluteTime, AmountJson, AmountString, Amounts, CurrencySpecification, FRAC_SEPARATOR, HttpStatusCode, PaytoString, TalerErrorCode, TranslatedString, assertUnreachable, buildPayto, parsePaytoUri, stringifyPaytoUri, } from "@gnu-taler/taler-util"; import { LocalNotificationBanner, ShowInputErrorLabel, notifyInfo, useLocalNotification, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Ref, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { mutate } from "swr"; import { useBankCoreApiContext } from "../context/config.js"; import { useBackendState } from "../hooks/backend.js"; import { useBankState } from "../hooks/bank-state.js"; import { RouteDefinition } from "../route.js"; import { undefinedIfEmpty, validateIBAN } from "../utils.js"; export function PaytoWireTransferForm({ focus, title, toAccount, onSuccess, routeCancel, onAuthorizationRequired, limit, }: { title: TranslatedString; focus?: boolean; toAccount?: string; onSuccess: () => void; onAuthorizationRequired: () => void; routeCancel?: RouteDefinition>; limit: AmountJson; }): VNode { const [isRawPayto, setIsRawPayto] = useState(false); const { state: credentials } = useBackendState(); const { api } = useBankCoreApiContext(); const sendingToFixedAccount = toAccount !== undefined; // FIXME: support other destination that just IBAN const [iban, setIban] = useState(toAccount); const [subject, setSubject] = useState(); const [amount, setAmount] = useState(); const [, updateBankState] = useBankState(); const [rawPaytoInput, rawPaytoInputSetter] = useState( undefined, ); const { i18n } = useTranslationContext(); const ibanRegex = "^[A-Z][A-Z][0-9]+$"; const trimmedAmountStr = amount?.trim(); const parsedAmount = Amounts.parse(`${limit.currency}:${trimmedAmountStr}`); const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; const [notification, notify, handleError] = useLocalNotification(); const errorsWire = undefinedIfEmpty({ iban: !iban ? i18n.str`Required` : !IBAN_REGEX.test(iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(iban, i18n), subject: !subject ? i18n.str`Required` : undefined, amount: !trimmedAmountStr ? i18n.str`Required` : !parsedAmount ? i18n.str`Not valid` : Amounts.isZero(parsedAmount) ? i18n.str`Should be greater than 0` : Amounts.cmp(limit, parsedAmount) === -1 ? i18n.str`Balance is not enough` : undefined, }); const parsed = !rawPaytoInput ? undefined : parsePaytoUri(rawPaytoInput); const errorsPayto = undefinedIfEmpty({ rawPaytoInput: !rawPaytoInput ? i18n.str`Required` : !parsed ? i18n.str`Does not follow the pattern` : !parsed.isKnown || parsed.targetType !== "iban" ? i18n.str`Only "IBAN" target are supported` : !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` : !IBAN_REGEX.test(parsed.iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(parsed.iban, i18n), }); async function doSend() { let payto_uri: PaytoString | undefined; let sendingAmount: AmountString | undefined; if (credentials.status !== "loggedIn") return; if (rawPaytoInput) { const p = parsePaytoUri(rawPaytoInput); if (!p) return; sendingAmount = p.params.amount as AmountString; delete p.params.amount; // if this payto is valid then it already have message payto_uri = stringifyPaytoUri(p); } else { if (!iban || !subject) return; const ibanPayto = buildPayto("iban", iban, undefined); ibanPayto.params.message = encodeURIComponent(subject); payto_uri = stringifyPaytoUri(ibanPayto); sendingAmount = `${limit.currency}:${trimmedAmountStr}` as AmountString; } const puri = payto_uri; const sAmount = sendingAmount; await handleError(async () => { const request = { payto_uri: puri, amount: sAmount, }; const resp = await api.createTransaction(credentials, request); mutate(() => true); if (resp.type === "fail") { switch (resp.case) { case HttpStatusCode.BadRequest: return notify({ type: "error", title: i18n.str`The request was invalid or the payto://-URI used unacceptable features.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); case HttpStatusCode.Unauthorized: return notify({ type: "error", title: i18n.str`Not enough permission to complete the operation.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); case TalerErrorCode.BANK_UNKNOWN_CREDITOR: return notify({ type: "error", title: i18n.str`The destination account "${puri}" was not found.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); case TalerErrorCode.BANK_SAME_ACCOUNT: return notify({ type: "error", title: i18n.str`The origin and the destination of the transfer can't be the same.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); case TalerErrorCode.BANK_UNALLOWED_DEBIT: return notify({ type: "error", title: i18n.str`Your balance is not enough.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); case HttpStatusCode.NotFound: return notify({ type: "error", title: i18n.str`The origin account "${puri}" was not found.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); case HttpStatusCode.Accepted: { updateBankState("currentChallenge", { operation: "create-transaction", id: String(resp.body.challenge_id), sent: AbsoluteTime.never(), request, }); return onAuthorizationRequired(); } default: assertUnreachable(resp); } } notifyInfo(i18n.str`Wire transfer created!`); onSuccess(); setAmount(undefined); setIban(undefined); setSubject(undefined); rawPaytoInputSetter(undefined); }); } return (
{/** * FIXME: Scan a qr code */}

{title}

{sendingToFixedAccount ? undefined : ( )}
{ e.preventDefault(); }} >
{!isRawPayto ? (
{ setIban(e.currentTarget.value.toUpperCase()); }} />

IBAN of the recipient's account

{ setSubject(e.currentTarget.value); }} />

Some text to identify the transfer

{ setAmount(d); }} />

Amount to transfer

) : (