diff options
Diffstat (limited to 'packages/demobank-ui/src')
10 files changed, 402 insertions, 393 deletions
diff --git a/packages/demobank-ui/src/components/app.tsx b/packages/demobank-ui/src/components/app.tsx index 27898caeb..4921b6bff 100644 --- a/packages/demobank-ui/src/components/app.tsx +++ b/packages/demobank-ui/src/components/app.tsx @@ -79,7 +79,7 @@ export default App; function getInitialBackendBaseURL(backendFromSettings: string | undefined): string { const overrideUrl = typeof localStorage !== "undefined" - ? localStorage.getItem("bank-base-url") + ? localStorage.getItem("corebank-api-base-url") : undefined; let result: string; diff --git a/packages/demobank-ui/src/pages/BankFrame.tsx b/packages/demobank-ui/src/pages/BankFrame.tsx index 0ac9ed8f1..737a00b57 100644 --- a/packages/demobank-ui/src/pages/BankFrame.tsx +++ b/packages/demobank-ui/src/pages/BankFrame.tsx @@ -119,10 +119,7 @@ export function BankFrame({ </main> <Footer - testingUrl={ - (typeof localStorage !== "undefined") && localStorage.getItem("bank-base-url") ? - localStorage.getItem("bank-base-url") ?? undefined : - undefined} + testingUrlKey="corebank-api-base-url" GIT_HASH={GIT_HASH} VERSION={VERSION} /> diff --git a/packages/demobank-ui/src/pages/ProfileNavigation.tsx b/packages/demobank-ui/src/pages/ProfileNavigation.tsx index 61a55fe16..5b0f09360 100644 --- a/packages/demobank-ui/src/pages/ProfileNavigation.tsx +++ b/packages/demobank-ui/src/pages/ProfileNavigation.tsx @@ -2,10 +2,13 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useBankCoreApiContext } from "../context/config.js"; import { assertUnreachable } from "./WithdrawalOperationPage.js"; +import { useBackendState } from "../hooks/backend.js"; -export function ProfileNavigation({ current, noCashout }: { noCashout?: boolean, current: "details" | "credentials" | "cashouts" }): VNode { +export function ProfileNavigation({ current }: { current: "details" | "credentials" | "cashouts" }): VNode { const { i18n } = useTranslationContext() const { config } = useBankCoreApiContext() + const { state: credentials } = useBackendState(); + const nonAdminUser = credentials.status !== "loggedIn" ? false : !credentials.isUserAdministrator return <div> <div class="sm:hidden"> <label for="tabs" class="sr-only"><i18n.Translate>Select a section</i18n.Translate></label> @@ -44,7 +47,7 @@ export function ProfileNavigation({ current, noCashout }: { noCashout?: boolean, <span><i18n.Translate>Credentials</i18n.Translate></span> <span aria-hidden="true" data-selected={current == "credentials"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span> </a> - {config.allow_conversion && !noCashout ? + {config.allow_conversion && nonAdminUser ? <a href="#/my-cashouts" data-selected={current == "cashouts"} class="rounded-r-lg text-gray-500 hover:text-gray-700 data-[selected=true]:text-gray-900 group relative min-w-0 flex-1 overflow-hidden bg-white py-4 px-4 text-center text-sm font-medium hover:bg-gray-50 focus:z-10"> <span>Cashouts</span> <span aria-hidden="true" data-selected={current == "cashouts"} class="bg-transparent data-[selected=true]:bg-indigo-500 absolute inset-x-0 bottom-0 h-0.5"></span> diff --git a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx index eb6f6fd27..08503fb9b 100644 --- a/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx +++ b/packages/demobank-ui/src/pages/PublicHistoriesPage.tsx @@ -35,7 +35,7 @@ export function PublicHistoriesPage({ }: Props): VNode { //TODO: implemented filter by account name const result = usePublicAccounts(undefined); const firstAccount = result && !(result instanceof TalerError) && result.data.public_accounts.length > 0 - ? result.data.public_accounts[0].account_name + ? result.data.public_accounts[0].username : undefined; const [showAccount, setShowAccount] = useState(firstAccount); @@ -54,8 +54,8 @@ export function PublicHistoriesPage({ }: Props): VNode { // Ask story of all the public accounts. for (const account of data.public_accounts) { - logger.trace("Asking transactions for", account.account_name); - const isSelected = account.account_name == showAccount; + logger.trace("Asking transactions for", account.username); + const isSelected = account.username == showAccount; accountsBar.push( <li class={ @@ -67,13 +67,13 @@ export function PublicHistoriesPage({ }: Props): VNode { <a href="#" class="pure-menu-link" - onClick={() => setShowAccount(account.account_name)} + onClick={() => setShowAccount(account.username)} > - {account.account_name} + {account.username} </a> </li>, ); - txs[account.account_name] = <Transactions account={account.account_name} />; + txs[account.username] = <Transactions account={account.username} />; } return ( diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx index 92419b7ed..994a8286e 100644 --- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx @@ -104,7 +104,7 @@ export function ShowAccountDetails({ <Fragment> <LocalNotificationBanner notification={notification} /> {accountIsTheCurrentUser ? - <ProfileNavigation current="details" noCashout={credentials.status === "loggedIn" ? credentials.isUserAdministrator : undefined} /> + <ProfileNavigation current="details" /> : <h1 class="text-base font-semibold leading-6 text-gray-900"> <i18n.Translate>Account "{account}"</i18n.Translate> @@ -133,9 +133,7 @@ export function ShowAccountDetails({ <AccountForm focus={update} - noCashout={credentials.status === "loggedIn" ? credentials.isUserAdministrator : undefined} username={account} - admin={credentials.status === "loggedIn" ? credentials.isUserAdministrator : undefined} template={result.body} purpose={update ? "update" : "show"} onChange={(a) => setSubmitAccount(a)} diff --git a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx index eef2a0692..ece1f63e7 100644 --- a/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx +++ b/packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx @@ -83,7 +83,7 @@ export function UpdateAccountPassword({ <Fragment> <LocalNotificationBanner notification={notification} /> {accountIsTheCurrentUser ? - <ProfileNavigation current="credentials" noCashout={credentials.status === "loggedIn" ? credentials.isUserAdministrator : undefined} /> : + <ProfileNavigation current="credentials" /> : <h1 class="text-base font-semibold leading-6 text-gray-900"> <i18n.Translate>Account "{accountName}"</i18n.Translate> </h1> 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) } diff --git a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx index 6ff723a31..3d196973e 100644 --- a/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx +++ b/packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx @@ -113,7 +113,6 @@ export function CreateNewAccount({ </div> <AccountForm template={undefined} - admin purpose="create" onChange={(a) => { setSubmitAccount(a); diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index ce1a6cf49..4b3077984 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -433,61 +433,64 @@ export function CreateCashout({ )} {/* channel */} - <div class="sm:col-span-5"> - <label - class="block text-sm font-medium leading-6 text-gray-900" - for="channel" - > - {i18n.str`Confirmation the operation using`} - </label> - - <div class="mt-2 max-w-xl text-sm text-gray-500"> - <div class="px-4 mt-4 grid grid-cols-1 gap-y-6"> - - <label onClick={() => { - if (!resultAccount.body.contact_data?.email) return; - form.channel = TanChannel.EMAIL - updateForm(structuredClone(form)) - }} data-disabled={!resultAccount.body.contact_data?.email} data-selected={form.channel === TanChannel.EMAIL} - class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"> - <input type="radio" name="channel" value="Newsletter" class="sr-only" /> - <span class="flex flex-1"> - <span class="flex flex-col"> - <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 "> - <i18n.Translate>Email</i18n.Translate> + {config.supported_tan_channels.length === 0 ? undefined : + <div class="sm:col-span-5"> + <label + class="block text-sm font-medium leading-6 text-gray-900" + for="channel" + > + {i18n.str`Confirmation the operation using`} + </label> + <div class="mt-2 max-w-xl text-sm text-gray-500"> + <div class="px-4 mt-4 grid grid-cols-1 gap-y-6"> + {config.supported_tan_channels.indexOf(TanChannel.EMAIL) === -1 ? undefined : + <label onClick={() => { + if (!resultAccount.body.contact_data?.email) return; + form.channel = TanChannel.EMAIL + updateForm(structuredClone(form)) + }} data-disabled={!resultAccount.body.contact_data?.email} data-selected={form.channel === TanChannel.EMAIL} + class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border bg-white data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"> + <input type="radio" name="channel" value="Newsletter" class="sr-only" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-0-label" class="block text-sm font-medium text-gray-900 "> + <i18n.Translate>Email</i18n.Translate> + </span> + {!resultAccount.body.contact_data?.email && i18n.str`add a email in your profile to enable this option`} + </span> </span> - {!resultAccount.body.contact_data?.email && i18n.str`add a email in your profile to enable this option`} - </span> - </span> - <svg data-selected={form.channel === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> - </svg> - </label> - - <label onClick={() => { - if (!resultAccount.body.contact_data?.phone) return; - form.channel = TanChannel.SMS - updateForm(structuredClone(form)) - }} data-disabled={!resultAccount.body.contact_data?.phone} data-selected={form.channel === TanChannel.SMS} - class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"> - <input type="radio" name="channel" value="Existing Customers" class="sr-only" /> - <span class="flex flex-1"> - <span class="flex flex-col"> - <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> - <i18n.Translate>SMS</i18n.Translate> + <svg data-selected={form.channel === TanChannel.EMAIL} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + } + + {config.supported_tan_channels.indexOf(TanChannel.SMS) === -1 ? undefined : + <label onClick={() => { + if (!resultAccount.body.contact_data?.phone) return; + form.channel = TanChannel.SMS + updateForm(structuredClone(form)) + }} data-disabled={!resultAccount.body.contact_data?.phone} data-selected={form.channel === TanChannel.SMS} + class="relative flex data-[disabled=false]:cursor-pointer rounded-lg border data-[disabled=true]:bg-gray-200 p-4 shadow-sm focus:outline-none border-gray-300 data-[selected=true]:ring-2 data-[selected=true]:ring-indigo-600"> + <input type="radio" name="channel" value="Existing Customers" class="sr-only" /> + <span class="flex flex-1"> + <span class="flex flex-col"> + <span id="project-type-1-label" class="block text-sm font-medium text-gray-900"> + <i18n.Translate>SMS</i18n.Translate> + </span> + {!resultAccount.body.contact_data?.phone && i18n.str`add a phone number in your profile to enable this option`} + </span> </span> - {!resultAccount.body.contact_data?.phone && i18n.str`add a phone number in your profile to enable this option`} - </span> - </span> - <svg data-selected={form.channel === TanChannel.SMS} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> - </svg> - </label> - + <svg data-selected={form.channel === TanChannel.SMS} class="h-5 w-5 text-indigo-600 data-[selected=false]:hidden" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> + <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /> + </svg> + </label> + } + </div> </div> - </div> - </div> + </div> + } </div> </div> diff --git a/packages/demobank-ui/src/stories.test.ts b/packages/demobank-ui/src/stories.test.ts index a060a6b48..ebd9e6d8a 100644 --- a/packages/demobank-ui/src/stories.test.ts +++ b/packages/demobank-ui/src/stories.test.ts @@ -64,8 +64,11 @@ function DefaultTestingContext({ const cfg: TalerCorebankApi.Config = { name: "libeufin-bank", allow_deletions: true, + supported_tan_channels: [], allow_registrations: true, allow_conversion: true, + allow_edit_cashout_payto_uri: false, + allow_edit_name: false, currency: "ASR", currency_specification: { name: "ARS", |