import { AmountString, Amounts, PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; import { CopyButton, ShowInputErrorLabel, useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { useBankCoreApiContext } from "../../context/config.js"; import { ErrorMessageMappingFor, PartialButDefined, WithIntermediate, undefinedIfEmpty, validateIBAN } from "../../utils.js"; import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; import { assertUnreachable } from "../WithdrawalOperationPage.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 = TalerCorebankApi.AccountData & { username: string, isExchange: boolean, isPublic: boolean, } 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, admin, noCashout, children, }: { focus?: boolean, children: ComponentChildren, username?: string, noCashout?: boolean, admin?: boolean, template: TalerCorebankApi.AccountData | undefined; onChange: ChangeByPurposeType[PurposeType]; purpose: PurposeType; }): VNode { const { config } = useBankCoreApiContext() const initial = initializeFromTemplate(username, template, config.default_debit_threshold); const [form, setForm] = useState(initial); const [errors, setErrors] = useState< ErrorMessageMappingFor | undefined >(undefined); const { i18n } = useTranslationContext(); function updateForm(newForm: typeof initial): void { const parsed = !newForm.cashout_payto_uri ? undefined : buildPayto("iban", newForm.cashout_payto_uri, undefined);; const trimmedAmountStr = newForm.debit_threshold?.trim(); const parsedAmount = Amounts.parse(`${config.currency}:${trimmedAmountStr}`); const errors = undefinedIfEmpty>({ cashout_payto_uri: (!newForm.cashout_payto_uri ? undefined : !parsed ? i18n.str`does not follow the pattern` : !parsed.isKnown || parsed.targetType !== "iban" ? i18n.str`only "IBAN" target are supported` : !IBAN_REGEX.test(parsed.iban) ? i18n.str`IBAN should have just uppercased letters and numbers` : validateIBAN(parsed.iban, i18n)), contact_data: undefinedIfEmpty({ email: !newForm.contact_data?.email ? undefined : !EMAIL_REGEX.test(newForm.contact_data.email) ? i18n.str`it should be an email` : undefined, phone: !newForm.contact_data?.phone ? undefined : !newForm.contact_data.phone.startsWith("+") ? i18n.str`should start with +` : !REGEX_JUST_NUMBERS_REGEX.test(newForm.contact_data.phone) ? i18n.str`phone number can't have other than numbers` : undefined, }), debit_threshold: !trimmedAmountStr ? (purpose === "create" ? i18n.str`required` : undefined) : !parsedAmount ? i18n.str`not valid` : undefined, name: !newForm.name ? i18n.str`required` : undefined, username: !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) switch (purpose) { case "create": { //typescript doesn't correctly narrow a generic type const callback = onChange as ChangeByPurposeType["create"] const result: TalerCorebankApi.RegisterAccountRequest = { cashout_payto_uri: cashoutURI, name: newForm.name!, password: getRandomPassword(), username: newForm.username!, challenge_contact_data: undefinedIfEmpty({ email: newForm.contact_data?.email, phone: newForm.contact_data?.phone, }), debit_threshold: `${config.currency}:${newForm.debit_threshold}` as AmountString, internal_payto_uri: internalURI, is_public: newForm.isPublic, is_taler_exchange: newForm.isExchange, } 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, challenge_contact_data: undefinedIfEmpty({ email: newForm.contact_data?.email, phone: newForm.contact_data?.phone, }), debit_threshold: newForm.debit_threshold as AmountString, is_taler_exchange: newForm.isExchange, name: newForm.name // is_public: newForm.isPublic } 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 in the bank

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

name of the person owner the account

{purpose !== "create" && ()}
{ if (form.contact_data) { form.contact_data.email = e.currentTarget.value; if (!form.contact_data.email) { form.contact_data.email = undefined } updateForm(structuredClone(form)); } }} autocomplete="off" />
{ if (form.contact_data) { form.contact_data.phone = e.currentTarget.value; if (!form.contact_data.email) { form.contact_data.email = undefined } updateForm(structuredClone(form)); } }} // placeholder="" autocomplete="off" />
{!noCashout &&
{ form.cashout_payto_uri = e.currentTarget.value as PaytoString; if (!form.cashout_payto_uri) { form.cashout_payto_uri = undefined } updateForm(structuredClone(form)); }} autocomplete="off" />

account number where the money is going to be sent when doing cashouts

} {admin ?
{ form.debit_threshold = e as AmountString updateForm(structuredClone(form)) }} />

how much is user able to transfer

Is an exchange
: undefined } {purpose === "create" ?
Is public

public accounts have their balance publicly accesible

: undefined }
{children}
); } function initializeFromTemplate( username: string | undefined, account: TalerCorebankApi.AccountData | undefined, default_debit_threshold: AmountString, ): WithIntermediate { const emptyAccount = { cashout_payto_uri: undefined, contact_data: undefined, payto_uri: undefined, balance: undefined, debit_threshold: Amounts.stringifyValue(default_debit_threshold) as AmountString, name: undefined, }; const emptyContact = { email: undefined, phone: undefined, }; const initial: PartialButDefined = structuredClone(account) ?? emptyAccount; if (typeof initial.contact_data === "undefined") { initial.contact_data = emptyContact; } if (initial.cashout_payto_uri) { const ac = parsePaytoUri(initial.cashout_payto_uri) if (ac?.isKnown && ac.targetType === "iban") { // we are using the cashout field for the iban number initial.cashout_payto_uri = ac.targetPath as any } } const result: WithIntermediate = initial as any // FIXME: check types result.username = username return initial as any; } function RenderPaytoDisabledField({ paytoURI }: { paytoURI: PaytoString | undefined }): VNode { const { i18n } = useTranslationContext() const payto = parsePaytoUri(paytoURI ?? ""); if (payto?.isKnown) { if (payto.targetType === "iban") { const value = payto.iban; return
value ?? ""} />

international bank account number

} if (payto.targetType === "x-taler-bank") { const value = payto.account; return
value ?? ""} />

internal account id

} if (payto.targetType === "bitcoin") { const value = payto.targetPath; return
value ?? ""} />

bitcoin address

} assertUnreachable(payto) } const value = paytoURI ?? "" return
value ?? ""} />

generic payto URI

}