summaryrefslogtreecommitdiff
path: root/packages/bank-ui/src/pages/admin/AccountForm.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bank-ui/src/pages/admin/AccountForm.tsx')
-rw-r--r--packages/bank-ui/src/pages/admin/AccountForm.tsx804
1 files changed, 804 insertions, 0 deletions
diff --git a/packages/bank-ui/src/pages/admin/AccountForm.tsx b/packages/bank-ui/src/pages/admin/AccountForm.tsx
new file mode 100644
index 000000000..c8195ddb0
--- /dev/null
+++ b/packages/bank-ui/src/pages/admin/AccountForm.tsx
@@ -0,0 +1,804 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+ */
+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 { useBankCoreApiContext } from "@gnu-taler/web-util/browser";
+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<PurposeType extends keyof ChangeByPurposeType>({
+ template,
+ username,
+ purpose,
+ onChange,
+ focus,
+ children,
+}: {
+ focus?: boolean;
+ children: ComponentChildren;
+ username?: string;
+ template: TalerCorebankApi.AccountData | undefined;
+ onChange: ChangeByPurposeType[PurposeType];
+ purpose: PurposeType;
+}): VNode {
+ const { config, url } = useBankCoreApiContext();
+ const { i18n } = useTranslationContext();
+ const { state: credentials } = useSessionState();
+ const [form, setForm] = useState<AccountFormData>({});
+
+ const [errors, setErrors] = useState<
+ ErrorMessageMappingFor<typeof defaultValue> | 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 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;
+
+ function updateForm(newForm: typeof defaultValue): void {
+ const trimmedAmountStr = newForm.debit_threshold?.trim();
+ const parsedAmount = Amounts.parse(
+ `${config.currency}:${trimmedAmountStr}`,
+ );
+
+ const errors = undefinedIfEmpty<
+ ErrorMessageMappingFor<typeof defaultValue>
+ >({
+ 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 (
+ <form
+ class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2"
+ autoCapitalize="none"
+ autoCorrect="off"
+ onSubmit={(e) => {
+ e.preventDefault();
+ }}
+ >
+ <div class="px-4 py-6 sm:p-8">
+ <div class="grid max-w-2xl grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="username"
+ >
+ {i18n.str`Login username`}
+ {editableUsername && <b style={{ color: "red" }}> *</b>}
+ </label>
+ <div class="mt-2">
+ <input
+ ref={focus && purpose === "create" ? doAutoFocus : undefined}
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="username"
+ id="username"
+ data-error={!!errors?.username && form.username !== undefined}
+ disabled={!editableUsername}
+ value={form.username ?? defaultValue.username}
+ onChange={(e) => {
+ form.username = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.username}
+ isDirty={form.username !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>Account id for authentication</i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="name"
+ >
+ {i18n.str`Full name`}
+ {editableName && <b style={{ color: "red" }}> *</b>}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="name"
+ data-error={!!errors?.name && form.name !== undefined}
+ id="name"
+ disabled={!editableName}
+ value={form.name ?? defaultValue.name}
+ onChange={(e) => {
+ form.name = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ // placeholder=""
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.name}
+ isDirty={form.name !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>Name of the account holder</i18n.Translate>
+ </p>
+ </div>
+
+ {purpose === "create" ? undefined : (
+ <TextField
+ id="internal-account"
+ label={i18n.str`Internal account`}
+ help={
+ purpose === "create"
+ ? i18n.str`If empty a random account id will be assigned`
+ : i18n.str`Share this id to receive bank transfers`
+ }
+ error={errors?.payto_uri}
+ onChange={(e) => {
+ form.payto_uri = e as PaytoString;
+ updateForm(structuredClone(form));
+ }}
+ rightIcons={
+ <CopyButton
+ class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+ getContent={() =>
+ form.payto_uri ?? defaultValue.payto_uri ?? ""
+ }
+ />
+ }
+ value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString}
+ disabled={!editableAccount}
+ />
+ )}
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="email"
+ >
+ {i18n.str`Email`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="email"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="email"
+ id="email"
+ data-error={!!errors?.email && form.email !== undefined}
+ disabled={purpose === "show"}
+ value={form.email ?? defaultValue.email}
+ onChange={(e) => {
+ form.email = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.email}
+ isDirty={form.email !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ To be used when second factor authentication is enabled
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="phone"
+ >
+ {i18n.str`Phone`}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="phone"
+ id="phone"
+ disabled={purpose === "show"}
+ value={form.phone ?? defaultValue.phone}
+ data-error={!!errors?.phone && form.phone !== undefined}
+ onChange={(e) => {
+ form.phone = e.currentTarget.value;
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.phone}
+ isDirty={form.phone !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ To be used when second factor authentication is enabled
+ </i18n.Translate>
+ </p>
+ </div>
+
+ {isCashoutEnabled && (
+ <TextField
+ id="cashout-account"
+ label={i18n.str`Cashout account`}
+ help={i18n.str`External account number where the money is going to be sent when doing cashouts`}
+ error={errors?.cashout_payto_uri}
+ onChange={(e) => {
+ form.cashout_payto_uri = e as PaytoString;
+ updateForm(structuredClone(form));
+ }}
+ value={
+ (form.cashout_payto_uri ??
+ defaultValue.cashout_payto_uri) as PaytoString
+ }
+ disabled={!editableCashout}
+ />
+ )}
+
+ <div class="sm:col-span-5">
+ <label
+ for="debit"
+ class="block text-sm font-medium leading-6 text-gray-900"
+ >{i18n.str`Max debt`}</label>
+ <InputAmount
+ name="debit"
+ left
+ currency={config.currency}
+ value={form.debit_threshold ?? defaultValue.debit_threshold}
+ onChange={
+ !editableThreshold
+ ? undefined
+ : (e) => {
+ form.debit_threshold = e as AmountString;
+ updateForm(structuredClone(form));
+ }
+ }
+ />
+ <ShowInputErrorLabel
+ message={
+ errors?.debit_threshold
+ ? String(errors?.debit_threshold)
+ : undefined
+ }
+ isDirty={form.debit_threshold !== undefined}
+ />
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ How much the balance can go below zero.
+ </i18n.Translate>
+ </p>
+ </div>
+
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>Is this account public?</i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name="is public"
+ data-enabled={
+ form.isPublic ?? defaultValue.isPublic ? "true" : "false"
+ }
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ form.isPublic = !(form.isPublic ?? defaultValue.isPublic);
+ updateForm(structuredClone(form));
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={
+ form.isPublic ?? defaultValue.isPublic ? "true" : "false"
+ }
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ <p class="mt-2 text-sm text-gray-500">
+ <i18n.Translate>
+ Public accounts have their balance publicly accessible
+ </i18n.Translate>
+ </p>
+ </div>
+
+ {purpose !== "create" || !userIsAdmin ? undefined : (
+ <div class="sm:col-span-5">
+ <div class="flex items-center justify-between">
+ <span class="flex flex-grow flex-col">
+ <span
+ class="text-sm text-black font-medium leading-6 "
+ id="availability-label"
+ >
+ <i18n.Translate>
+ Is this account a payment provider?
+ </i18n.Translate>
+ </span>
+ </span>
+ <button
+ type="button"
+ name="is exchange"
+ data-enabled={
+ form.isExchange ?? defaultValue.isExchange
+ ? "true"
+ : "false"
+ }
+ class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
+ role="switch"
+ aria-checked="false"
+ aria-labelledby="availability-label"
+ aria-describedby="availability-description"
+ onClick={() => {
+ form.isExchange = !form.isExchange;
+ updateForm(structuredClone(form));
+ }}
+ >
+ <span
+ aria-hidden="true"
+ data-enabled={
+ form.isExchange ?? defaultValue.isExchange
+ ? "true"
+ : "false"
+ }
+ class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
+ ></span>
+ </button>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ {children}
+ </form>
+ );
+}
+
+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 "<unknown>";
+ if (type === "iban" && p.targetType === "iban") return p.iban;
+ if (type === "x-taler-bank" && p.targetType === "x-taler-bank")
+ return p.account;
+ return "<unsupported>";
+}
+
+{
+ /* <div class="sm:col-span-5">
+ <label
+ class="block text-sm font-medium leading-6 text-gray-900"
+ for="cashout"
+ >
+ {}
+ </label>
+ <div class="mt-2">
+ <input
+ type="text"
+ ref={focus && purpose === "update" ? doAutoFocus : undefined}
+ data-error={!!errors?.cashout_payto_uri && form.cashout_payto_uri !== undefined}
+ class="block w-full disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ name="cashout"
+ id="cashout"
+ disabled={purpose === "show"}
+ value={form.cashout_payto_uri ?? defaultValue.cashout_payto_uri}
+ onChange={(e) => {
+ form.cashout_payto_uri = e.currentTarget.value as PaytoString;
+ if (!form.cashout_payto_uri) {
+ form.cashout_payto_uri = undefined
+ }
+ updateForm(structuredClone(form));
+ }}
+ autocomplete="off"
+ />
+ <ShowInputErrorLabel
+ message={errors?.cashout_payto_uri}
+ isDirty={form.cashout_payto_uri !== undefined}
+ />
+ </div>
+ <p class="mt-2 text-sm text-gray-500" >
+ <i18n.Translate></i18n.Translate>
+ </p>
+ </div> */
+}
+
+// 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 (
+// <div class="sm:col-span-5">
+// <label
+// class="block text-sm font-medium leading-6 text-gray-900"
+// for={name}
+// >
+// {label}
+// </label>
+// <div class="mt-2">
+// <div class="flex justify-between">
+// <input
+// type="text"
+// class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+// name={name}
+// id={name}
+// disabled={disabled}
+// value={value ?? ""}
+// onChange={(e) => {
+// onChange(e.currentTarget.value);
+// }}
+// />
+// <CopyButton
+// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+// getContent={() => value ?? ""}
+// />
+// </div>
+// <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+// </div>
+// <p class="mt-2 text-sm text-gray-500">{help}</p>
+// </div>
+// );
+// }
+// if (type === "x-taler-bank") {
+// return (
+// <div class="sm:col-span-5">
+// <label
+// class="block text-sm font-medium leading-6 text-gray-900"
+// for={name}
+// >
+// {label}
+// </label>
+// <div class="mt-2">
+// <div class="flex justify-between">
+// <input
+// type="text"
+// class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+// name={name}
+// id={name}
+// disabled={disabled}
+// value={value ?? ""}
+// onChange={(e) => {
+// onChange(e.currentTarget.value);
+// }}
+// />
+// <CopyButton
+// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+// getContent={() => value ?? ""}
+// />
+// </div>
+// <ShowInputErrorLabel message={error} isDirty={value !== undefined} />
+// </div>
+// <p class="mt-2 text-sm text-gray-500">
+// {help}
+// </p>
+// </div>
+// );
+// }
+// if (type === "bitcoin") {
+// return (
+// <div class="sm:col-span-5">
+// <label
+// class="block text-sm font-medium leading-6 text-gray-900"
+// for={name}
+// >
+// {label}
+// </label>
+// <div class="mt-2">
+// <div class="flex justify-between">
+// <input
+// type="text"
+// class="mr-4 w-full block-inline disabled:bg-gray-100 rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 data-[error=true]:ring-red-500 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+// name={name}
+// id={name}
+// disabled={disabled}
+// value={value ?? ""}
+// />
+// <CopyButton
+// class="p-2 rounded-full text-black shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 "
+// getContent={() => value ?? ""}
+// />
+// <ShowInputErrorLabel
+// message={error}
+// isDirty={value !== undefined}
+// />
+// </div>
+// </div>
+// <p class="mt-2 text-sm text-gray-500">
+// {/* <i18n.Translate>bitcoin address</i18n.Translate> */}
+// {help}
+// </p>
+// </div>
+// );
+// }
+// assertUnreachable(type);
+// }