/* 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 { AmountString, Amounts, PaytoString, TalerCorebankApi, TranslatedString, assertUnreachable, buildPayto, parsePaytoUri, stringifyPaytoUri, } from "@gnu-taler/taler-util"; import { Attention, CopyButton, ShowInputErrorLabel, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { ComponentChildren, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { VersionHint, useBankCoreApiContext } from "../../context/config.js"; import { useBackendState } from "../../hooks/backend.js"; import { ErrorMessageMappingFor, TanChannel, undefinedIfEmpty, validateIBAN, } from "../../utils.js"; import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; import { getRandomPassword } from "../rnd.js"; const IBAN_REGEX = /^[A-Z][A-Z0-9]*$/; const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const REGEX_JUST_NUMBERS_REGEX = /^\+[0-9 ]*$/; export type AccountFormData = { debit_threshold?: string; isExchange?: boolean; isPublic?: boolean; name?: string; username?: string; payto_uri?: string; cashout_payto_uri?: string; email?: string; phone?: string; tan_channel?: TanChannel | "remove"; }; type ChangeByPurposeType = { create: (a: TalerCorebankApi.RegisterAccountRequest | undefined) => void; update: (a: TalerCorebankApi.AccountReconfiguration | undefined) => void; show: undefined; }; /** * FIXME: * is_public is missing on PATCH * account email/password should require 2FA * * * @param param0 * @returns */ export function AccountForm({ template, username, purpose, onChange, focus, children, }: { focus?: boolean; children: ComponentChildren; username?: string; template: TalerCorebankApi.AccountData | undefined; onChange: ChangeByPurposeType[PurposeType]; purpose: PurposeType; }): VNode { const { config, hints } = useBankCoreApiContext(); const { i18n } = useTranslationContext(); const { state: credentials } = useBackendState(); const [form, setForm] = useState({}); const [errors, setErrors] = useState< ErrorMessageMappingFor | undefined >(undefined); const defaultValue: AccountFormData = { debit_threshold: Amounts.stringifyValue( template?.debit_threshold ?? config.default_debit_threshold, ), isExchange: template?.is_taler_exchange, isPublic: template?.is_public, name: template?.name ?? "", cashout_payto_uri: stringifyIbanPayto(template?.cashout_payto_uri) ?? ("" as PaytoString), payto_uri: stringifyIbanPayto(template?.payto_uri) ?? ("" as PaytoString), email: template?.contact_data?.email ?? "", phone: template?.contact_data?.phone ?? "", username: username ?? "", tan_channel: template?.tan_channel, }; const OLD_CASHOUT_API = hints.indexOf(VersionHint.CASHOUT_BEFORE_2FA) !== -1; const showingCurrentUserInfo = credentials.status !== "loggedIn" ? false : username === credentials.username; const userIsAdmin = credentials.status !== "loggedIn" ? false : credentials.isUserAdministrator; const editableUsername = purpose === "create"; const editableName = purpose === "create" || (purpose === "update" && (config.allow_edit_name || userIsAdmin)); const isCashoutEnabled = config.allow_conversion; const editableCashout = showingCurrentUserInfo && (purpose === "create" || (purpose === "update" && (config.allow_edit_cashout_payto_uri || userIsAdmin))); const editableThreshold = userIsAdmin && (purpose === "create" || purpose === "update"); const editableAccount = purpose === "create" && userIsAdmin; const hasPhone = !!defaultValue.phone || !!form.phone; const hasEmail = !!defaultValue.email || !!form.email; function updateForm(newForm: typeof defaultValue): void { const cashoutParsed = !newForm.cashout_payto_uri ? undefined : buildPayto("iban", newForm.cashout_payto_uri, undefined); const internalParsed = !newForm.payto_uri ? undefined : buildPayto("iban", newForm.payto_uri, undefined); const trimmedAmountStr = newForm.debit_threshold?.trim(); const parsedAmount = Amounts.parse( `${config.currency}:${trimmedAmountStr}`, ); const errors = undefinedIfEmpty< ErrorMessageMappingFor >({ cashout_payto_uri: !newForm.cashout_payto_uri ? undefined : !editableCashout ? undefined : !cashoutParsed ? i18n.str`Doesn't have the pattern of an IBAN number` : !cashoutParsed.isKnown || cashoutParsed.targetType !== "iban" ? i18n.str`Only "IBAN" target are supported` : !IBAN_REGEX.test(cashoutParsed.iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(cashoutParsed.iban, i18n), payto_uri: !newForm.payto_uri ? undefined : !editableAccount ? undefined : !internalParsed ? i18n.str`Doesn't have the pattern of an IBAN number` : !internalParsed.isKnown || internalParsed.targetType !== "iban" ? i18n.str`Only "IBAN" target are supported` : !IBAN_REGEX.test(internalParsed.iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(internalParsed.iban, i18n), email: !newForm.email ? undefined : !EMAIL_REGEX.test(newForm.email) ? i18n.str`Doesn't have the pattern of an email` : undefined, phone: !newForm.phone ? undefined : !newForm.phone.startsWith("+") // FIXME: better phone number check ? i18n.str`Should start with +` : !REGEX_JUST_NUMBERS_REGEX.test(newForm.phone) ? i18n.str`Phone number can't have other than numbers` : undefined, debit_threshold: !editableThreshold ? undefined : !trimmedAmountStr ? undefined : !parsedAmount ? i18n.str`Not valid` : undefined, name: !editableName ? undefined // disabled : !newForm.name ? i18n.str`Required` : undefined, username: !editableUsername ? undefined : !newForm.username ? i18n.str`Required` : undefined, }); setErrors(errors); setForm(newForm); if (!onChange) return; if (errors) { onChange(undefined); } else { const cashout = !newForm.cashout_payto_uri ? undefined : buildPayto("iban", newForm.cashout_payto_uri, undefined); const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout); const internal = !newForm.payto_uri ? undefined : buildPayto("iban", newForm.payto_uri, undefined); const internalURI = !internal ? undefined : stringifyPaytoUri(internal); const threshold = !parsedAmount ? undefined : Amounts.stringify(parsedAmount); switch (purpose) { case "create": { // typescript doesn't correctly narrow a generic type const callback = onChange as ChangeByPurposeType["create"]; const result: TalerCorebankApi.RegisterAccountRequest = { name: newForm.name!, password: getRandomPassword(), username: newForm.username!, contact_data: undefinedIfEmpty({ email: newForm.email, phone: newForm.phone, }), debit_threshold: threshold ?? config.default_debit_threshold, cashout_payto_uri: cashoutURI, payto_uri: internalURI, is_public: newForm.isPublic, is_taler_exchange: newForm.isExchange, tan_channel: newForm.tan_channel === "remove" ? undefined : newForm.tan_channel, }; callback(result); return; } case "update": { // typescript doesn't correctly narrow a generic type const callback = onChange as ChangeByPurposeType["update"]; const result: TalerCorebankApi.AccountReconfiguration = { cashout_payto_uri: cashoutURI, contact_data: undefinedIfEmpty({ email: newForm.email, phone: newForm.phone, }), debit_threshold: threshold, is_public: newForm.isPublic, name: newForm.name, tan_channel: newForm.tan_channel === "remove" ? null : newForm.tan_channel, }; callback(result); return; } case "show": { return; } default: { assertUnreachable(purpose); } } } } return (
{ e.preventDefault(); }} >
{ form.username = e.currentTarget.value; updateForm(structuredClone(form)); }} // placeholder="" autocomplete="off" />

Account identification

{ form.name = e.currentTarget.value; updateForm(structuredClone(form)); }} // placeholder="" autocomplete="off" />

Name of the account holder

{ form.payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} />
{ form.email = e.currentTarget.value; updateForm(structuredClone(form)); }} autocomplete="off" />
{ form.phone = e.currentTarget.value; updateForm(structuredClone(form)); }} autocomplete="off" />
{showingCurrentUserInfo && isCashoutEnabled && ( { form.cashout_payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} /> )}
{ form.debit_threshold = e as AmountString; updateForm(structuredClone(form)); } } />

