/* 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, assertUnreachable, buildPayto, parsePaytoUri, stringifyPaytoUri, } from "@gnu-taler/taler-util"; import { 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 { useSessionState } from "../../hooks/session.js"; import { ErrorMessageMappingFor, TanChannel, undefinedIfEmpty, validateIBAN, validateTalerBank, } from "../../utils.js"; import { InputAmount, TextField, doAutoFocus, } from "../PaytoWireTransferForm.js"; import { getRandomPassword } from "../rnd.js"; 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, url } = useBankCoreApiContext(); const { i18n } = useTranslationContext(); const { state: credentials } = useSessionState(); const [form, setForm] = useState({}); const [errors, setErrors] = useState< ErrorMessageMappingFor | undefined >(undefined); const paytoType = config.wire_type === "X_TALER_BANK" ? ("x-taler-bank" as const) : ("iban" as const); const cashoutPaytoType: typeof paytoType = "iban" as const; 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: getAccountId(cashoutPaytoType, template?.cashout_payto_uri) ?? ("" as PaytoString), payto_uri: getAccountId(paytoType, 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 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 = 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 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 : !newForm.cashout_payto_uri ? undefined : cashoutPaytoType === "iban" ? validateIBAN(newForm.cashout_payto_uri, i18n) : cashoutPaytoType === "x-taler-bank" ? validateTalerBank(newForm.cashout_payto_uri, i18n) : undefined, payto_uri: !newForm.payto_uri ? undefined : !editableAccount ? undefined : !newForm.payto_uri ? undefined : paytoType === "iban" ? validateIBAN(newForm.payto_uri, i18n) : paytoType === "x-taler-bank" ? validateTalerBank(newForm.payto_uri, i18n) : undefined, 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 { let cashout; if (newForm.cashout_payto_uri) switch (cashoutPaytoType) { case "x-taler-bank": { cashout = buildPayto( "x-taler-bank", url.host, newForm.cashout_payto_uri, ); break; } case "iban": { cashout = buildPayto("iban", newForm.cashout_payto_uri, undefined); break; } default: assertUnreachable(cashoutPaytoType); } const cashoutURI = !cashout ? undefined : stringifyPaytoUri(cashout); let internal; if (newForm.payto_uri) switch (paytoType) { case "x-taler-bank": { internal = buildPayto("x-taler-bank", url.host, newForm.payto_uri); break; } case "iban": { internal = buildPayto("iban", newForm.payto_uri, undefined); break; } default: assertUnreachable(paytoType); } 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 ? undefined : newForm.email, phone: !newForm.phone ? undefined : 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 ? undefined : newForm.email, phone: !newForm.phone ? undefined : 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 id for authentication

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

Name of the account holder

{purpose === "create" ? undefined : ( { form.payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} rightIcons={ form.payto_uri ?? defaultValue.payto_uri ?? "" } /> } value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString} disabled={!editableAccount} /> )}
{ form.email = e.currentTarget.value; updateForm(structuredClone(form)); }} autocomplete="off" />

To be used when second factor authentication is enabled

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

To be used when second factor authentication is enabled

{isCashoutEnabled && ( { form.cashout_payto_uri = e as PaytoString; updateForm(structuredClone(form)); }} value={ (form.cashout_payto_uri ?? defaultValue.cashout_payto_uri) as PaytoString } disabled={!editableCashout} /> )} {/* 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 : ( )}
)}
{ form.debit_threshold = e as AmountString; updateForm(structuredClone(form)); } } />

How much the balance can go below zero.

Is this account public?

Public accounts have their balance publicly accessible

{purpose !== "create" || !userIsAdmin ? undefined : (
Is this account a payment provider?
)}
{children}
); } function getAccountId( type: "iban" | "x-taler-bank", s: PaytoString | undefined, ): string | undefined { if (s === undefined) return undefined; const p = parsePaytoUri(s); if (p === undefined) return undefined; if (!p.isKnown) return ""; if (type === "iban" && p.targetType === "iban") return p.iban; if (type === "x-taler-bank" && p.targetType === "x-taler-bank") return p.account; return ""; } { /*
{ 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 ( //
// //
//
// { // onChange(e.currentTarget.value); // }} // /> // value ?? ""} // /> //
// //
//

// {help} //

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

// {/* bitcoin address */} // {help} //

//
// ); // } // assertUnreachable(type); // }