taler-typescript-core

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

commit 5995765e34f9a59ec0a3d659549411b9cfa6bc96
parent 6cf375d47e91c94d985d4e626fde6fea10147067
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Fri, 13 Mar 2026 11:27:14 -0300

fix #11235

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx | 4+++-
Mpackages/merchant-backoffice-ui/src/components/menu/SideBar.tsx | 40++++++++++++++--------------------------
Mpackages/merchant-backoffice-ui/src/hooks/instance.ts | 37+++++++++++++++++++++++++++++++++++++
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mpackages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx | 3+--
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 94++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/newAccount/index.tsx | 3+--
Mpackages/taler-util/src/http-client/merchant.ts | 7+------
8 files changed, 170 insertions(+), 96 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx @@ -45,6 +45,8 @@ export function InputCurrency<T>({ side, }: Props<keyof T>): VNode { const { config } = useSessionContext(); + const id = config.currencies[config.currency].num_fractional_input_digits; + const step = Math.pow(10, -1 * (id ?? 0)); return ( <InputWithAddon<T> name={name} @@ -60,7 +62,7 @@ export function InputCurrency<T>({ expand={expand} toStr={(v?: AmountString) => v?.split(":")[1] || ""} fromStr={(v: string) => (!v ? undefined : `${config.currency}:${v}`)} - inputExtra={{ min: 0, step: 0.001 }} + inputExtra={{ min: 0, step }} > {children} </InputWithAddon> diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -20,18 +20,16 @@ */ import { - getMerchantAccountKycStatusSimplified, MerchantAccountKycStatusSimplified, - MerchantPersona, - TalerError, + MerchantPersona } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useSessionContext } from "../../context/session.js"; import { useSettingsContext } from "../../context/settings.js"; -import { useInstanceKYCDetailsLongPolling } from "../../hooks/instance.js"; import { UIElement, usePreference } from "../../hooks/preference.js"; import { LangSelector } from "./LangSelector.js"; +import { useInstanceKYCSimplifiedWorstStatusLongPolling } from "../../hooks/instance.js"; const TALER_SCREEN_ID = 17; // const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; @@ -41,27 +39,15 @@ interface Props { mobile?: boolean; } + export function Sidebar({ mobile }: Props): VNode { const { i18n } = useTranslationContext(); const { state, logOut, config } = useSessionContext(); - const kycStatus = useInstanceKYCDetailsLongPolling(); - - const allKycData = - kycStatus !== undefined && - !(kycStatus instanceof TalerError) && - kycStatus.type === "ok" - ? kycStatus.body.kyc_data - : []; - - const simplifiedKycStatus = allKycData.reduce((prev, cur) => { - const st = getMerchantAccountKycStatusSimplified(cur.status); - if (st > prev) return st; - return prev; - }, MerchantAccountKycStatusSimplified.OK); + const worstKycStatus = useInstanceKYCSimplifiedWorstStatusLongPolling(); const [{ persona }] = usePreference(); const hideKycMenuItem = - simplifiedKycStatus === MerchantAccountKycStatusSimplified.OK && + worstKycStatus === MerchantAccountKycStatusSimplified.OK && persona !== "expert"; const isLoggedIn = state.status === "loggedIn"; @@ -239,13 +225,13 @@ export function Sidebar({ mobile }: Props): VNode { htmlElement="li" point={UIElement.sidebar_kycStatus} class={ - simplifiedKycStatus === + worstKycStatus === MerchantAccountKycStatusSimplified.WARNING ? "is-warning" - : simplifiedKycStatus === + : worstKycStatus === MerchantAccountKycStatusSimplified.ERROR ? "is-error" - : simplifiedKycStatus === + : worstKycStatus === MerchantAccountKycStatusSimplified.ACTION_REQUIRED ? "is-warning" : undefined @@ -255,19 +241,19 @@ export function Sidebar({ mobile }: Props): VNode { href={"#/kyc"} class="has-icon" style={ - simplifiedKycStatus === + worstKycStatus === MerchantAccountKycStatusSimplified.WARNING ? { backgroundColor: "darkorange", color: "black", } - : simplifiedKycStatus === + : worstKycStatus === MerchantAccountKycStatusSimplified.ERROR ? { backgroundColor: "#e93c3c", color: "black", } - : simplifiedKycStatus === + : worstKycStatus === MerchantAccountKycStatusSimplified.ACTION_REQUIRED ? { backgroundColor: "darkorange", @@ -365,7 +351,9 @@ export function Sidebar({ mobile }: Props): VNode { </li> <li> <div class="has-icon"> - <span class="icon" style={{ width: "3rem" }}>ID</span> + <span class="icon" style={{ width: "3rem" }}> + ID + </span> <span class="menu-item-label">{state.instance}</span> </div> </li> diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -16,6 +16,9 @@ import { AccessToken, + getMerchantAccountKycStatusSimplified, + MerchantAccountKycStatusSimplified, + TalerError, TalerHttpError, TalerMerchantManagementResultByMethod, } from "@gnu-taler/taler-util"; @@ -103,6 +106,40 @@ export function useInstanceKYCDetailsLongPolling() { return result; } +export function useInstanceKYCSimplifiedWorstStatusLongPolling() { + const kycStatus = useInstanceKYCDetailsLongPolling(); + + const allKycData = + kycStatus !== undefined && + !(kycStatus instanceof TalerError) && + kycStatus.type === "ok" + ? kycStatus.body.kyc_data + : []; + + return allKycData.reduce((prev, cur) => { + const st = getMerchantAccountKycStatusSimplified(cur.status); + if (st > prev) return st; + return prev; + }, MerchantAccountKycStatusSimplified.OK); +} + +export function useInstanceKYCSimplifiedBestStatusLongPolling() { + const kycStatus = useInstanceKYCDetailsLongPolling(); + + const allKycData = + kycStatus !== undefined && + !(kycStatus instanceof TalerError) && + kycStatus.type === "ok" + ? kycStatus.body.kyc_data + : []; + + return allKycData.reduce((prev, cur) => { + const st = getMerchantAccountKycStatusSimplified(cur.status); + if (st < prev) return st; + return prev; + }, MerchantAccountKycStatusSimplified.ERROR); +} + export function useInstanceKYCDetails() { const { state, lib } = useSessionContext(); const token = state.token; 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 @@ -21,10 +21,14 @@ import { HttpStatusCode, + MerchantAccountKycStatusSimplified, TalerError, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; -import { NotificationCardBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + NotificationCardBulma, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; @@ -32,6 +36,7 @@ import { useInstanceBankAccounts } from "../../../../hooks/bank.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; @@ -40,9 +45,66 @@ interface Props { onSelect: (id: string) => void; } -export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { +export function MissingBankAccountsWarning(): VNode { + const { i18n } = useTranslationContext(); + const result = useInstanceBankAccounts(); + + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <Fragment />; + } + if (result.type === "fail") { + return <Fragment />; + } + if (!result.body.accounts.length) { + return ( + <NotificationCardBulma + notification={{ + type: "WARN", + message: i18n.str`You must provide a bank account to receive payments.`, + description: i18n.str`Without this information, you cannot create new payment orders that are transferred to a bank account.`, + }} + /> + ); + } + return <Fragment />; +} + +export function LimitedKycActionWarning(): VNode { const { i18n } = useTranslationContext(); + const status = useInstanceKYCSimplifiedBestStatusLongPolling(); + switch (status) { + case MerchantAccountKycStatusSimplified.ACTION_REQUIRED: + return ( + <NotificationCardBulma + notification={{ + type: "WARN", + message: i18n.str`You must complete kyc requirements to receive payments.`, + description: i18n.str`Without this information, you cannot create new payment orders that are transferred to a bank account.`, + }} + /> + ); + case MerchantAccountKycStatusSimplified.WARNING: + case MerchantAccountKycStatusSimplified.ERROR: + return ( + <NotificationCardBulma + notification={{ + type: "WARN", + message: i18n.str`You must must pass KYC to receive payments.`, + description: i18n.str`Without this information, you cannot create new payment orders that are transferred to a bank account.`, + }} + /> + ); + case MerchantAccountKycStatusSimplified.OK: + return <Fragment />; + default: + assertUnreachable(status); + } + return <Fragment />; +} + +export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { const result = useInstanceBankAccounts(); if (!result) return <Loading />; @@ -65,15 +127,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { return ( <Fragment> - {result.body.accounts.length < 1 && ( - <NotificationCardBulma - notification={{ - type: "WARN", - message: i18n.str`You must provide a bank account to receive payments.`, - description: i18n.str`Without this information, you cannot create new payment orders that are transferred to a bank account.`, - }} - /> - )} + <MissingBankAccountsWarning /> <section class="section is-main-section"> <CardTable accounts={result.body.accounts.map((o) => ({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx @@ -21,10 +21,9 @@ import { assertUnreachable, - getMerchantAccountKycStatusSimplified, Paytos, Result, - TalerMerchantApi, + TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -65,6 +65,7 @@ import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { MissingBankAccountsWarning } from "../../accounts/list/index.js"; const TALER_SCREEN_ID = 42; @@ -443,39 +444,38 @@ export function CreatePage({ return ( <div> <LocalNotificationBannerBulma notification={notification} /> + <MissingBankAccountsWarning /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> <div class="column is-four-fifths"> {/* // FIXME: translating plural singular */} - <InputGroup - name="inventory_products" - label={i18n.str`Manage products in order`} - alternative={ - allProducts.length > 0 && ( - <p> - <i18n.Translate> - {allProducts.length} products with a total price of{" "} - <RenderAmountBulma - value={totalPrice.amount} - specMap={config.currencies} - /> - . - </i18n.Translate> - </p> - ) - } - tooltip={i18n.str`Manage list of products in the order.`} - > - <InventoryProductForm - currentProducts={value.inventoryProducts || {}} - onAddProduct={addProductToTheInventoryList} - inventory={instanceInventory} - /> - - <FragmentPersonaFlag - point={UIElement.option_advanceOrderCreation} + <FragmentPersonaFlag point={UIElement.option_advanceOrderCreation}> + <InputGroup + name="inventory_products" + label={i18n.str`Manage products in order`} + alternative={ + allProducts.length > 0 && ( + <p> + <i18n.Translate> + {allProducts.length} products with a total price of{" "} + <RenderAmountBulma + value={totalPrice.amount} + specMap={config.currencies} + /> + . + </i18n.Translate> + </p> + ) + } + tooltip={i18n.str`Manage list of products in the order.`} > + <InventoryProductForm + currentProducts={value.inventoryProducts || {}} + onAddProduct={addProductToTheInventoryList} + inventory={instanceInventory} + /> + <NonInventoryProductFrom productToEdit={editingProduct} onAddProduct={(p) => { @@ -483,28 +483,28 @@ export function CreatePage({ return addNewProduct(p); }} /> - </FragmentPersonaFlag> - {allProducts.length > 0 && ( - <ProductList - list={allProducts} - actions={[ - { - name: i18n.str`Remove`, - tooltip: i18n.str`Remove this product from the order.`, - handler: (e, index) => { - if (e.product_id) { - removeProductFromTheInventoryList(e.product_id); - } else { - removeFromNewProduct(index); - setEditingProduct(e); - } + {allProducts.length > 0 && ( + <ProductList + list={allProducts} + actions={[ + { + name: i18n.str`Remove`, + tooltip: i18n.str`Remove this product from the order.`, + handler: (e, index) => { + if (e.product_id) { + removeProductFromTheInventoryList(e.product_id); + } else { + removeFromNewProduct(index); + setEditingProduct(e); + } + }, }, - }, - ]} - /> - )} - </InputGroup> + ]} + /> + )} + </InputGroup> + </FragmentPersonaFlag> <FormProvider<Entity> errors={errors} diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx @@ -15,7 +15,6 @@ */ import { - AbsoluteTime, assertUnreachable, buildCodecForObject, Codec, @@ -26,7 +25,7 @@ import { HttpStatusCode, InstanceConfigurationMessage, MerchantAuthMethod, - TanChannel, + TanChannel } from "@gnu-taler/taler-util"; import { buildStorageKey, diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -1726,12 +1726,7 @@ export class TalerMerchantInstanceHttpClient { token: AccessToken, orderId: string, params: TalerMerchantApi.GetOrderRequestParams = {}, - ): Promise< - | OperationOk<TalerMerchantApi.MerchantOrderStatusResponse> - | OperationFail<TalerErrorCode.MERCHANT_GENERIC_ORDER_UNKNOWN> - | OperationFail<TalerErrorCode.MERCHANT_GENERIC_INSTANCE_UNKNOWN> - | OperationFail<HttpStatusCode.Unauthorized> - > { + ) { const url = new URL(`private/orders/${orderId}`, this.baseUrl); if (params.allowRefundedForRepurchase !== undefined) {