import { AmountString, Amounts, PaytoString, TalerCorebankApi, TranslatedString, 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"; import { useBackendState } from "../../hooks/backend.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, } 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 } = 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 ?? "", } 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 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 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>({ cashout_payto_uri: (!newForm.cashout_payto_uri ? undefined : !editableCashout ? undefined : !cashoutParsed ? i18n.str`does not follow the pattern` : !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`does not follow the pattern` : !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`it should be 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, } 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 ?? template?.contact_data?.email, phone: newForm.phone ?? template?.contact_data?.phone, }), debit_threshold: threshold, is_public: !!newForm.isPublic, name: newForm.name, } 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

{ 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 && { 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

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

public accounts have their balance publicly accesible

        {JSON.stringify(errors, undefined, 2)}
      
{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) }