How much is user able to transfer after zero balance

{purpose !== "create" || !userIsAdmin ? undefined : (
Is this a Taler Exchange?
)} {/* channel, not shown if old cashout api */} {OLD_CASHOUT_API || config.supported_tan_channels.length === 0 ? undefined : (
{config.supported_tan_channels.indexOf(TanChannel.EMAIL) === -1 ? undefined : ( )} {config.supported_tan_channels.indexOf(TanChannel.SMS) === -1 ? undefined : ( )}
)}
Is this account public?

Public accounts have their balance publicly accessible

{children}
); } function stringifyIbanPayto(s: PaytoString | undefined): string | undefined { if (s === undefined) return undefined; const p = parsePaytoUri(s); if (p === undefined) return undefined; if (!p.isKnown) return undefined; if (p.targetType !== "iban") return undefined; return p.iban; } { /*
{ form.cashout_payto_uri = e.currentTarget.value as PaytoString; if (!form.cashout_payto_uri) { form.cashout_payto_uri = undefined } updateForm(structuredClone(form)); }} autocomplete="off" />

*/ } function PaytoField({ name, label, help, type, value, disabled, onChange, error, }: { error: TranslatedString | undefined; name: string; label: TranslatedString; help: TranslatedString; onChange: (s: string) => void; type: "iban" | "x-taler-bank" | "bitcoin"; disabled?: boolean; value: string | undefined; }): VNode { if (type === "iban") { return (
{ onChange(e.currentTarget.value); }} /> value ?? ""} />

{help}

); } if (type === "x-taler-bank") { return (
value ?? ""} />

{/* internal account id */} {help}

); } if (type === "bitcoin") { return (
value ?? ""} />

{/* bitcoin address */} {help}

); } assertUnreachable(type); }