taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 2870dd3778b00bafa78956dada74f1b08ed8711f
parent ede33f90440fc6ab5940cb604a914057d733e8f3
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Wed,  8 Apr 2026 17:40:46 -0300

fix #11345

Diffstat:
Mpackages/merchant-backoffice-ui/src/Routing.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/components/form/InputGroup.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/hooks/instance.ts | 26++++++++++++++++++++++----
Mpackages/merchant-backoffice-ui/src/hooks/order.ts | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 66+++++++++++++++++++++++++++++++++++++-----------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx | 36++++++++++++++++++++++++++++++++++--
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 337+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackages/taler-util/src/http-client/merchant.ts | 2+-
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,