summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-12-06 14:05:37 -0300
committerSebastian <sebasjm@gmail.com>2023-12-06 14:10:10 -0300
commit35ee1ddd797be94557ab23f500c69fabee468387 (patch)
treee618fab464884275b484c599fc81ae9391002572
parent71acfc274c9cfef70d9c2ed542958434ac94b137 (diff)
downloadwallet-core-35ee1ddd797be94557ab23f500c69fabee468387.tar.gz
wallet-core-35ee1ddd797be94557ab23f500c69fabee468387.tar.bz2
wallet-core-35ee1ddd797be94557ab23f500c69fabee468387.zip
account form
-rw-r--r--packages/demobank-ui/src/components/app.tsx2
-rw-r--r--packages/demobank-ui/src/pages/BankFrame.tsx5
-rw-r--r--packages/demobank-ui/src/pages/ProfileNavigation.tsx7
-rw-r--r--packages/demobank-ui/src/pages/PublicHistoriesPage.tsx12
-rw-r--r--packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx4
-rw-r--r--packages/demobank-ui/src/pages/account/UpdateAccountPassword.tsx2
-rw-r--r--packages/demobank-ui/src/pages/admin/AccountForm.tsx654
-rw-r--r--packages/demobank-ui/src/pages/admin/CreateNewAccount.tsx1
-rw-r--r--packages/demobank-ui/src/pages/business/CreateCashout.tsx105
-rw-r--r--packages/demobank-ui/src/stories.test.ts3
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",