commit f045d1efc5fff9fca3480841764e85eee8ac768a
parent cb1beccfe021351323ce032257941972638820e3
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 22 Oct 2025 17:27:44 -0300
fix #10524
Diffstat:
8 files changed, 193 insertions(+), 152 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx
@@ -220,7 +220,7 @@ export function Routing(_p: Props): VNode {
onCancel={() => route(InstancePaths.order_list)}
onReseted={() => route(InstancePaths.order_list)}
/>
- <Route default component={() => <LoginPage />} />
+ <Route default component={() => <LoginPage showCreateAccount />} />
</Router>
</Fragment>
);
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -34,6 +34,7 @@ import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js";
+import { useSettingsContext } from "../../context/settings.js";
export interface Props<T> extends InputProps<T> {}
@@ -41,7 +42,7 @@ export interface Props<T> extends InputProps<T> {}
// https://datatracker.ietf.org/doc/html/rfc8905
type Entity = {
// iban, bitcoin, x-taler-bank. it defined the format
- target: string;
+ target: string | undefined;
// path1 if the first field to be used
path1?: string;
// path2 if the second field to be used, optional
@@ -148,8 +149,7 @@ function validateIBAN_path1(
i18n: ReturnType<typeof useTranslationContext>["i18n"],
): TranslatedString | undefined {
// Check total length
- if (iban.length < 4)
- return i18n.str`IBANs usually have more than 4 digits.`;
+ if (iban.length < 4) return i18n.str`IBANs usually have more than 4 digits.`;
if (iban.length > 34)
return i18n.str`IBANs usually have fewer than 34 digits.`;
@@ -196,22 +196,25 @@ export function InputPaytoForm<T>({
}: Props<keyof T>): VNode {
const { value: initialValueStr, onChange } = useField<T>(name);
+ const { supportedWireMethods } = useSettingsContext();
+
const initialPayto = parsePaytoUri(initialValueStr ?? "");
const { i18n } = useTranslationContext();
- const targets = [
- i18n.str`Select a wire method...`,
- "iban",
- "bitcoin",
- "ethereum",
- "x-taler-bank",
- ];
- const noTargetValue = targets[0];
+ const allTargets = ["iban", "bitcoin", "ethereum", "x-taler-bank"];
+
+ // only the one supported by the server
+ const targets =
+ !supportedWireMethods || !supportedWireMethods.length
+ ? allTargets
+ : allTargets.filter((t) => supportedWireMethods.indexOf(t) !== -1);
+
const defaultTarget: Entity = {
- target: noTargetValue,
+ target: targets.length ? targets[0] : "",
params: {},
};
+ // FIXME: use new paytos API and EBICS is not supported
const paths = !initialPayto ? [] : initialPayto.targetPath.split("/");
const initialPath1 = paths.length >= 1 ? paths[0] : undefined;
const initialPath2 = paths.length >= 2 ? paths[1] : undefined;
@@ -224,27 +227,11 @@ export function InputPaytoForm<T>({
path1: initialPath1,
path2: initialPath2,
};
+
const [value, setValue] = useState<Partial<Entity>>(initial);
- useEffect(() => {
- const nv = parsePaytoUri(initialValueStr ?? "");
- const paths = !initialPayto ? [] : initialPayto.targetPath.split("/");
- if (nv !== undefined && nv.isKnown) {
- if (nv.targetType === "iban" && paths.length >= 2) {
- //FIXME: workaround EBIC not supported
- paths[0] = paths[1];
- delete paths[1];
- }
- setValue({
- target: nv.targetType,
- params: nv.params,
- path1: paths.length >= 1 ? paths[0] : undefined,
- path2: paths.length >= 2 ? paths[1] : undefined,
- });
- }
- }, [initialValueStr]);
const errors = undefinedIfEmpty<FormErrors<Entity>>({
- target: value.target === noTargetValue ? i18n.str`Required` : undefined,
+ target: value.target === undefined ? i18n.str`Required` : undefined,
path1: !value.path1
? i18n.str`Required`
: value.target === "iban"
@@ -298,6 +285,9 @@ export function InputPaytoForm<T>({
onChange(str as T[keyof T]);
}, [str]);
+ if (!targets.length) {
+ return <i18n.Translate>None of the supported wire method of the server are currently supported by this app. Server settings: {supportedWireMethods?.join(",") ?? "-"}</i18n.Translate>
+ }
return (
<InputGroup name="payto" label={label} fixed tooltip={tooltip}>
<FormProvider<Entity>
@@ -306,14 +296,18 @@ export function InputPaytoForm<T>({
object={value}
valueHandler={setValue}
>
+ {targets.length === 1 ? undefined:
<InputSelector<Entity>
name="target"
label={i18n.str`Wire method`}
tooltip={i18n.str`Select the method you want to use to transfer your earnings to your business account.`}
values={targets}
readonly={readonly}
- toStr={(v) => (v === noTargetValue ? i18n.str`Select a wire method...` : v)}
- />
+ toStr={(v) =>
+ v === undefined ? i18n.str`Select a wire method...` : v
+ }
+ />
+ }
{value.target === "ach" && (
<Fragment>
@@ -416,7 +410,8 @@ export function InputPaytoForm<T>({
<Fragment>
<div>
<i18n.Translate>
- Enter the data without a scheme. A subpath may be included:
+ Enter the data without a scheme. A subpath may be
+ included:
</i18n.Translate>
</div>
<div>bank.com/</div>
@@ -436,7 +431,7 @@ export function InputPaytoForm<T>({
{/**
* Show additional fields apart from the payto
*/}
- {value.target !== noTargetValue && (
+ {value.target !== undefined && (
<Fragment>
<Input
name="params.receiver-name"
@@ -456,7 +451,7 @@ function cleanupPath1(str: string, type?: string) {
if (type === "iban") {
// we tolerate country in any case
// and don't care about any space
- return str.replace(/\s/g, '').toUpperCase();
+ return str.replace(/\s/g, "").toUpperCase();
}
if (type === "x-taler-bank") {
return !str.endsWith("/") ? str + "/" : str;
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -23,6 +23,7 @@ import { NavigationBar } from "./NavigationBar.js";
import { Sidebar } from "./SideBar.js";
import { useSessionContext } from "../../context/session.js";
import { useNavigationContext } from "@gnu-taler/web-util/browser";
+import { LangSelector } from "./LangSelector.js";
function getInstanceTitle(path: string, id: string): string {
switch (path) {
@@ -233,6 +234,19 @@ export function NotConnectedAppMenu({
onMobileMenu={() => setMobileOpen(!mobileOpen)}
title={title}
/>
+ <div
+ style={{
+ width: "100%",
+ display: "flex",
+ justifyContent: "space-between",
+ paddingTop:8,
+ paddingRight: 8,
+
+ }}
+ >
+ <div />
+ <LangSelector />
+ </div>
</div>
);
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -46,6 +46,7 @@ import { undefinedIfEmpty } from "../../../../utils/table.js";
import { safeConvertURL } from "../update/UpdatePage.js";
import { TestRevenueErrorType, testRevenueAPI } from "./index.js";
import { usePreference } from "../../../../hooks/preference.js";
+import { useSettingsContext } from "../../../../context/settings.js";
type Entity = TalerMerchantApi.AccountAddDetails & {
verified?: boolean;
@@ -74,7 +75,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
const facadeURL = safeConvertURL(state.credit_facade_url);
const [revenuePayto, setRevenuePayto] = useState<PaytoUri | undefined>(
- // parsePaytoUri("payto://x-taler-bank/asd.com:1010/asd/pepe"),
undefined,
);
const [testError, setTestError] = useState<TranslatedString | undefined>(
@@ -230,85 +230,91 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
name="payto_uri"
label={i18n.str`Account details`}
/>
- <div class="message-body" style={{ marginBottom: 10 }}>
- <p>
- <i18n.Translate>
- If the bank supports Taler Revenue API then you can add the
- endpoint URL below to keep the revenue information in sync.
- </i18n.Translate>
- </p>
- </div>
- <Input<Entity>
- name="credit_facade_url"
- label={i18n.str`Endpoint URL`}
- help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/"
- expand
- tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
- />
- <InputSelector
- name="credit_facade_credentials.type"
- label={i18n.str`Auth type`}
- tooltip={i18n.str`Choose the authentication type for the account info URL`}
- values={accountAuthType}
- toStr={(str) => {
- if (str === "none") return i18n.str`Without authentication`;
- if (str === "bearer") return i18n.str`With token`;
- return "With username and password";
- }}
- />
- {state.credit_facade_credentials?.type === "basic" ? (
+ {!developerMode ? undefined : (
<Fragment>
- <Input
- name="credit_facade_credentials.username"
- label={i18n.str`Username`}
- tooltip={i18n.str`Username to access the account information.`}
+ <div class="message-body" style={{ marginBottom: 10 }}>
+ <p>
+ <i18n.Translate>
+ If the bank supports Taler Revenue API then you can add
+ the endpoint URL below to keep the revenue information
+ in sync.
+ </i18n.Translate>
+ </p>
+ </div>
+ <Input<Entity>
+ name="credit_facade_url"
+ label={i18n.str`Endpoint URL`}
+ help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/"
+ expand
+ tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
/>
- <Input
- name="credit_facade_credentials.password"
- inputType="password"
- label={i18n.str`Password`}
- tooltip={i18n.str`Password to access the account information.`}
+ <InputSelector
+ name="credit_facade_credentials.type"
+ label={i18n.str`Auth type`}
+ tooltip={i18n.str`Choose the authentication type for the account info URL`}
+ values={accountAuthType}
+ toStr={(str) => {
+ if (str === "none")
+ return i18n.str`Without authentication`;
+ if (str === "bearer") return i18n.str`With token`;
+ return "With username and password";
+ }}
/>
- </Fragment>
- ) : undefined}
- {state.credit_facade_credentials?.type === "bearer" ? (
- <Fragment>
- <Input
- name="credit_facade_credentials.token"
- label={i18n.str`Token`}
- inputType="password"
- tooltip={i18n.str`Access token to access the account information.`}
+ {state.credit_facade_credentials?.type === "basic" ? (
+ <Fragment>
+ <Input
+ name="credit_facade_credentials.username"
+ label={i18n.str`Username`}
+ tooltip={i18n.str`Username to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.password"
+ inputType="password"
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ />
+ </Fragment>
+ ) : undefined}
+ {state.credit_facade_credentials?.type === "bearer" ? (
+ <Fragment>
+ <Input
+ name="credit_facade_credentials.token"
+ label={i18n.str`Token`}
+ inputType="password"
+ tooltip={i18n.str`Access token to access the account information.`}
+ />
+ </Fragment>
+ ) : undefined}
+ <InputToggle<Entity>
+ label={i18n.str`Match`}
+ tooltip={i18n.str`Check if the information matches the server info.`}
+ name="verified"
+ readonly
+ threeState
+ help={
+ testError !== undefined
+ ? testError
+ : state.verified === undefined
+ ? i18n.str`Not verified`
+ : state.verified
+ ? i18n.str`Last test was successful.`
+ : i18n.str`Last test failed.`
+ }
+ side={
+ <button
+ class="button is-info"
+ data-tooltip={i18n.str`Compare info from server with account form`}
+ disabled={!state.credit_facade_url}
+ onClick={async () => {
+ await testAccountInfo();
+ }}
+ >
+ <i18n.Translate>Test</i18n.Translate>
+ </button>
+ }
/>
</Fragment>
- ) : undefined}
- <InputToggle<Entity>
- label={i18n.str`Match`}
- tooltip={i18n.str`Check if the information matches the server info.`}
- name="verified"
- readonly
- threeState
- help={
- testError !== undefined
- ? testError
- : state.verified === undefined
- ? i18n.str`Not verified`
- : state.verified
- ? i18n.str`Last test was successful.`
- : i18n.str`Last test failed.`
- }
- side={
- <button
- class="button is-info"
- data-tooltip={i18n.str`Compare info from server with account form`}
- disabled={!state.credit_facade_url}
- onClick={async () => {
- await testAccountInfo();
- }}
- >
- <i18n.Translate>Test</i18n.Translate>
- </button>
- }
- />
+ )}
</FormProvider>
<div class="buttons is-right mt-5">
diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
@@ -40,7 +40,9 @@ import { SolveMFAChallenges } from "../../components/SolveMFA.js";
import { useSessionContext } from "../../context/session.js";
import { Notification } from "../../utils/types.js";
-interface Props {}
+interface Props {
+ showCreateAccount?: boolean;
+}
export const TEMP_TEST_TOKEN = (description: TranslatedString) =>
({
@@ -57,11 +59,11 @@ export const FOREVER_REFRESHABLE_TOKEN = (description: TranslatedString) =>
}) as LoginTokenRequest;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
-export function LoginPage(_p: Props): VNode {
+export function LoginPage({ showCreateAccount }: Props): VNode {
const [password, setPassword] = useState("");
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { state, logIn, getInstanceForUsername, config } = useSessionContext();
- const [username, setUsername] = useState(state.instance);
+ const [username, setUsername] = useState(showCreateAccount? "" : state.instance);
const { i18n } = useTranslationContext();
@@ -110,12 +112,15 @@ export function LoginPage(_p: Props): VNode {
}
}
} catch (error) {
- const details = error instanceof TalerError ? JSON.stringify(error.errorDetail) : undefined
+ const details =
+ error instanceof TalerError
+ ? JSON.stringify(error.errorDetail)
+ : undefined;
setNotif({
message: i18n.str`Failed to login.`,
type: "ERROR",
description: error instanceof Error ? error.message : undefined,
- details
+ details,
});
}
},
@@ -234,16 +239,18 @@ export function LoginPage(_p: Props): VNode {
</AsyncButton>
</footer>
</div>
- <div>
- <a href={"#/account/new"} class="has-icon">
- <span class="icon">
- <i class="mdi mdi-account-plus" />
- </span>
- <span class="menu-item-label">
- <i18n.Translate>Create new account</i18n.Translate>
- </span>
- </a>
- </div>
+ {!showCreateAccount ? undefined : (
+ <div style={{ marginTop: 8 }}>
+ <a href={"#/account/new"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-account-plus" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Create new account</i18n.Translate>
+ </span>
+ </a>
+ </div>
+ )}
</div>
</div>
<div
diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx
@@ -18,6 +18,7 @@ import {
Duration,
HttpStatusCode,
MerchantAuthMethod,
+ MerchantTanChannel,
} from "@gnu-taler/taler-util";
import {
undefinedIfEmpty,
@@ -42,7 +43,6 @@ import {
PHONE_JUST_NUMBERS_REGEX,
} from "../../utils/constants.js";
import { Notification } from "../../utils/types.js";
-import { FOREVER_REFRESHABLE_TOKEN } from "../login/index.js";
export interface Account {
id: string;
@@ -62,10 +62,16 @@ interface Props {
}
export function NewAccount({ onCancel, onCreated }: Props): VNode {
const { i18n } = useTranslationContext();
- const { state: session, lib, logIn } = useSessionContext();
+ const { state: session, lib, logIn, config } = useSessionContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const [value, setValue] = useState<Partial<Account>>({ });
+ const [value, setValue] = useState<Partial<Account>>({});
+
+ const serverRequiresEmail =
+ config.mandatory_tan_channels?.indexOf(MerchantTanChannel.EMAIL) !== -1;
+ const serverRequiresSms =
+ config.mandatory_tan_channels?.indexOf(MerchantTanChannel.SMS) !== -1;
+
const errors = undefinedIfEmpty<FormErrors<Account>>({
id: !value.id
? i18n.str`Required`
@@ -79,18 +85,22 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode {
: value.password !== value.repeat
? i18n.str`Doesn't match`
: undefined,
- email: !value.email
- ? i18n.str`Required`
- : !EMAIL_REGEX.test(value.email)
- ? i18n.str`Doesn't have the pattern of an email`
- : undefined,
- phone: !value.phone
- ? i18n.str`Required`
- : !value.phone.startsWith("+")
- ? i18n.str`Should start with +`
- : !PHONE_JUST_NUMBERS_REGEX.test(value.phone)
- ? i18n.str`A phone number consists of numbers only`
+ email: !serverRequiresEmail
+ ? undefined
+ : !value.email
+ ? i18n.str`Required`
+ : !EMAIL_REGEX.test(value.email)
+ ? i18n.str`Doesn't have the pattern of an email`
: undefined,
+ phone: !serverRequiresSms
+ ? undefined
+ : !value.phone
+ ? i18n.str`Required`
+ : !value.phone.startsWith("+")
+ ? i18n.str`Should start with +`
+ : !PHONE_JUST_NUMBERS_REGEX.test(value.phone)
+ ? i18n.str`A phone number consists of numbers only`
+ : undefined,
});
function valueHandler(s: (d: Partial<Account>) => Partial<Account>): void {
@@ -126,7 +136,7 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode {
jurisdiction: {},
name: value.name!,
use_stefan: true,
- email: value.email!,
+ email: value.email,
phone_number: value.phone,
},
{
@@ -222,16 +232,20 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode {
inputType="password"
name="repeat"
/>
- <Input<Account>
- label={i18n.str`Email`}
- tooltip={i18n.str`Contact email`}
- name="email"
- />
- <Input<Account>
- label={i18n.str`Phone`}
- tooltip={i18n.str`Contact phone number`}
- name="phone"
- />
+ {serverRequiresEmail ? (
+ <Input<Account>
+ label={i18n.str`Email`}
+ tooltip={i18n.str`Contact email`}
+ name="email"
+ />
+ ) : undefined}
+ {serverRequiresSms ? (
+ <Input<Account>
+ label={i18n.str`Phone`}
+ tooltip={i18n.str`Contact phone number`}
+ name="phone"
+ />
+ ) : undefined}
</FormProvider>
</section>
<footer
diff --git a/packages/merchant-backoffice-ui/src/settings.json b/packages/merchant-backoffice-ui/src/settings.json
@@ -1,3 +1,4 @@
{
- "backendBaseURL": "http://merchant.taler.test/"
+ "backendBaseURL": "http://merchant.taler.test/",
+ "supportedWireMethods": ["iban"]
}
diff --git a/packages/merchant-backoffice-ui/src/settings.ts b/packages/merchant-backoffice-ui/src/settings.ts
@@ -18,6 +18,7 @@ import {
Codec,
buildCodecForObject,
canonicalizeBaseUrl,
+ codecForList,
codecForString,
codecOptional
} from "@gnu-taler/taler-util";
@@ -26,6 +27,8 @@ export interface MerchantUiSettings {
// Where merchant backend is localted
// default: window.origin without "webui/"
backendBaseURL?: string;
+
+ supportedWireMethods?: string[];
}
/**
@@ -38,6 +41,7 @@ const defaultSettings: MerchantUiSettings = {
const codecForBankUISettings = (): Codec<MerchantUiSettings> =>
buildCodecForObject<MerchantUiSettings>()
.property("backendBaseURL", codecOptional(codecForString()))
+ .property("supportedWireMethods", codecOptional(codecForList(codecForString())))
.build("MerchantUiSettings");
function removeUndefineField<T extends object>(obj: T): T {