commit 87f78f03f3cebc4223c074e2d410df4b4050647e
parent edf2eeb58c233617e25ca8976e17646ee0b93837
Author: Sebastian <sebasjm@gmail.com>
Date: Mon, 11 Nov 2024 12:17:35 -0300
fix #9280
Diffstat:
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({