diff options
author | Sebastian <sebasjm@gmail.com> | 2023-12-06 14:05:37 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-12-06 14:10:10 -0300 |
commit | 35ee1ddd797be94557ab23f500c69fabee468387 (patch) | |
tree | e618fab464884275b484c599fc81ae9391002572 /packages/demobank-ui/src/pages/admin/AccountForm.tsx | |
parent | 71acfc274c9cfef70d9c2ed542958434ac94b137 (diff) | |
download | wallet-core-35ee1ddd797be94557ab23f500c69fabee468387.tar.gz wallet-core-35ee1ddd797be94557ab23f500c69fabee468387.tar.bz2 wallet-core-35ee1ddd797be94557ab23f500c69fabee468387.zip |
account form
Diffstat (limited to 'packages/demobank-ui/src/pages/admin/AccountForm.tsx')
-rw-r--r-- | packages/demobank-ui/src/pages/admin/AccountForm.tsx | 654 |
1 files changed, 330 insertions, 324 deletions
diff --git a/packages/demobank-ui/src/pages/admin/AccountForm.tsx b/packages/demobank-ui/src/pages/admin/AccountForm.tsx index 61702f7d4..c64431918 100644 --- a/packages/demobank-ui/src/pages/admin/AccountForm.tsx +++ b/packages/demobank-ui/src/pages/admin/AccountForm.tsx @@ -1,4 +1,4 @@ -import { AmountString, Amounts, PaytoString, TalerCorebankApi, buildPayto, parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +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"; @@ -7,16 +7,23 @@ import { ErrorMessageMappingFor, PartialButDefined, WithIntermediate, undefinedI 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 = TalerCorebankApi.AccountData & { - username: string, - isExchange: boolean, - isPublic: boolean, +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 = { @@ -39,69 +46,102 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ 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 { i18n } = useTranslationContext(); + const { state: credentials } = useBackendState(); + const [form, setForm] = useState<AccountFormData>({}); - const initial = initializeFromTemplate(username, template, config.default_debit_threshold); - const [form, setForm] = useState(initial); const [errors, setErrors] = useState< - ErrorMessageMappingFor<typeof initial> | undefined + ErrorMessageMappingFor<typeof defaultValue> | undefined >(undefined); - const { i18n } = useTranslationContext(); - function updateForm(newForm: typeof initial): void { - const parsed = !newForm.cashout_payto_uri + + 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<ErrorMessageMappingFor<typeof initial>>({ + const errors = undefinedIfEmpty<ErrorMessageMappingFor<typeof defaultValue>>({ 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, + ? 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; @@ -114,23 +154,25 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ 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 = { - 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, + contact_data: undefinedIfEmpty({ + email: newForm.email, + phone: newForm.phone, }), - debit_threshold: `${config.currency}:${newForm.debit_threshold}` as AmountString, - internal_payto_uri: internalURI, - is_public: newForm.isPublic, - is_taler_exchange: newForm.isExchange, + 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; @@ -138,17 +180,16 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ 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, + contact_data: undefinedIfEmpty({ + email: newForm.email ?? template?.contact_data?.email, + phone: newForm.phone ?? template?.contact_data?.phone, }), - debit_threshold: newForm.debit_threshold as AmountString, - is_taler_exchange: newForm.isExchange, - name: newForm.name - // is_public: newForm.isPublic + debit_threshold: threshold, + is_public: !!newForm.isPublic, + name: newForm.name, } callback(result) return; @@ -162,7 +203,6 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ } } } - return ( <form class="bg-white shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl md:col-span-2" @@ -181,7 +221,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ for="username" > {i18n.str`Username`} - {purpose === "create" && <b style={{ color: "red" }}> *</b>} + {editableUsername && <b style={{ color: "red" }}> *</b>} </label> <div class="mt-2"> <input @@ -191,8 +231,8 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ name="username" id="username" data-error={!!errors?.username && form.username !== undefined} - disabled={purpose !== "create"} - value={form.username ?? ""} + disabled={!editableUsername} + value={form.username ?? defaultValue.username} onChange={(e) => { form.username = e.currentTarget.value; updateForm(structuredClone(form)); @@ -216,7 +256,7 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ for="name" > {i18n.str`Name`} - {purpose === "create" && <b style={{ color: "red" }}> *</b>} + {editableName && <b style={{ color: "red" }}> *</b>} </label> <div class="mt-2"> <input @@ -225,8 +265,8 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ name="name" data-error={!!errors?.name && form.name !== undefined} id="name" - disabled={purpose !== "create"} - value={form.name ?? ""} + disabled={!editableName} + value={form.name ?? defaultValue.name} onChange={(e) => { form.name = e.currentTarget.value; updateForm(structuredClone(form)); @@ -245,7 +285,20 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ </div> - {purpose !== "create" && (<RenderPaytoDisabledField paytoURI={form.payto_uri as PaytoString} />)} + <PaytoField + type="iban" + name="internal-account" + label={i18n.str`Internal IBAN`} + help={purpose === "create" ? + i18n.str`if empty a random account number will be assigned` : + i18n.str`account identification for bank transfer`} + value={(form.payto_uri ?? defaultValue.payto_uri) as PaytoString} + disabled={!editableAccount} + error={errors?.payto_uri} + onChange={(e) => { + form.payto_uri = e as PaytoString + updateForm(structuredClone(form)) + }} /> <div class="sm:col-span-5"> <label @@ -260,23 +313,18 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ 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?.contact_data?.email && form.contact_data?.email !== undefined} + data-error={!!errors?.email && form.email !== undefined} disabled={purpose === "show"} - value={form.contact_data?.email ?? ""} + value={form.email ?? defaultValue.email} onChange={(e) => { - if (form.contact_data) { - form.contact_data.email = e.currentTarget.value; - if (!form.contact_data.email) { - form.contact_data.email = undefined - } - updateForm(structuredClone(form)); - } + form.email = e.currentTarget.value; + updateForm(structuredClone(form)); }} autocomplete="off" /> <ShowInputErrorLabel - message={errors?.contact_data?.email} - isDirty={form.contact_data?.email !== undefined} + message={errors?.email} + isDirty={form.email !== undefined} /> </div> </div> @@ -295,85 +343,56 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ name="phone" id="phone" disabled={purpose === "show"} - value={form.contact_data?.phone ?? ""} - data-error={!!errors?.contact_data?.phone && form.contact_data?.phone !== undefined} + value={form.phone ?? defaultValue.phone} + data-error={!!errors?.phone && form.phone !== undefined} onChange={(e) => { - if (form.contact_data) { - form.contact_data.phone = e.currentTarget.value; - if (!form.contact_data.email) { - form.contact_data.email = undefined - } - updateForm(structuredClone(form)); - } + form.phone = e.currentTarget.value; + updateForm(structuredClone(form)); }} - // placeholder="" autocomplete="off" /> <ShowInputErrorLabel - message={errors?.contact_data?.phone} - isDirty={form.contact_data?.phone !== undefined} + message={errors?.phone} + isDirty={form.phone !== undefined} /> </div> </div> - {!noCashout && - <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="cashout" - > - {i18n.str`Cashout IBAN`} - </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 ?? ""} - 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>account number where the money is going to be sent when doing cashouts</i18n.Translate> - </p> - </div> + {showingCurrentUserInfo && + <PaytoField + type="iban" + name="cashout-account" + label={i18n.str`Cashout IBAN`} + help={i18n.str`account number where the money is going to be sent when doing cashouts`} + value={(form.cashout_payto_uri ?? defaultValue.cashout_payto_uri) as PaytoString} + disabled={!editableCashout} + error={errors?.cashout_payto_uri} + onChange={(e) => { + form.cashout_payto_uri = e as PaytoString + updateForm(structuredClone(form)) + }} /> } - {admin ? <Fragment> - <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 ?? ""} - onChange={(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" >how much is user able to transfer </p> - </div> + <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" >how much is user able to transfer </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"> @@ -381,210 +400,197 @@ export function AccountForm<PurposeType extends keyof ChangeByPurposeType>({ <i18n.Translate>Is an exchange</i18n.Translate> </span> </span> - <button type="button" data-enabled={!!form.isExchange} 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" + <button type="button" 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} 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> + <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> - </Fragment> : - undefined - } + </div>} - {purpose === "create" ? - <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 public</i18n.Translate> - </span> + <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 public</i18n.Translate> </span> - <button type="button" data-enabled={!!form.isPublic} 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" + </span> + <button type="button" 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 - updateForm(structuredClone(form)) - }}> - <span aria-hidden="true" data-enabled={!!form.isPublic} 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 accesible</i18n.Translate> - </p> + onClick={() => { + form.isPublic = !form.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> - : undefined - } + <p class="mt-2 text-sm text-gray-500" > + <i18n.Translate>public accounts have their balance publicly accesible</i18n.Translate> + </p> + </div> </div> </div> + <pre> + {JSON.stringify(errors, undefined, 2)} + </pre> {children} </form> ); } -function initializeFromTemplate( - username: string | undefined, - account: TalerCorebankApi.AccountData | undefined, - default_debit_threshold: AmountString, -): WithIntermediate<AccountFormData> { - 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<TalerCorebankApi.AccountData> = - 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<AccountFormData> = initial as any // FIXME: check types - result.username = username - return initial as any; +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 } +{/* <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 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 <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="internal-iban" - > - {i18n.str`Internal IBAN`} - </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="internal-iban" - id="internal-iban" - disabled={true} - 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 ?? ""} - /> - </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> - <p class="mt-2 text-sm text-gray-500" > - <i18n.Translate>international bank account number</i18n.Translate> - </p> - </div> - } - if (payto.targetType === "x-taler-bank") { - const value = payto.account; - return <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="account-id" - > - {i18n.str`Account ID`} - </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="account-id" - id="account-id" - disabled={true} - 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 ?? ""} - /> - </div> - </div> - <p class="mt-2 text-sm text-gray-500" > - <i18n.Translate>internal account id</i18n.Translate> - </p> + <ShowInputErrorLabel + message={error} + isDirty={value !== undefined} + /> </div> - } - if (payto.targetType === "bitcoin") { - const value = payto.targetPath; - return <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="account-id" - > - {i18n.str`Bitcoin address`} - </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="account-id" - id="account-id" - disabled={true} - 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 ?? ""} - /> - </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 ?? ""} + /> + <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> - <p class="mt-2 text-sm text-gray-500" > - <i18n.Translate>bitcoin address</i18n.Translate> - </p> + <ShowInputErrorLabel + message={error} + isDirty={value !== undefined} + /> </div> - } - assertUnreachable(payto) + <p class="mt-2 text-sm text-gray-500" > + {/* <i18n.Translate>internal account id</i18n.Translate> */} + {help} + </p> + </div> } - - const value = paytoURI ?? "" - return <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="internal-payto" - > - {i18n.str`Internal account`} - </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="internal-payto" - id="internal-payto" - disabled={true} - 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 ?? ""} - /> + 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> - <p class="mt-2 text-sm text-gray-500" > - <i18n.Translate>generic payto URI</i18n.Translate> - </p> - </div> + } + assertUnreachable(type) } |