commit 2870dd3778b00bafa78956dada74f1b08ed8711f
parent ede33f90440fc6ab5940cb604a914057d733e8f3
Author: Sebastian <sebasjm@taler-systems.com>
Date: Wed, 8 Apr 2026 17:40:46 -0300
fix #11345
Diffstat:
9 files changed, 267 insertions(+), 212 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx
@@ -589,8 +589,8 @@ export function Routing(_p: Props): VNode {
<Route
path={InstancePaths.bank_new}
component={BankAccountCreatePage}
- onConfirm={() => {
- route(InstancePaths.bank_list);
+ onConfirm={(id: string) => {
+ route(InstancePaths.bank_update.replace(":bid", id));
}}
onBack={() => {
route(InstancePaths.bank_list);
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx b/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx
@@ -50,8 +50,8 @@ export function InputGroup<T>({
<div class="card">
<header
class="card-header"
- style={{ cursor: "pointer" }}
- onClick={(): void => setActive(!active)}
+ style={fixed ? undefined :{ cursor: "pointer" }}
+ onClick={fixed ? undefined : (): void => setActive(!active)}
>
<p class="card-header-title">
{label}
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -18,6 +18,7 @@ import {
AccessToken,
getMerchantAccountKycStatusSimplified,
MerchantAccountKycStatusSimplified,
+ Paytos,
TalerError,
TalerHttpError,
TalerMerchantManagementResultByMethod,
@@ -88,7 +89,12 @@ export function useInstanceKYCDetailsLongPolling() {
);
},
async (ct, latestData) => {
- if (latestData === undefined || latestData.type === "fail" || !latestData.body.etag) return undefined
+ if (
+ latestData === undefined ||
+ latestData.type === "fail" ||
+ !latestData.body.etag
+ )
+ return undefined;
const r = await lib.instance.getCurrentInstanceKycStatus(token!, {
longpoll: {
type: "state-change",
@@ -97,7 +103,7 @@ export function useInstanceKYCDetailsLongPolling() {
},
ct,
});
- mutate(r, { revalidate: false });
+ await mutate(r, { revalidate: false });
return r;
},
[],
@@ -110,7 +116,9 @@ export function useInstanceKYCDetailsLongPolling() {
return result;
}
-export function useInstanceKYCSimplifiedWorstStatusLongPolling() {
+export function useInstanceKYCSimplifiedWorstStatusLongPolling(
+ account?: Paytos.URI,
+) {
const kycStatus = useInstanceKYCDetailsLongPolling();
const allKycData =
@@ -120,11 +128,21 @@ export function useInstanceKYCSimplifiedWorstStatusLongPolling() {
? kycStatus.body.kyc_data
: [];
- return allKycData.reduce((prev, cur) => {
+ const uri_str =
+ account === undefined ? undefined : Paytos.toFullString(account);
+
+ const filtered =
+ uri_str === undefined
+ ? allKycData
+ : allKycData.filter((data) => data.payto_uri === uri_str);
+
+ const r = filtered.reduce((prev, cur) => {
const st = getMerchantAccountKycStatusSimplified(cur.status);
if (st > prev) return st;
return prev;
}, MerchantAccountKycStatusSimplified.OK);
+
+ return r;
}
export function useInstanceKYCSimplifiedBestStatusLongPolling() {
diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts
@@ -87,7 +87,7 @@ export function useOrderDetailsWithLongPoll(orderId: string) {
},
ct,
});
- mutate(r, { revalidate: false });
+ await mutate(r, { revalidate: false });
return r;
},
[orderId],
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
@@ -67,7 +67,7 @@ type Entity = TalerMerchantApi.AccountAddDetails & {
} & TalerForm;
interface Props {
- onCreated: () => void;
+ onCreated: (id: string) => void;
onBack?: () => void;
}
@@ -108,17 +108,17 @@ export function CreatePage({ onCreated, onBack }: Props): VNode {
!state.credit_facade_credentials || !state.credit_facade_url
? undefined
: {
- username:
- state.credit_facade_credentials.type === "basic" &&
+ username:
+ state.credit_facade_credentials.type === "basic" &&
!state.credit_facade_credentials.username
- ? i18n.str`Required`
- : undefined,
- password:
- state.credit_facade_credentials.type === "basic" &&
+ ? i18n.str`Required`
+ : undefined,
+ password:
+ state.credit_facade_credentials.type === "basic" &&
!state.credit_facade_credentials.password
- ? i18n.str`Required`
- : undefined,
- },
+ ? i18n.str`Required`
+ : undefined,
+ },
) as any,
credit_facade_url: !state.credit_facade_url
? undefined
@@ -145,39 +145,43 @@ export function CreatePage({ onCreated, onBack }: Props): VNode {
? undefined
: state.credit_facade_credentials?.type === "basic"
? {
- type: "basic",
- password: state.credit_facade_credentials.password,
- username: state.credit_facade_credentials.username,
- }
+ type: "basic",
+ password: state.credit_facade_credentials.password,
+ username: state.credit_facade_credentials.username,
+ }
: {
- type: "none",
- };
+ type: "none",
+ };
const { state: session, lib } = useSessionContext();
const request: TalerMerchantApi.AccountAddDetails | undefined =
!state.payto_uri
? undefined
: {
- payto_uri: state.payto_uri,
- credit_facade_credentials,
- credit_facade_url,
- extra_wire_subject_metadata: state.extra_wire_subject_metadata,
- };
+ payto_uri: state.payto_uri,
+ credit_facade_credentials,
+ credit_facade_url,
+ extra_wire_subject_metadata: state.extra_wire_subject_metadata,
+ };
const [notification, safeFunctionHandler] = useLocalNotificationBetter();
const mfa = useChallengeHandler();
const add = safeFunctionHandler(
i18n.str`add bank account`,
async (token: AccessToken, request: Entity, challengeIds: string[]) => {
- const resp = await lib.instance.addBankAccount(token, request, { challengeIds })
+ const resp = await lib.instance.addBankAccount(token, request, {
+ challengeIds,
+ });
if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) {
await maybeTryFirstMFA(lib.instance, mfa, resp.body);
}
- return resp
+ return resp;
},
!session.token || !request ? undefined : [session.token, request, []],
);
- add.onSuccess = onCreated;
+ add.onSuccess = (resp) => {
+ onCreated(resp.h_wire)
+ };
add.onFail = (fail) => {
switch (fail.case) {
case HttpStatusCode.Accepted:
@@ -403,7 +407,7 @@ export async function maybeTryFirstMFA(
b: TalerMerchantApi.ChallengeResponse,
repeat?: SafeHandlerTemplate<[ids: string[]], any>,
) {
- const letUserDecide = b.combi_and === false && b.challenges.length > 1
+ const letUserDecide = b.combi_and === false && b.challenges.length > 1;
if (b.challenges.length === 0 || letUserDecide) {
mfa.onChallengeRequired(b, repeat);
return;
@@ -413,9 +417,13 @@ export async function maybeTryFirstMFA(
if (result.type === "fail") {
mfa.onChallengeRequired(b, repeat);
} else {
- mfa.onChallengeRequiredWithInitial(b, {
- request: challenge,
- response: result.body,
- }, repeat);
+ mfa.onChallengeRequiredWithInitial(
+ b,
+ {
+ request: challenge,
+ response: result.body,
+ },
+ repeat,
+ );
}
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -39,7 +39,7 @@ import { CreatePage } from "./CreatePage.js";
export type Entity = TalerMerchantApi.AccountAddDetails;
interface Props {
onBack?: () => void;
- onConfirm: () => void;
+ onConfirm: (id:string) => void;
}
export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
@@ -22,8 +22,9 @@
import {
HttpStatusCode,
MerchantAccountKycStatusSimplified,
+ Paytos,
TalerError,
- assertUnreachable,
+ assertUnreachable
} from "@gnu-taler/taler-util";
import {
NotificationCardBulma,
@@ -33,10 +34,13 @@ import { Fragment, VNode, h } from "preact";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
+import {
+ useInstanceKYCSimplifiedBestStatusLongPolling,
+ useInstanceKYCSimplifiedWorstStatusLongPolling
+} from "../../../../hooks/instance.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CardTable } from "./Table.js";
-import { useInstanceKYCSimplifiedBestStatusLongPolling } from "../../../../hooks/instance.js";
const TALER_SCREEN_ID = 34;
@@ -70,6 +74,34 @@ export function MissingBankAccountsWarning(): VNode {
return <Fragment />;
}
+export function UnableToUseBankAccountWarning({
+ account,
+}: {
+ account: Paytos.URI;
+}): VNode {
+ const { i18n } = useTranslationContext();
+ const status = useInstanceKYCSimplifiedWorstStatusLongPolling(account);
+ switch (status) {
+ case MerchantAccountKycStatusSimplified.ACTION_REQUIRED:
+ case MerchantAccountKycStatusSimplified.WARNING:
+ case MerchantAccountKycStatusSimplified.ERROR:
+ return <NotificationCardBulma
+ notification={{
+ type: "WARN",
+ message: i18n.str`This account is not ready to be used.`,
+ description: <i18n.Translate>
+ There are pending actions related to KYC. <a href="#/kyc">More details.</a>
+ </i18n.Translate>,
+ }}
+ />
+
+ case MerchantAccountKycStatusSimplified.OK:
+ return <Fragment />;
+ default:
+ assertUnreachable(status);
+ }
+}
+
export function LimitedKycActionWarning(): VNode {
const { i18n } = useTranslationContext();
const status = useInstanceKYCSimplifiedBestStatusLongPolling();
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
@@ -57,6 +57,7 @@ import { undefinedIfEmpty } from "../../../../utils/table.js";
import { TestRevenueErrorType, testRevenueAPI } from "../create/index.js";
import { Tooltip } from "../../../../components/Tooltip.js";
import { maybeTryFirstMFA } from "../create/CreatePage.js";
+import { UnableToUseBankAccountWarning } from "../list/index.js";
const TALER_SCREEN_ID = 36;
@@ -128,33 +129,33 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
!state.credit_facade_credentials || !state.credit_facade_url
? undefined
: (undefinedIfEmpty({
- type:
- replacingAccountId &&
+ type:
+ replacingAccountId &&
// @ts-expect-error unedit is not in facade creds
state.credit_facade_credentials?.type === "unedit"
- ? i18n.str`Required`
- : undefined,
- username:
- state.credit_facade_credentials?.type !== "basic"
- ? undefined
- : !state.credit_facade_credentials.username
? i18n.str`Required`
: undefined,
+ username:
+ state.credit_facade_credentials?.type !== "basic"
+ ? undefined
+ : !state.credit_facade_credentials.username
+ ? i18n.str`Required`
+ : undefined,
- token:
- state.credit_facade_credentials?.type !== "bearer"
- ? undefined
- : !state.credit_facade_credentials.token
- ? i18n.str`Required`
- : undefined,
+ token:
+ state.credit_facade_credentials?.type !== "bearer"
+ ? undefined
+ : !state.credit_facade_credentials.token
+ ? i18n.str`Required`
+ : undefined,
- password:
- state.credit_facade_credentials?.type !== "basic"
- ? undefined
- : !state.credit_facade_credentials.password
- ? i18n.str`Required`
- : undefined,
- }) as any),
+ password:
+ state.credit_facade_credentials?.type !== "basic"
+ ? undefined
+ : !state.credit_facade_credentials.password
+ ? i18n.str`Required`
+ : undefined,
+ }) as any),
});
const hasErrors = errors !== undefined;
@@ -167,25 +168,25 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
| TalerMerchantApi.FacadeCredentials
| undefined =
credit_facade_url == undefined ||
- state.credit_facade_credentials === undefined
+ state.credit_facade_credentials === undefined
? undefined
: // @ts-expect-error unedit is not in facade creds
- state.credit_facade_credentials.type === "unedit"
+ state.credit_facade_credentials.type === "unedit"
? undefined
: state.credit_facade_credentials.type === "basic"
? {
- type: "basic",
- password: state.credit_facade_credentials.password,
- username: state.credit_facade_credentials.username,
- }
+ type: "basic",
+ password: state.credit_facade_credentials.password,
+ username: state.credit_facade_credentials.username,
+ }
: state.credit_facade_credentials.type === "bearer"
? {
- type: "bearer",
- token: state.credit_facade_credentials.token,
- }
+ type: "bearer",
+ token: state.credit_facade_credentials.token,
+ }
: {
- type: "none",
- };
+ type: "none",
+ };
const { state: session, lib } = useSessionContext();
const [notification, safeFunctionHandler] = useLocalNotificationBetter();
@@ -228,18 +229,18 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
!session.token
? undefined
: [
- session.token,
- replacingAccountId ? state.payto_uri! : undefined,
- account.h_wire,
- {
- credit_facade_credentials,
- credit_facade_url,
- extra_wire_subject_metadata: !state.extra_wire_subject_metadata
- ? undefined
- : state.extra_wire_subject_metadata,
- },
- [],
- ],
+ session.token,
+ replacingAccountId ? state.payto_uri! : undefined,
+ account.h_wire,
+ {
+ credit_facade_credentials,
+ credit_facade_url,
+ extra_wire_subject_metadata: !state.extra_wire_subject_metadata
+ ? undefined
+ : state.extra_wire_subject_metadata,
+ },
+ [],
+ ],
);
update.onSuccess = onUpdated;
update.onFail = (fail) => {
@@ -313,11 +314,12 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
/>
);
}
+ const payto = Result.unpack(Paytos.fromString(account.payto_uri))
return (
<Fragment>
<LocalNotificationBanner notification={notification} />
- <section class="section">
+ <section class="section ">
<section class="hero is-hero-bar">
<div class="hero-body">
<div class="level">
@@ -325,142 +327,137 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
<div class="level-item">
<span class="is-size-4">
<i18n.Translate>Account:</i18n.Translate>{" "}
- <b>
- {
- Result.unpack(Paytos.fromString(account.payto_uri))
- .displayName
- }
- </b>
+ <b>{payto.displayName}</b>
</span>
</div>
</div>
</div>
</div>
</section>
- <hr />
-
- <section class="section is-main-section">
- <div class="columns">
- <div class="column is-four-fifths">
- <FormProvider
- object={state}
- valueHandler={setState}
- errors={errors}
- >
- <InputPaytoForm<FormType>
- name="payto_uri"
- label={i18n.str`Account`}
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <UnableToUseBankAccountWarning account={payto} />
+ </div>
+ </div>
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputPaytoForm<FormType>
+ name="payto_uri"
+ label={i18n.str`Account details`}
+ />
+ <FragmentPersonaFlag point={UIElement.option_exraWireSubject}>
+ <Input<Entity>
+ name="extra_wire_subject_metadata"
+ label={i18n.str`Extra subject`}
+ expand
+ tooltip={i18n.str`Additional text to include in the wire transfer subject when settling the payment`}
+ />
+ </FragmentPersonaFlag>
+ <FragmentPersonaFlag point={UIElement.action_useRevenueApi}>
+ <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 === "basic")
+ return i18n.str`With username and password`;
+ if (str === "bearer") return i18n.str`With token`;
+ return i18n.str`Do not change`;
+ }}
/>
- <FragmentPersonaFlag point={UIElement.option_exraWireSubject}>
- <Input<Entity>
- name="extra_wire_subject_metadata"
- label={i18n.str`Extra subject`}
- expand
- tooltip={i18n.str`Additional text to include in the wire transfer subject when settling the payment`}
- />
- </FragmentPersonaFlag>
- <FragmentPersonaFlag point={UIElement.action_useRevenueApi}>
- <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 === "basic")
- return i18n.str`With username and password`;
- if (str === "bearer") return i18n.str`With token`;
- return i18n.str`Do not change`;
- }}
- />
- {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.`}
- autoComplete="username"
- />
- <InputPassword
- name="credit_facade_credentials.password"
- expand
- label={i18n.str`Password`}
- tooltip={i18n.str`Password to access the account information.`}
- autoComplete="current-password"
- />
- </Fragment>
- ) : undefined}
- {state.credit_facade_credentials?.type === "bearer" ? (
- <Fragment>
- <InputPassword
- name="credit_facade_credentials.token"
- label={i18n.str`Token`}
- expand
- tooltip={i18n.str`Access token to access the account information.`}
- />
- </Fragment>
- ) : undefined}
- <InputToggle<FormType>
- label={i18n.str`Match`}
- tooltip={i18n.str`Check where the information match against the server info.`}
- name="verified"
- readonly
- threeState
- side={
- <Tooltip
- text={i18n.str`Compare info from server with account form`}
+ {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.`}
+ autoComplete="username"
+ />
+ <InputPassword
+ name="credit_facade_credentials.password"
+ expand
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ autoComplete="current-password"
+ />
+ </Fragment>
+ ) : undefined}
+ {state.credit_facade_credentials?.type === "bearer" ? (
+ <Fragment>
+ <InputPassword
+ name="credit_facade_credentials.token"
+ label={i18n.str`Token`}
+ expand
+ tooltip={i18n.str`Access token to access the account information.`}
+ />
+ </Fragment>
+ ) : undefined}
+ <InputToggle<FormType>
+ label={i18n.str`Match`}
+ tooltip={i18n.str`Check where the information match against the server info.`}
+ name="verified"
+ readonly
+ threeState
+ side={
+ <Tooltip
+ text={i18n.str`Compare info from server with account form`}
+ >
+ <ButtonBetterBulma
+ type="button"
+ class="button is-info"
+ onClick={test}
>
- <ButtonBetterBulma
- type="button"
- class="button is-info"
- onClick={test}
- >
- <i18n.Translate>Test</i18n.Translate>
- </ButtonBetterBulma>
- </Tooltip>
- }
- />
- </FragmentPersonaFlag>
- </FormProvider>
-
- <div class="buttons is-right mt-5">
- {onBack && (
- <button class="button" onClick={onBack}>
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- )}
- <Tooltip
- text={
- hasErrors
- ? i18n.str`Please complete the marked fields`
- : i18n.str`Confirm operation`
+ <i18n.Translate>Test</i18n.Translate>
+ </ButtonBetterBulma>
+ </Tooltip>
}
- >
- <ButtonBetterBulma onClick={update} type="submit">
- <i18n.Translate>Confirm</i18n.Translate>
- </ButtonBetterBulma>
- </Tooltip>
- </div>
+ />
+ </FragmentPersonaFlag>
+ </FormProvider>
+
+ <div class="buttons is-right mt-5">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <Tooltip
+ text={
+ hasErrors
+ ? i18n.str`Please complete the marked fields`
+ : i18n.str`Confirm operation`
+ }
+ >
+ <ButtonBetterBulma onClick={update} type="submit">
+ <i18n.Translate>Confirm</i18n.Translate>
+ </ButtonBetterBulma>
+ </Tooltip>
</div>
</div>
- </section>
+ </div>
</section>
{!revenuePayto ? undefined : (
<CompareAccountsModal
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -834,7 +834,7 @@ export class TalerMerchantInstanceHttpClient {
}
/**
- * https://docs.taler.net/core/api-merchant.html#get--instances-$INSTANCE-private-kyc
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-kyc
*/
async getCurrentInstanceKycStatus(
token: AccessToken,