taler-typescript-core

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

commit 87f78f03f3cebc4223c074e2d410df4b4050647e
parent edf2eeb58c233617e25ca8976e17646ee0b93837
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 11 Nov 2024 12:17:35 -0300

fix #9280

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/form/FormProvider.tsx | 16+++++++++++++++-
Mpackages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx | 13+++++++------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 43+++++++++++++++++++++++--------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 48++++++++++++++++++++++++++++++++----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx | 7++++---
Mpackages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx | 5+++--
7 files changed, 85 insertions(+), 49 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { FacadeCredentials, Location, TalerMerchantApi, TranslatedString } from "@gnu-taler/taler-util"; import { ComponentChildren, createContext, h, VNode } from "preact"; import { useContext, useMemo } from "preact/hooks"; @@ -92,8 +93,21 @@ export function useFormContext<T>() { return useContext<FormType<T>>(FormContext); } +// declare const __taler_form: unique symbol; +export type TalerForm = { + __taler_form?: true; +}; + export type FormErrors<T> = { - [P in keyof T]?: string | FormErrors<T[P]>; + [P in keyof T]?: T[P] extends Location + ? FormErrors<T[P]> + : T[P] extends FacadeCredentials + ? FormErrors<T[P]> + : T[P] extends TalerForm + ? FormErrors<T[P]> + : T[P] extends Partial<TalerForm> + ? FormErrors<T[P]> + : TranslatedString | undefined; }; export type FormtoStr<T> = { diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx @@ -20,6 +20,7 @@ */ import { PaytoUri, + TranslatedString, parsePaytoUri, stringifyPaytoUri, } from "@gnu-taler/taler-util"; @@ -28,7 +29,7 @@ import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { COUNTRY_TABLE } from "../../utils/constants.js"; import { undefinedIfEmpty } from "../../utils/table.js"; -import { FormErrors, FormProvider } from "./FormProvider.js"; +import { FormErrors, FormProvider, TalerForm } from "./FormProvider.js"; import { Input } from "./Input.js"; import { InputGroup } from "./InputGroup.js"; import { InputSelector } from "./InputSelector.js"; @@ -53,7 +54,7 @@ type Entity = { amount?: string; instruction?: string; [name: string]: string | undefined; - }; + } & TalerForm; }; function isEthereumAddress(address: string) { @@ -76,7 +77,7 @@ function checkAddressChecksum(_address: string) { function validateBitcoin_path1( addr: string, i18n: ReturnType<typeof useTranslationContext>["i18n"], -): string | undefined { +): TranslatedString | undefined { try { const valid = /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/.test(addr); if (valid) return undefined; @@ -89,7 +90,7 @@ function validateBitcoin_path1( function validateEthereum_path1( addr: string, i18n: ReturnType<typeof useTranslationContext>["i18n"], -): string | undefined { +): TranslatedString | undefined { try { const valid = isEthereumAddress(addr); if (valid) return undefined; @@ -118,7 +119,7 @@ const DOMAIN_REGEX = function validateTalerBank_path1( addr: string, i18n: ReturnType<typeof useTranslationContext>["i18n"], -): string | undefined { +): TranslatedString | undefined { try { const valid = DOMAIN_REGEX.test(addr); if (valid) return undefined; @@ -145,7 +146,7 @@ function validateTalerBank_path1( function validateIBAN_path1( iban: string, i18n: ReturnType<typeof useTranslationContext>["i18n"], -): string | undefined { +): TranslatedString | undefined { // Check total length if (iban.length < 4) return i18n.str`IBAN numbers usually have more that 4 digits`; 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 @@ -36,6 +36,7 @@ import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, + TalerForm, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; @@ -49,7 +50,9 @@ import { undefinedIfEmpty } from "../../../../utils/table.js"; import { safeConvertURL } from "../update/UpdatePage.js"; import { testRevenueAPI, TestRevenueErrorType } from "./index.js"; -type Entity = TalerMerchantApi.AccountAddDetails & { verified?: boolean }; +type Entity = TalerMerchantApi.AccountAddDetails & { + verified?: boolean; +} & TalerForm; interface Props { onCreate: (d: TalerMerchantApi.AccountAddDetails) => Promise<void>; @@ -72,23 +75,25 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const [testError, setTestError] = useState<TranslatedString | undefined>( undefined, ); - const errors: FormErrors<Entity> = { + const errors = undefinedIfEmpty<FormErrors<Entity>>({ payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, - credit_facade_credentials: !state.credit_facade_credentials - ? undefined - : undefinedIfEmpty({ - username: - state.credit_facade_credentials.type === "basic" && - !state.credit_facade_credentials.username - ? i18n.str`Required` - : undefined, - password: - state.credit_facade_credentials.type === "basic" && - !state.credit_facade_credentials.password - ? i18n.str`Required` - : undefined, - }), + credit_facade_credentials: undefinedIfEmpty( + !state.credit_facade_credentials + ? undefined + : { + username: + state.credit_facade_credentials.type === "basic" && + !state.credit_facade_credentials.username + ? i18n.str`Required` + : undefined, + password: + state.credit_facade_credentials.type === "basic" && + !state.credit_facade_credentials.password + ? i18n.str`Required` + : undefined, + }, + ) as any, credit_facade_url: !state.credit_facade_url ? undefined : !facadeURL @@ -100,11 +105,9 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : facadeURL.hash ? i18n.str`URL must not hash param` : undefined, - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as Record<string, unknown>)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); 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 @@ -138,7 +138,7 @@ export function UpdatePage({ : !state.credit_facade_credentials.password ? i18n.str`Required` : undefined, - }), + }) as any, }); const hasErrors = errors !== undefined; 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 @@ -34,6 +34,7 @@ import { useEffect, useState } from "preact/hooks"; import { FormErrors, FormProvider, + TalerForm, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; @@ -47,10 +48,11 @@ import { InventoryProductForm } from "../../../../components/product/InventoryPr import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js"; import { ProductList } from "../../../../components/product/ProductList.js"; import { useSessionContext } from "../../../../context/session.js"; +import { WithId } from "../../../../declaration.js"; import { usePreference } from "../../../../hooks/preference.js"; import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { WithId } from "../../../../declaration.js"; +import { error } from "console"; interface Props { onCreate: (d: TalerMerchantApi.PostOrderRequest) => void; @@ -103,17 +105,17 @@ export interface ProductMap { [id: string]: ProductAndQuantity; } -interface Pricing { +interface Pricing extends TalerForm { products_price: string; order_price: string; summary: string; } -interface Shipping { +interface Shipping extends TalerForm { delivery_date?: Date; delivery_location?: TalerMerchantApi.Location; fullfilment_url?: string; } -interface Payments { +interface Payments extends TalerForm { refund_deadline: Duration; pay_deadline: Duration; wire_transfer_deadline: Duration; @@ -122,7 +124,7 @@ interface Payments { createToken: boolean; minimum_age?: number; } -interface Entity { +interface Entity extends TalerForm { inventoryProducts: ProductMap; products: TalerMerchantApi.Product[]; pricing: Partial<Pricing>; @@ -152,7 +154,7 @@ export function CreatePage({ : Amounts.parse(value.pricing.order_price); const errors = undefinedIfEmpty<FormErrors<Entity>>({ - pricing: undefinedIfEmpty({ + pricing: undefinedIfEmpty<FormErrors<Pricing>>({ summary: !value.pricing?.summary ? i18n.str`Required` : undefined, order_price: !value.pricing?.order_price ? i18n.str`Required` @@ -489,7 +491,7 @@ export function CreatePage({ tooltip={i18n.str`Title of the order to be shown to the customer`} /> - {pref.advanceOrderMode && ( + {(pref.advanceOrderMode|| errors?.shipping) && ( <InputGroup name="shipping" label={i18n.str`Shipping and fulfillment`} @@ -517,13 +519,13 @@ export function CreatePage({ </InputGroup> )} - {(pref.advanceOrderMode || requiresSomeTalerOptions) && ( + {(pref.advanceOrderMode || requiresSomeTalerOptions || errors?.payments) && ( <InputGroup name="payments" label={i18n.str`Taler payment options`} tooltip={i18n.str`Override default Taler payment settings for this order`} > - {(pref.advanceOrderMode || noDefault_payDeadline) && ( + {(pref.advanceOrderMode || noDefault_payDeadline || errors?.payments?.pay_deadline !== undefined) && ( <InputDuration name="payments.pay_deadline" label={i18n.str`Payment time`} @@ -555,7 +557,7 @@ export function CreatePage({ } /> )} - {pref.advanceOrderMode && ( + {(pref.advanceOrderMode || errors?.payments?.refund_deadline !== undefined) && ( <InputDuration name="payments.refund_deadline" label={i18n.str`Refund time`} @@ -588,7 +590,7 @@ export function CreatePage({ } /> )} - {(pref.advanceOrderMode || noDefault_wireDeadline) && ( + {(pref.advanceOrderMode || noDefault_wireDeadline || errors?.payments?.wire_transfer_deadline !== undefined) && ( <InputDuration name="payments.wire_transfer_deadline" label={i18n.str`Wire transfer time`} @@ -622,7 +624,7 @@ export function CreatePage({ } /> )} - {pref.advanceOrderMode && ( + {(pref.advanceOrderMode || errors?.payments?.auto_refund_deadline !== undefined) && ( <InputDuration name="payments.auto_refund_deadline" label={i18n.str`Auto-refund time`} @@ -636,21 +638,21 @@ export function CreatePage({ /> )} - {pref.advanceOrderMode && ( + {(pref.advanceOrderMode || errors?.payments?.max_fee !== undefined) && ( <InputCurrency name="payments.max_fee" label={i18n.str`Maximum fee`} tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} /> )} - {pref.advanceOrderMode && ( + {(pref.advanceOrderMode|| errors?.payments?.createToken !== undefined) && ( <InputToggle name="payments.createToken" label={i18n.str`Create token`} tooltip={i18n.str`If the order ID is easy to guess the token will prevent users to steal orders from others.`} /> )} - {pref.advanceOrderMode && ( + {(pref.advanceOrderMode|| errors?.payments?.minimum_age !== undefined) && ( <InputNumber name="payments.minimum_age" label={i18n.str`Minimum age required`} @@ -665,7 +667,7 @@ export function CreatePage({ </InputGroup> )} - {pref.advanceOrderMode && ( + {(pref.advanceOrderMode|| errors?.extra !== undefined) && ( <InputGroup name="extra" label={i18n.str`Additional information`} @@ -800,3 +802,17 @@ function DeadlineHelp({ duration }: { duration?: Duration }): VNode { </i18n.Translate> ); } + +function getAll(s: object): string[] { + return Object.entries(s).flatMap(([key, value]) => { + if (typeof value === "object") + return getAll(value).map((v) => `${key}.${v}`); + if (!value) return []; + return key; + }); +} + +function describeMissingFields(errors: object | undefined): string[] { + if (!errors) return []; + return getAll(errors); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -28,6 +28,7 @@ import emptyImage from "../../../../assets/empty.png"; import { FormErrors, FormProvider, + TalerForm, } from "../../../../components/form/FormProvider.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; @@ -335,7 +336,7 @@ interface FastProductUpdateFormProps { onUpdate: (data: TalerMerchantApi.ProductPatchDetail) => Promise<void>; onCancel: () => void; } -interface FastProductUpdate { +interface FastProductUpdate extends TalerForm { incoming: number; lost: number; price: string; @@ -403,6 +404,7 @@ function FastProductWithManagedStockUpdateForm({ lost: 0, price: product.price, }); + const { i18n } = useTranslationContext(); const currentStock = product.total_stock - product.total_sold - product.total_lost; @@ -410,7 +412,7 @@ function FastProductWithManagedStockUpdateForm({ const errors: FormErrors<FastProductUpdate> = { lost: currentStock + value.incoming < value.lost - ? `lost can't be greater that current + incoming (max ${ + ? i18n.str`lost can't be greater that current + incoming (max ${ currentStock + value.incoming })` : undefined, @@ -419,7 +421,6 @@ function FastProductWithManagedStockUpdateForm({ const hasErrors = Object.keys(errors).some( (k) => (errors as Record<string, unknown>)[k] !== undefined, ); - const { i18n } = useTranslationContext(); return ( <Fragment> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -27,6 +27,7 @@ import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, + TalerForm, } from "../../../components/form/FormProvider.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; import { useSessionContext } from "../../../context/session.js"; @@ -35,7 +36,7 @@ import { undefinedIfEmpty } from "../../../utils/table.js"; export type Entity = Omit<Omit<TalerMerchantApi.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & { default_pay_delay: Duration, default_wire_transfer_delay: Duration, -}; +} & TalerForm; interface Props { onUpdate: (d: TalerMerchantApi.InstanceReconfigurationMessage) => void; @@ -54,7 +55,7 @@ function convert( default_pay_delay: Duration.fromTalerProtocolDuration(default_pay_delay), default_wire_transfer_delay: Duration.fromTalerProtocolDuration(default_wire_transfer_delay), }; - return { ...defaults, ...rest }; + return { ...defaults, ...rest } as Entity; } export function UpdatePage({