commit 4f3f4d303ad8d6c4e3462ba6f3950412eec5b2c4 parent e38a967023ffe5e8229aef25f673daff44f99dc4 Author: Sebastian <sebasjm@gmail.com> Date: Thu, 11 Jul 2024 11:19:44 -0300 fixing i18n strings Diffstat:
79 files changed, 943 insertions(+), 1203 deletions(-)
diff --git a/packages/merchant-backoffice-ui/dev.mjs b/packages/merchant-backoffice-ui/dev.mjs @@ -24,7 +24,7 @@ const build = initializeDev({ type: "development", source: { js: devEntryPoints, - assets: [{base:"src",files:["src/index.html"]}], + assets: [{base:"src",files:["src/index.html","src/settings.json"]}], }, css: "sass", destination: "./dist/dev", diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -119,9 +119,6 @@ export enum InstancePaths { interface = "/interface", } -// eslint-disable-next-line @typescript-eslint/no-empty-function -// const noop = () => { }; - export enum AdminPaths { list_instances = "/instances", new_instance = "/instance/new", @@ -596,52 +593,12 @@ function AdminInstanceUpdatePage({ id, ...rest }: { id: string } & InstanceUpdatePageProps): VNode { - // const { i18n } = useTranslationContext(); return ( <Fragment> <InstanceAdminUpdatePage {...rest} instanceId={id} - // onLoadError={(error: HttpError<TalerErrorDetail>) => { - // const notif = - // error.type === ErrorType.TIMEOUT - // ? { - // message: i18n.str`The request to the backend take too long and was cancelled`, - // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - // type: "ERROR" as const, - // } - // : { - // message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, - // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - // details: - // error.type === ErrorType.CLIENT || - // error.type === ErrorType.SERVER - // ? error.payload.hint - // : undefined, - // type: "ERROR" as const, - // }; - // return ( - // <Fragment> - // <NotificationCard notification={notif} /> - // <LoginPage /> - // </Fragment> - // ); - // }} - // onUnauthorized={() => { - // return ( - // <Fragment> - // <NotificationCard - // notification={{ - // message: i18n.str`Access denied`, - // description: i18n.str`The access token provided is invalid`, - // type: "ERROR", - // }} - // /> - // <LoginPage /> - // </Fragment> - // ); - // }} /> </Fragment> ); @@ -707,7 +664,7 @@ function KycBanner(): VNode { <NotificationCard notification={{ type: "WARN", - message: "KYC verification needed", + message: i18n.str`KYC verification needed`, description: ( <div> <p> diff --git a/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx @@ -1,7 +1,6 @@ /* -/* This file is part of GNU Taler - (C) 2022 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -15,115 +14,175 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util"; +import { TalerError, TalerErrorCode } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { VNode, h } from "preact"; import { NotificationCard } from "./menu/index.js"; /** * equivalent to ErrorLoading for merchant-backoffice which uses notification-card - * @param param0 - * @returns + * @param param0 + * @returns */ -export function ErrorLoadingMerchant({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode { - const { i18n } = useTranslationContext() +export function ErrorLoadingMerchant({ + error, +}: { + error: TalerError; + showDetail?: boolean; +}): VNode { + const { i18n } = useTranslationContext(); switch (error.errorDetail.code) { ////////////////// // Every error that can be produce in a Http Request ////////////////// case TalerErrorCode.GENERIC_TIMEOUT: { if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) { - const { requestMethod, requestUrl, timeoutMs } = error.errorDetail - return <NotificationCard - notification={{ - type: "ERROR", - message: i18n.str`The request reached a timeout, check your connection.`, - description: error.message, - details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2) - }} /> + const { requestMethod, requestUrl, timeoutMs } = error.errorDetail; + return ( + <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`The request reached a timeout, check your connection.`, + description: error.message, + details: JSON.stringify( + { requestMethod, requestUrl, timeoutMs }, + undefined, + 2, + ), + }} + /> + ); } - assertUnreachable(1 as never) + break; } case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: { if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) { - const { requestMethod, requestUrl, timeoutMs } = error.errorDetail - return <NotificationCard - notification={{ - type: "ERROR", - message: i18n.str`The request was cancelled.`, - description: error.message, - details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2) - }} /> + const { requestMethod, requestUrl, timeoutMs } = error.errorDetail; + return ( + <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`The request was cancelled.`, + description: error.message, + details: JSON.stringify( + { requestMethod, requestUrl, timeoutMs }, + undefined, + 2, + ), + }} + /> + ); } - assertUnreachable(1 as never) + break; } case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { - if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) { - const { requestMethod, requestUrl, timeoutMs } = error.errorDetail - return <NotificationCard - notification={{ - type: "ERROR", - message: i18n.str`The request reached a timeout, check your connection.`, - description: error.message, - details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2) - }} /> + if ( + error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT) + ) { + const { requestMethod, requestUrl, timeoutMs } = error.errorDetail; + return ( + <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`The request reached a timeout, check your connection.`, + description: error.message, + details: JSON.stringify( + { requestMethod, requestUrl, timeoutMs }, + undefined, + 2, + ), + }} + /> + ); } - assertUnreachable(1 as never) + break; } case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { - const { requestMethod, requestUrl, throttleStats } = error.errorDetail - return <NotificationCard - notification={{ - type: "ERROR", - message: i18n.str`A lot of request were made to the same server and this action was throttled.`, - description: error.message, - details: JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2) - }} /> + const { requestMethod, requestUrl, throttleStats } = error.errorDetail; + return ( + <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`A lot of request were made to the same server and this action was throttled.`, + description: error.message, + details: JSON.stringify( + { requestMethod, requestUrl, throttleStats }, + undefined, + 2, + ), + }} + /> + ); } - assertUnreachable(1 as never) + break; } case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { - if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) { - const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail - return <NotificationCard - notification={{ - type: "ERROR", - message: i18n.str`The response of the request is malformed.`, - description: error.message, - details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2) - }} /> + if ( + error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE) + ) { + const { requestMethod, requestUrl, httpStatusCode, validationError } = + error.errorDetail; + return ( + <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`The response of the request is malformed.`, + description: error.message, + details: JSON.stringify( + { requestMethod, requestUrl, httpStatusCode, validationError }, + undefined, + 2, + ), + }} + /> + ); } - assertUnreachable(1 as never) + break; } case TalerErrorCode.WALLET_NETWORK_ERROR: { if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) { - const { requestMethod, requestUrl } = error.errorDetail - return <NotificationCard - notification={{ - type: "ERROR", - message: i18n.str`Could not complete the request due to a network problem.`, - description: error.message, - details: JSON.stringify({ requestMethod, requestUrl }, undefined, 2) - }} /> + const { requestMethod, requestUrl } = error.errorDetail; + return ( + <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`Could not complete the request due to a network problem.`, + description: error.message, + details: JSON.stringify( + { requestMethod, requestUrl }, + undefined, + 2, + ), + }} + /> + ); } - assertUnreachable(1 as never) + break; } case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) { - const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail - return <NotificationCard - notification={{ - type: "ERROR", - message: i18n.str`Unexpected request error.`, - description: error.message, - details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2) - }} /> + const { requestMethod, requestUrl, httpStatusCode, errorResponse } = + error.errorDetail; + return ( + <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`Unexpected request error.`, + description: error.message, + details: JSON.stringify( + { requestMethod, requestUrl, httpStatusCode, errorResponse }, + undefined, + 2, + ), + }} + /> + ); } - assertUnreachable(1 as never) + break; } ////////////////// - // Every other error + // Every other error ////////////////// // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { // return <Attention type="danger" title={i18n.str``}> @@ -133,14 +192,34 @@ export function ErrorLoadingMerchant({ error, showDetail }: { error: TalerError, // Default message for unhandled case ////////////////// default: { - return <NotificationCard - notification={{ - type: "ERROR", - message: i18n.str`Unexpected error.`, - description: error.message, - details: JSON.stringify(error.errorDetail, undefined, 2) - }} /> + return ( + <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`Unexpected error.`, + description: error.message, + details: JSON.stringify(error.errorDetail, undefined, 2), + }} + /> + ); } } + /** + * This should not happen + * + * The only reason why this is possible is because the case statement + * follows and if `if (error.hasErrorCode` which returned false. + * + * TODO: add a better check + */ + return ( + <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`Unexpected error.`, + description: error.message, + details: JSON.stringify(error.errorDetail, undefined, 2), + }} + /> + ); } - diff --git a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx @@ -50,7 +50,7 @@ export function InputArray<T>({ const error = localError || formError; - const array: any[] = (value ? value! : []) as any; + const array: T[keyof T][] = (value ? value! : []); const [currentValue, setCurrentValue] = useState(""); const { i18n } = useTranslationContext(); @@ -104,12 +104,12 @@ export function InputArray<T>({ return; } setLocalError(null); - onChange([v, ...array] as any); + onChange([v, ...array] as T[keyof T]); setCurrentValue(""); }} - data-tooltip={i18n.str`add element to the list`} + data-tooltip={i18n.str`Add element to the list`} > - <i18n.Translate>add</i18n.Translate> + <i18n.Translate>Add</i18n.Translate> </button> </p> </div> @@ -121,12 +121,12 @@ export function InputArray<T>({ class="tag is-medium is-info mb-0" style={{ maxWidth: "90%" }} > - {v} + {String(v)} </span> <a class="tag is-medium is-danger is-delete mb-0" onClick={() => { - onChange(array.filter((f) => f !== v) as any); + onChange(array.filter((f) => f !== v) as T[keyof T]); setCurrentValue(toStr(v)); }} /> diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx @@ -124,25 +124,25 @@ export function InputDate<T>({ <span data-tooltip={ withTimestampSupport - ? i18n.str`change value to unknown date` - : i18n.str`change value to empty` + ? i18n.str`Change value to unknown date` + : i18n.str`Change value to empty` } > <button class="button is-info mr-3" onClick={() => onChange(undefined as any)} > - <i18n.Translate>clear</i18n.Translate> + <i18n.Translate>Clear</i18n.Translate> </button> </span> )} {withTimestampSupport && ( - <span data-tooltip={i18n.str`change value to never`}> + <span data-tooltip={i18n.str`Change value to never`}> <button class="button is-info" onClick={() => onChange({ t_s: "never" } as any)} > - <i18n.Translate>never</i18n.Translate> + <i18n.Translate>Never</i18n.Translate> </button> </span> )} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx @@ -56,7 +56,7 @@ export function InputDuration<T>({ if (!value) { strValue = ""; } else if (value.d_ms === "forever") { - strValue = i18n.str`forever`; + strValue = i18n.str`Forever`; } else { if (value.d_ms === undefined) { throw Error( @@ -149,22 +149,22 @@ export function InputDuration<T>({ </div> {withForever && ( - <span data-tooltip={i18n.str`change value to never`}> + <span data-tooltip={i18n.str`Change value to never`}> <button class="button is-info mr-3" onClick={() => onChange({ d_ms: "forever" } as any)} > - <i18n.Translate>forever</i18n.Translate> + <i18n.Translate>Forever</i18n.Translate> </button> </span> )} {!readonly && !withoutClear && ( - <span data-tooltip={i18n.str`change value to empty`}> + <span data-tooltip={i18n.str`Change value to empty`}> <button class="button is-info " onClick={() => onChange(undefined as any)} > - <i18n.Translate>clear</i18n.Translate> + <i18n.Translate>Clear</i18n.Translate> </button> </span> )} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx @@ -92,7 +92,7 @@ export function InputImage<T>({ "", ), ); - return onChange(`data:${f[0].type};base64,${b64}` as any); + return onChange(`data:${f[0].type};base64,${b64}` as T[keyof T]); }); }} /> @@ -102,7 +102,7 @@ export function InputImage<T>({ {error && <p class="help is-danger">{error}</p>} {sizeError && ( <p class="help is-danger"> - <i18n.Translate>Image should be smaller than 1 MB</i18n.Translate> + <i18n.Translate>Image must be smaller than 1 MB</i18n.Translate> </p> )} {!value && ( diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx @@ -18,9 +18,13 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { + PaytoUri, + parsePaytoUri, + stringifyPaytoUri, +} from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +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"; @@ -30,9 +34,7 @@ import { InputGroup } from "./InputGroup.js"; import { InputSelector } from "./InputSelector.js"; import { InputProps, useField } from "./useField.js"; -export interface Props<T> extends InputProps<T> { - isValid?: (e: any) => boolean; -} +export interface Props<T> extends InputProps<T> {} // type Entity = PaytoUriGeneric // https://datatracker.ietf.org/doc/html/rfc8905 @@ -175,7 +177,7 @@ function validateIBAN_path1( const checksum = calculate_iban_checksum(step3); if (checksum !== 1) - return i18n.str`IBAN number is not valid, checksum is wrong`; + return i18n.str`IBAN number is invalid, checksum is wrong`; return undefined; } @@ -192,17 +194,16 @@ export function InputPaytoForm<T>({ const targets = [ i18n.str`Choose one...`, - i18n.str`iban`, - i18n.str`x-taler-bank`, - i18n.str`bitcoin`, - i18n.str`ethereum`, + "iban", + "bitcoin", + "ethereum", + "x-taler-bank", ]; const noTargetValue = targets[0]; const defaultTarget: Entity = { target: noTargetValue, params: {}, }; - const paths = !initialPayto ? [] : initialPayto.targetPath.split("/"); const initialPath1 = paths.length >= 1 ? paths[0] : undefined; @@ -235,10 +236,10 @@ export function InputPaytoForm<T>({ } }, [initialValueStr]); - const errors: FormErrors<Entity> = { - target: value.target === noTargetValue ? i18n.str`required` : undefined, + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + target: value.target === noTargetValue ? i18n.str`Required` : undefined, path1: !value.path1 - ? i18n.str`required` + ? i18n.str`Required` : value.target === "iban" ? validateIBAN_path1(value.path1, i18n) : value.target === "bitcoin" @@ -251,38 +252,36 @@ export function InputPaytoForm<T>({ path2: value.target === "x-taler-bank" ? !value.path2 - ? i18n.str`required` + ? i18n.str`Required` : undefined : undefined, params: undefinedIfEmpty({ "receiver-name": !value.params?.["receiver-name"] - ? i18n.str`required` + ? i18n.str`Required` : undefined, }), - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const path1WithSlash = value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1; const pto = hasErrors || !value.target ? undefined - : { + : ({ targetType: value.target, targetPath: value.path2 ? `${path1WithSlash}${value.path2}` : value.path1 ?? "", - params: value.params ?? ({} as any), + params: value.params ?? {}, isKnown: false as const, - }; + } as PaytoUri); const str = !pto ? undefined : stringifyPaytoUri(pto); useEffect(() => { - onChange(str as any); + onChange(str as T[keyof T]); }, [str]); return ( diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx @@ -103,7 +103,7 @@ export function InputSearchOnList<T extends Entity>({ <InputWithAddon<Search> name="name" label={label} - tooltip={i18n.str`enter description or id`} + tooltip={i18n.str`Enter description or id`} addonAfter={ <span class="icon"> <i class="mdi mdi-magnify" /> diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx @@ -78,14 +78,16 @@ export function InputSelector<T>({ ); })} </select> - - {help} </p> - {required && ( - <span class="icon has-text-danger is-right" style={{height: "2.5em"}}> - <i class="mdi mdi-alert" /> - </span> - )} + <p class="help">{help}</p> + {required && ( + <span + class="icon has-text-danger is-right" + style={{ height: "2.5em" }} + > + <i class="mdi mdi-alert" /> + </span> + )} {error && <p class="help is-danger">{error}</p>} </div> </div> diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx @@ -96,7 +96,7 @@ export function InputStock<T>({ {!alreadyExist ? ( <button class="button" - data-tooltip={i18n.str`click here to configure the stock of the product, leave it as is and the backend will not control stock`} + data-tooltip={i18n.str`Click here to configure the stock of the product, leave it as is and the backend will not control stock.`} onClick={(): void => { valueHandler({ current: 0, @@ -112,7 +112,7 @@ export function InputStock<T>({ ) : ( <button class="button" - data-tooltip={i18n.str`this product has been configured without stock control`} + data-tooltip={i18n.str`This product has been configured without stock control`} disabled > <span> @@ -133,17 +133,11 @@ export function InputStock<T>({ const stockAddedErrors: FormErrors<typeof addedStock> = { lost: currentStock + addedStock.incoming < addedStock.lost - ? i18n.str`lost cannot be greater than current and incoming (max ${currentStock + addedStock.incoming + ? i18n.str`Lost can't be greater than current and incoming (max ${currentStock + addedStock.incoming })` : undefined, }; - // const stockUpdateDescription = stockAddedErrors.lost ? '' : ( - // !!addedStock.incoming || !!addedStock.lost ? - // i18n.str`current stock will change from ${currentStock} to ${currentStock + addedStock.incoming - addedStock.lost}` : - // i18n.str`current stock will stay at ${currentStock}` - // ) - return ( <Fragment> <div class="card"> @@ -192,7 +186,7 @@ export function InputStock<T>({ side={ <button class="button is-danger" - data-tooltip={i18n.str`remove stock control for this product`} + data-tooltip={i18n.str`Remove stock control for this product`} onClick={(): void => { valueHandler(undefined as any); }} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx @@ -40,15 +40,15 @@ export function InputTaxes<T>({ name, label }: Props<keyof T>): VNode { const [value, valueHandler] = useState<Partial<Entity>>({}); const errors = undefinedIfEmpty({ - name: !value.name ? i18n.str`required` : undefined, + name: !value.name ? i18n.str`Required` : undefined, tax: !value.tax - ? i18n.str`required` + ? i18n.str`Required` : Amounts.parse(value.tax) === undefined - ? i18n.str`invalid` + ? i18n.str`Invalid` : undefined, }); - const hasErrors = errors === undefined; + const hasErrors = errors !== undefined; const submit = useCallback((): void => { onChange([value as any, ...taxes] as any); @@ -124,7 +124,7 @@ export function InputTaxes<T>({ name, label }: Props<keyof T>): VNode { <div class="buttons is-right mt-5"> <button class="button is-info" - data-tooltip={i18n.str`add tax to the tax list`} + data-tooltip={i18n.str`Add tax to the tax list`} disabled={hasErrors} onClick={submit} > diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx @@ -1,19 +1,42 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ import { TranslatedString } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -export function JumpToElementById({ testIfExist, onSelect, placeholder, description }: { placeholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<boolean>, onSelect: (id: string) => void }): VNode { - const { i18n } = useTranslationContext() +export function JumpToElementById({ + testIfExist, + onSelect, + placeholder, + description, +}: { + placeholder: TranslatedString; + description: TranslatedString; + testIfExist: (id: string) => Promise<boolean>; + onSelect: (id: string) => void; +}): VNode { + const { i18n } = useTranslationContext(); - const [error, setError] = useState<string | undefined>( - undefined, - ); + const [error, setError] = useState<string | undefined>(undefined); - const [id, setId] = useState<string>() + const [id, setId] = useState<string>(); async function check(currentId: string | undefined): Promise<void> { if (!currentId) { - setError(i18n.str`missing id`); + setError(i18n.str`Missing id`); return; } try { @@ -22,42 +45,38 @@ export function JumpToElementById({ testIfExist, onSelect, placeholder, descript onSelect(currentId); setError(undefined); } else { - setError(i18n.str`not found`); + setError(i18n.str`Not found`); } } catch { - setError(i18n.str`not found`); + setError(i18n.str`Not found`); } } - return <div class="level"> - <div class="level-left"> - <div class="level-item"> - <div class="field has-addons"> - <div class="control"> - <input - class={error ? "input is-danger" : "input"} - type="text" - value={id ?? ""} - onChange={(e) => setId(e.currentTarget.value)} - placeholder={placeholder} - /> - {error && <p class="help is-danger">{error}</p>} + return ( + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <div class="field has-addons"> + <div class="control"> + <input + class={error ? "input is-danger" : "input"} + type="text" + value={id ?? ""} + onChange={(e) => setId(e.currentTarget.value)} + placeholder={placeholder} + /> + {error && <p class="help is-danger">{error}</p>} + </div> + <span class="has-tooltip-bottom" data-tooltip={description}> + <button class="button" onClick={() => check(id)}> + <span class="icon"> + <i class="mdi mdi-arrow-right" /> + </span> + </button> + </span> </div> - <span - class="has-tooltip-bottom" - data-tooltip={description} - > - <button - class="button" - onClick={(e) => check(id)} - > - <span class="icon"> - <i class="mdi mdi-arrow-right" /> - </span> - </button> - </span> </div> </div> </div> - </div> + ); } diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -25,6 +25,7 @@ import { Fragment, VNode, h } from "preact"; import { useSessionContext } from "../../context/session.js"; import { useInstanceKYCDetails } from "../../hooks/instance.js"; import { LangSelector } from "./LangSelector.js"; +import { usePreference } from "../../hooks/preference.js"; // const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; @@ -37,6 +38,7 @@ export function Sidebar({ mobile }: Props): VNode { const { i18n } = useTranslationContext(); const { state, logOut, config } = useSessionContext(); const kycStatus = useInstanceKYCDetails(); + const [pref] = usePreference(); const needKYC = kycStatus !== undefined && @@ -45,7 +47,7 @@ export function Sidebar({ mobile }: Props): VNode { !!kycStatus.body; const isLoggedIn = state.status === "loggedIn"; const hasToken = isLoggedIn && state.token !== undefined; - + return ( <aside class="aside is-placed-left is-expanded" @@ -118,16 +120,18 @@ export function Sidebar({ mobile }: Props): VNode { </span> </a> </li> - <li> - <a href={"/tokenfamilies"} class="has-icon"> - <span class="icon"> - <i class="mdi mdi-key" /> - </span> - <span class="menu-item-label"> - <i18n.Translate>Token Families</i18n.Translate> - </span> - </a> - </li> + {pref.developerMode ? ( + <li> + <a href={"/tokenfamilies"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-key" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Token Families</i18n.Translate> + </span> + </a> + </li> + ) : undefined} {needKYC && ( <li> <a href={"/kyc"} class="has-icon"> diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -143,7 +143,13 @@ export function ContinueModal({ ); } -export function SimpleModal({ onCancel, children }: any): VNode { +export function SimpleModal({ + onCancel, + children, +}: { + onCancel: () => void; + children: ComponentChildren; +}): VNode { return ( <div class="modal is-active"> <div class="modal-background " onClick={onCancel} /> @@ -233,15 +239,13 @@ export function ImportingAccountModal({ parsed !== undefined ? codecForAccountLetter().decode(parsed) : undefined; } catch (e) { account = undefined; - if (e instanceof Error) { - parsingError = e.message; - } + parsingError = e instanceof Error ? e.message : String(e); } const errors: FormErrors<{ letter: string }> = { letter: !letter - ? i18n.str`required` + ? i18n.str`Required` : parsed === undefined - ? i18n.str`letter should be a JSON string` + ? i18n.str`Letter must be a JSON string` : account === undefined ? i18n.str`JSON string is invalid` : undefined, @@ -329,7 +333,7 @@ export function CompareAccountsModal({ const { i18n } = useTranslationContext(); return ( <ConfirmModal - label={i18n.str`Update form`} + label={i18n.str`Correct form`} description={i18n.str`Comparing account details`} active onCancel={onCancel} @@ -455,7 +459,7 @@ export function DeleteModal({ <i18n.Translate> Deleting an instance{" "} <b> - <i18n.Translate>cannot be undone</i18n.Translate> + <i18n.Translate>Can't be undone</i18n.Translate> </b> . </i18n.Translate> @@ -496,7 +500,7 @@ export function PurgeModal({ <i18n.Translate> Purging an instance{" "} <b> - <i18n.Translate>cannot be undone</i18n.Translate> + <i18n.Translate>Can't be undone</i18n.Translate> </b> . </i18n.Translate> @@ -530,20 +534,20 @@ export function UpdateTokenModal({ const hasInputTheCorrectOldToken = oldToken && oldToken !== form.old_token; const errors = undefinedIfEmpty({ old_token: hasInputTheCorrectOldToken - ? i18n.str`is not the same as the current access token` + ? i18n.str`Is not the same as the current access token` : undefined, new_token: !form.new_token - ? i18n.str`cannot be empty` + ? i18n.str`Required` : form.new_token === form.old_token - ? i18n.str`cannot be the same as the old token` + ? i18n.str`Can't be the same as the old token` : undefined, repeat_token: form.new_token !== form.repeat_token - ? i18n.str`is not the same` + ? i18n.str`Is not the same` : undefined, }); - const hasErrors = errors === undefined; + const hasErrors = errors !== undefined; const { state } = useSessionContext(); @@ -564,20 +568,20 @@ export function UpdateTokenModal({ <Input<State> name="old_token" label={i18n.str`Old access token`} - tooltip={i18n.str`access token currently in use`} + tooltip={i18n.str`Access token currently in use`} inputType="password" /> )} <Input<State> name="new_token" label={i18n.str`New access token`} - tooltip={i18n.str`next access token to be used`} + tooltip={i18n.str`Next access token to be used`} inputType="password" /> <Input<State> name="repeat_token" label={i18n.str`Repeat access token`} - tooltip={i18n.str`confirm the same access token`} + tooltip={i18n.str`Confirm the same access token`} inputType="password" /> </FormProvider> @@ -605,21 +609,19 @@ export function SetTokenNewInstanceModal({ }); const { i18n } = useTranslationContext(); - const errors = { + const errors = undefinedIfEmpty({ new_token: !form.new_token - ? i18n.str`cannot be empty` + ? i18n.str`Required` : form.new_token === form.old_token - ? i18n.str`cannot be the same as the old access token` + ? i18n.str`Can't be the same as the old access token` : undefined, repeat_token: form.new_token !== form.repeat_token - ? i18n.str`is not the same` + ? i18n.str`Is not the same` : undefined, - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; return ( <div class="modal is-active"> @@ -641,13 +643,13 @@ export function SetTokenNewInstanceModal({ <Input<State> name="new_token" label={i18n.str`New access token`} - tooltip={i18n.str`next access token to be used`} + tooltip={i18n.str`Next access token to be used`} inputType="password" /> <Input<State> name="repeat_token" label={i18n.str`Repeat access token`} - tooltip={i18n.str`confirm the same access token`} + tooltip={i18n.str`Confirm the same access token`} inputType="password" /> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx @@ -110,7 +110,7 @@ export function InventoryProductForm({ <InputNumber<Form> name="quantity" label={i18n.str`Quantity`} - tooltip={i18n.str`how many products will be added`} + tooltip={i18n.str`How many products will be added`} /> )} </div> diff --git a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx @@ -15,17 +15,16 @@ */ import { AmountString, Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks"; -import * as yup from "yup"; import { useListener } from "../../hooks/listener.js"; +import { undefinedIfEmpty } from "../../utils/table.js"; import { FormErrors, FormProvider } from "../form/FormProvider.js"; import { Input } from "../form/Input.js"; import { InputCurrency } from "../form/InputCurrency.js"; import { InputImage } from "../form/InputImage.js"; import { InputNumber } from "../form/InputNumber.js"; import { InputTaxes } from "../form/InputTaxes.js"; -import { undefinedIfEmpty } from "../../utils/table.js"; type Entity = TalerMerchantApi.Product; @@ -69,7 +68,7 @@ export function NonInventoryProductFrom({ <div class="buttons"> <button class="button is-success" - data-tooltip={i18n.str`describe and add a product that is not in the inventory list`} + data-tooltip={i18n.str`Describe and add a product that is not in the inventory list`} onClick={() => setShowCreateProduct(true)} > <i18n.Translate>Add custom product</i18n.Translate> @@ -145,22 +144,21 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { taxes: [], ...initial, }); - // let errors: FormErrors<NonInventoryProduct> = {}; - const errors: FormErrors<NonInventoryProduct> | undefined = undefinedIfEmpty({ + const errors = undefinedIfEmpty<FormErrors<NonInventoryProduct>>({ quantity: value.quantity === undefined - ? i18n.str`required` + ? i18n.str`Required` : typeof value.quantity !== "number" - ? i18n.str`should be a number` + ? i18n.str`Must be a number` : value.quantity < 1 - ? i18n.str`should be grater than 0` + ? i18n.str`Must be grater than 0` : undefined, - description: !value.description ? i18n.str`required` : undefined, - unit: !value.description ? i18n.str`required` : undefined, + description: !value.description ? i18n.str`Required` : undefined, + unit: !value.description ? i18n.str`Required` : undefined, price: !value.price - ? i18n.str`required` + ? i18n.str`Required` : Amounts.parse(value.price) === undefined - ? i18n.str`invalid` + ? i18n.str`Invalid` : undefined, }); @@ -168,7 +166,7 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { return value as TalerMerchantApi.Product; }, [value]); - const hasErrors = errors === undefined; + const hasErrors = errors !== undefined; useEffect(() => { onSubscribe(hasErrors ? undefined : submit); @@ -185,29 +183,29 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { <InputImage<NonInventoryProduct> name="image" label={i18n.str`Image`} - tooltip={i18n.str`photo of the product`} + tooltip={i18n.str`Photo of the product.`} /> <Input<NonInventoryProduct> name="description" inputType="multiline" label={i18n.str`Description`} - tooltip={i18n.str`full product description`} + tooltip={i18n.str`Full product description.`} /> <Input<NonInventoryProduct> name="unit" label={i18n.str`Unit`} - tooltip={i18n.str`name of the product unit`} + tooltip={i18n.str`Name of the product unit.`} /> <InputCurrency<NonInventoryProduct> name="price" label={i18n.str`Price`} - tooltip={i18n.str`amount in the current currency`} + tooltip={i18n.str`Amount in the current currency.`} /> <InputNumber<NonInventoryProduct> name="quantity" label={i18n.str`Quantity`} - tooltip={i18n.str`how many products will be added`} + tooltip={i18n.str`How many products will be added.`} /> <InputTaxes<NonInventoryProduct> name="taxes" label={i18n.str`Taxes`} /> diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx @@ -66,19 +66,19 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { }); const errors = undefinedIfEmpty({ - product_id: !value.product_id ? i18n.str`required` : undefined, - description: !value.description ? i18n.str`required` : undefined, - unit: !value.unit ? i18n.str`required` : undefined, + product_id: !value.product_id ? i18n.str`Required` : undefined, + description: !value.description ? i18n.str`Required` : undefined, + unit: !value.unit ? i18n.str`Required` : undefined, price: !value.price - ? i18n.str`required` + ? i18n.str`Required` : Amounts.parse(value.price) === undefined - ? i18n.str`invalid amount` + ? i18n.str`Invalid amount` : undefined, minimum_age: value.minimum_age === undefined ? undefined : value.minimum_age < 1 - ? i18n.str`should be greater than 0` + ? i18n.str`Must be greater than 0` : undefined, }); @@ -131,47 +131,47 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { name="product_id" addonBefore={new URL("product/", state.backendUrl.href).href} label={i18n.str`ID`} - tooltip={i18n.str`product identification to use in URLs (for internal use only)`} + tooltip={i18n.str`Product identification to use in URLs (for internal use only)`} /> )} <InputImage<Entity> name="image" label={i18n.str`Image`} - tooltip={i18n.str`illustration of the product for customers`} + tooltip={i18n.str`Illustration of the product for customers`} /> <Input<Entity> name="description" inputType="multiline" label={i18n.str`Description`} - tooltip={i18n.str`product description for customers`} + tooltip={i18n.str`Product description for customers`} /> <InputNumber<Entity> name="minimum_age" label={i18n.str`Age restriction`} - tooltip={i18n.str`is this product restricted for customer below certain age?`} - help={i18n.str`minimum age of the customer`} + tooltip={i18n.str`Is this product restricted for customer below certain age?`} + help={i18n.str`Minimum age of the customer`} /> <Input<Entity> name="unit" label={i18n.str`Unit name`} - tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`} - help={i18n.str`example: kg, items or liters`} + tooltip={i18n.str`Unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`} + help={i18n.str`Example: kg, items or liters`} /> <InputCurrency<Entity> name="price" label={i18n.str`Price per unit`} - tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`} + tooltip={i18n.str`Sale price for customers, including taxes, for above units of the product`} /> <InputStock name="stock" label={i18n.str`Stock`} alreadyExist={alreadyExist} - tooltip={i18n.str`inventory for products with finite supply (for internal use only)`} + tooltip={i18n.str`Inventory for products with finite supply (for internal use only)`} /> <InputTaxes<Entity> name="taxes" label={i18n.str`Taxes`} - tooltip={i18n.str`taxes included in the product price, exposed to customers`} + tooltip={i18n.str`Taxes included in the product price, exposed to customers`} /> </FormProvider> </div> diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx @@ -34,19 +34,19 @@ export function ProductList({ list, actions = [] }: Props): VNode { <thead> <tr> <th> - <i18n.Translate>image</i18n.Translate> + <i18n.Translate>Image</i18n.Translate> </th> <th> - <i18n.Translate>description</i18n.Translate> + <i18n.Translate>Description</i18n.Translate> </th> <th> - <i18n.Translate>quantity</i18n.Translate> + <i18n.Translate>Quantity</i18n.Translate> </th> <th> - <i18n.Translate>unit price</i18n.Translate> + <i18n.Translate>Unit price</i18n.Translate> </th> <th> - <i18n.Translate>total price</i18n.Translate> + <i18n.Translate>Total price</i18n.Translate> </th> <th /> </tr> diff --git a/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx b/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx @@ -58,13 +58,13 @@ export function TokenFamilyForm({ onSubscribe }: Props) { const { i18n } = useTranslationContext(); const errors: FormErrors<Entity> = { - slug: !value.slug ? i18n.str`required` : undefined, - name: !value.name ? i18n.str`required` : undefined, - description: !value.description ? i18n.str`required` : undefined, + slug: !value.slug ? i18n.str`Required` : undefined, + name: !value.name ? i18n.str`Required` : undefined, + description: !value.description ? i18n.str`Required` : undefined, valid_after: !value.valid_after ? undefined : undefined, - valid_before: !value.valid_before ? i18n.str`required` : undefined, - duration: !value.duration ? i18n.str`required` : undefined, - kind: !value.kind ? i18n.str`required` : undefined, + valid_before: !value.valid_before ? i18n.str`Required` : undefined, + duration: !value.duration ? i18n.str`Required` : undefined, + kind: !value.kind ? i18n.str`Required` : undefined, }; const hasErrors = Object.keys(errors).some( @@ -94,42 +94,42 @@ export function TokenFamilyForm({ onSubscribe }: Props) { name="slug" addonBefore={new URL("tokenfamily/", state.backendUrl.href).href} label={i18n.str`Slug`} - tooltip={i18n.str`token family slug to use in URLs (for internal use only)`} + tooltip={i18n.str`Token family slug to use in URLs (for internal use only)`} /> <InputSelector<Entity> name="kind" label={i18n.str`Kind`} - tooltip={i18n.str`token family kind`} + tooltip={i18n.str`Token family kind`} values={["discount", "subscription"]} /> <Input<Entity> name="name" inputType="text" label={i18n.str`Name`} - tooltip={i18n.str`user-readable token family name`} + tooltip={i18n.str`User-readable token family name`} /> <Input<Entity> name="description" inputType="multiline" label={i18n.str`Description`} - tooltip={i18n.str`token family description for customers`} + tooltip={i18n.str`Token family description for customers`} /> <InputDate<Entity> name="valid_after" label={i18n.str`Valid After`} - tooltip={i18n.str`token family can issue tokens after this date`} + tooltip={i18n.str`Token family can issue tokens after this date`} withTimestampSupport /> <InputDate<Entity> name="valid_before" label={i18n.str`Valid Before`} - tooltip={i18n.str`token family can issue tokens until this date`} + tooltip={i18n.str`Token family can issue tokens until this date`} withTimestampSupport /> <InputDuration<Entity> name="duration" label={i18n.str`Duration`} - tooltip={i18n.str`validity duration of a issued token`} + tooltip={i18n.str`Validity duration of a issued token`} withForever /> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/hooks/async.ts b/packages/merchant-backoffice-ui/src/hooks/async.ts @@ -25,7 +25,7 @@ export interface Options { } export interface AsyncOperationApi<T> { - request: (...a: any) => void; + request: (...a: unknown[]) => void; cancel: () => void; data: T | undefined; isSlow: boolean; @@ -34,15 +34,15 @@ export interface AsyncOperationApi<T> { } export function useAsync<T>( - fn?: (...args: any) => Promise<T>, + fn?: (...args: unknown[]) => Promise<T>, { slowTolerance: tooLong }: Options = { slowTolerance: 1000 }, ): AsyncOperationApi<T> { const [data, setData] = useState<T | undefined>(undefined); const [isLoading, setLoading] = useState<boolean>(false); - const [error, setError] = useState<any>(undefined); + const [error, setError] = useState<string>(); const [isSlow, setSlow] = useState(false); - const request = async (...args: any) => { + const request = async (...args: unknown[]) => { if (!fn) return; setLoading(true); @@ -54,7 +54,7 @@ export function useAsync<T>( const result = await fn(...args); setData(result); } catch (error) { - setError(error); + setError(error instanceof Error ? error.message : String(error)); } setLoading(false); setSlow(false); diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts @@ -28,6 +28,7 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; export interface Preferences { advanceOrderMode: boolean; advanceInstanceMode: boolean; + developerMode: boolean; hideKycUntil: AbsoluteTime; hideMissingAccountUntil: AbsoluteTime; dateFormat: "ymd" | "dmy" | "mdy"; @@ -36,6 +37,7 @@ export interface Preferences { const defaultSettings: Preferences = { advanceOrderMode: false, advanceInstanceMode: false, + developerMode: false, hideKycUntil: AbsoluteTime.never(), hideMissingAccountUntil: AbsoluteTime.never(), dateFormat: "ymd", @@ -45,6 +47,7 @@ export const codecForPreferences = (): Codec<Preferences> => buildCodecForObject<Preferences>() .property("advanceOrderMode", codecForBoolean()) .property("advanceInstanceMode", codecForBoolean()) + .property("developerMode", codecForBoolean()) .property("hideKycUntil", codecForAbsoluteTime) .property("hideMissingAccountUntil", codecForAbsoluteTime) .property( diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -38,13 +38,10 @@ import { usePreference } from "../../../hooks/preference.js"; import { INSTANCE_ID_REGEX } from "../../../utils/constants.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; -export type Entity = Omit< - Omit<TalerMerchantApi.InstanceConfigurationMessage, "default_pay_delay">, - "default_wire_transfer_delay" -> & { +export type Entity = TalerMerchantApi.InstanceConfigurationMessage & { auth_token?: string; - default_pay_delay: Duration; - default_wire_transfer_delay: Duration; + // default_pay_delay: Duration; + // default_wire_transfer_delay: Duration; }; interface Props { @@ -53,14 +50,16 @@ interface Props { forceId?: string; } +const twoHours = Duration.fromSpec({ hours: 2 }); +const twoDays = Duration.fromSpec({ days: 2 }); + function with_defaults(id?: string): Partial<Entity> { return { id, - // accounts: [], user_type: "business", use_stefan: true, - default_pay_delay: { d_ms: 2 * 60 * 60 * 1000 }, // two hours - default_wire_transfer_delay: { d_ms: 2 * 60 * 60 * 24 * 1000 }, // two days + default_pay_delay: Duration.toTalerProtocolDuration(twoHours), + default_wire_transfer_delay: Duration.toTalerProtocolDuration(twoDays), }; } @@ -74,48 +73,48 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { const errors: FormErrors<Entity> = { id: !value.id - ? i18n.str`required` + ? i18n.str`Required` : !INSTANCE_ID_REGEX.test(value.id) - ? i18n.str`is not valid` + ? i18n.str`Invalid` : undefined, - name: !value.name ? i18n.str`required` : undefined, + name: !value.name ? i18n.str`Required` : undefined, user_type: !value.user_type - ? i18n.str`required` + ? i18n.str`Required` : value.user_type !== "business" && value.user_type !== "individual" - ? i18n.str`should be business or individual` + ? i18n.str`Must be business or individual` : undefined, // accounts: // !value.accounts || !value.accounts.length - // ? i18n.str`required` + // ? i18n.str`Required` // : undefinedIfEmpty( // value.accounts.map((p) => { // return !PAYTO_REGEX.test(p.payto_uri) - // ? i18n.str`is not valid` + // ? i18n.str`Invalid` // : undefined; // }), // ), default_pay_delay: !value.default_pay_delay - ? i18n.str`required` - : !!value.default_wire_transfer_delay && - value.default_wire_transfer_delay.d_ms !== "forever" && - value.default_pay_delay.d_ms !== "forever" && - value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms - ? i18n.str`pay delay can't be greater than wire transfer delay` + ? i18n.str`Required` + : value.default_wire_transfer_delay !== undefined && + value.default_wire_transfer_delay.d_us !== "forever" && + value.default_pay_delay.d_us !== "forever" && + value.default_pay_delay.d_us > value.default_wire_transfer_delay.d_us + ? i18n.str`Pay delay can't be greater than wire transfer delay` : undefined, default_wire_transfer_delay: !value.default_wire_transfer_delay - ? i18n.str`required` + ? i18n.str`Required` : undefined, address: undefinedIfEmpty({ address_lines: value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` + ? i18n.str`Max 7 lines` : undefined, }), jurisdiction: undefinedIfEmpty({ address_lines: value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` + ? i18n.str`Max 7 lines` : undefined, }), }; @@ -136,20 +135,8 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { : { method: "token", token: createRFC8959AccessTokenPlain(newToken) }; if (!newValue.address) newValue.address = {}; if (!newValue.jurisdiction) newValue.jurisdiction = {}; - // remove above use conversion - // schema.validateSync(value, { abortEarly: false }) - newValue.default_pay_delay = Duration.toTalerProtocolDuration( - newValue.default_pay_delay!, - ) as any; - newValue.default_wire_transfer_delay = Duration.toTalerProtocolDuration( - newValue.default_wire_transfer_delay!, - ) as any; - // delete value.default_pay_delay; - // delete value.default_wire_transfer_delay; - return onCreate( - newValue as any as TalerMerchantApi.InstanceConfigurationMessage, - ); + return onCreate(newValue as TalerMerchantApi.InstanceConfigurationMessage); }; function updateToken(token: string | null) { @@ -241,7 +228,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { ? "button has-tooltip-bottom" : "button is-info has-tooltip-bottom" } - data-tooltip={i18n.str`change authorization configuration`} + data-tooltip={i18n.str`Change authorization configuration`} onClick={() => updateIsTokenDialogActive(true)} > <div class="icon is-centered"> @@ -291,7 +278,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields and choose authorization method` - : "confirm operation" + : i18n.str`Confirm operation` } > <i18n.Translate>Confirm</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx @@ -18,9 +18,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../components/menu/index.js"; @@ -47,9 +45,7 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode { <CreatePage onBack={onBack} forceId={forceId} - onCreate={async ( - d: TalerMerchantApi.InstanceConfigurationMessage, - ) => { + onCreate={async (d: TalerMerchantApi.InstanceConfigurationMessage) => { if (state.status !== "loggedIn") return; try { const resp = await lib.instance.createInstance(state.token, d); @@ -79,16 +75,12 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode { } } onConfirm(); - } catch (ex) { - if (ex instanceof Error) { - setNotif({ - message: i18n.str`Failed to create instance`, - type: "ERROR", - description: ex.message, - }); - } else { - console.error(ex); - } + } catch (error) { + setNotif({ + message: i18n.str`Failed to create instance`, + type: "ERROR", + description: error instanceof Error ? error.message : String(error), + }); } }} /> diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx @@ -97,7 +97,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new instance`} + data-tooltip={i18n.str`Add new instance`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> 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 @@ -21,16 +21,15 @@ import { HttpStatusCode, - PaytoString, PaytoUri, TalerError, TalerMerchantApi, TranslatedString, assertUnreachable, - parsePaytoUri, + parsePaytoUri } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -40,6 +39,7 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; import { CompareAccountsModal, ImportingAccountModal, @@ -47,7 +47,6 @@ import { import { undefinedIfEmpty } from "../../../../utils/table.js"; import { safeConvertURL } from "../update/UpdatePage.js"; import { testRevenueAPI } from "./index.js"; -import { InputToggle } from "../../../../components/form/InputToggle.js"; type Entity = TalerMerchantApi.AccountAddDetails & { verified?: boolean }; @@ -73,7 +72,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { undefined, ); const errors: FormErrors<Entity> = { - payto_uri: !state.payto_uri ? i18n.str`required` : undefined, + payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, credit_facade_credentials: !state.credit_facade_credentials ? undefined @@ -81,12 +80,12 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { username: state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username - ? i18n.str`required` + ? i18n.str`Required` : undefined, password: state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password - ? i18n.str`required` + ? i18n.str`Required` : undefined, }), credit_facade_url: !state.credit_facade_url @@ -94,11 +93,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : !facadeURL ? i18n.str`Invalid url` : !facadeURL.href.endsWith("/") - ? i18n.str`URL should end with a '/'` + ? i18n.str`URL must end with a '/'` : facadeURL.searchParams.size > 0 - ? i18n.str`URL should not contain params` + ? i18n.str`URL must not contain params` : facadeURL.hash - ? i18n.str`URL should not hash param` + ? i18n.str`URL must not hash param` : undefined, }; @@ -223,7 +222,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <Input<Entity> name="credit_facade_url" label={i18n.str`Endpoint URL`} - help="https://bank.demo.taler.net/accounts/_username_/taler-revenue/" + 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`} /> @@ -273,7 +272,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={i18n.str`Compare info from server with account form`} disabled={!state.credit_facade_url} onClick={async () => { - const result = await testAccountInfo(); + await testAccountInfo(); }} > <i18n.Translate>Test</i18n.Translate> @@ -302,7 +301,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > 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 @@ -64,7 +64,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { .then((created) => { if (created.type === "fail") { setNotif({ - message: i18n.str`could not create account`, + message: i18n.str`Could not create account`, type: "ERROR", description: created.detail.hint, }); @@ -72,11 +72,11 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { } onConfirm(); }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not create account`, + message: i18n.str`Could not create account`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx @@ -64,7 +64,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new accounts`} + data-tooltip={i18n.str`Add new account`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -133,7 +133,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { {bitcoinAccounts.length > 0 && ( <div class="table-container"> <p class="card-header-title"> - <i18n.Translate>Bitcoin type accounts</i18n.Translate> + <i18n.Translate>Wire method: Bitcoin</i18n.Translate> </p> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -177,10 +177,10 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> </div> </td> @@ -195,7 +195,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { {talerbankAccounts.length > 0 && ( <div class="table-container"> <p class="card-header-title"> - <i18n.Translate>Taler type accounts</i18n.Translate> + <i18n.Translate>Wire method: x-taler-bank</i18n.Translate> </p> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -230,10 +230,10 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> </div> </td> @@ -248,7 +248,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { {ibanAccounts.length > 0 && ( <div class="table-container"> <p class="card-header-title"> - <i18n.Translate>IBAN type accounts</i18n.Translate> + <i18n.Translate>Wire method: IBAN</i18n.Translate> </p> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -283,10 +283,10 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> </div> </td> @@ -301,7 +301,7 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { {unkownAccounts.length > 0 && ( <div class="table-container"> <p class="card-header-title"> - <i18n.Translate>Other type accounts</i18n.Translate> + <i18n.Translate>Other accounts</i18n.Translate> </p> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> @@ -336,10 +336,10 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected accounts from the database`} + data-tooltip={i18n.str`Delete selected accounts from the database`} onClick={() => onDelete(acc)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> </div> </td> 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 @@ -95,22 +95,22 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`bank account delete successfully`, + message: i18n.str`Bank account delete successfully`, type: "SUCCESS", }); } else { setNotif({ - message: i18n.str`could not delete the bank account`, + message: i18n.str`Could not delete the bank account`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => + .catch((error: unknown) => setNotif({ - message: i18n.str`could not delete the bank account`, + message: i18n.str`Could not delete the bank account`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ); }} 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 @@ -95,19 +95,19 @@ export function UpdatePage({ const facadeURL = safeConvertURL(state.credit_facade_url); - const errors: FormErrors<FormType> = { - payto_uri: !state.payto_uri ? i18n.str`required` : undefined, + const errors = undefinedIfEmpty<FormErrors<FormType>>({ + payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, credit_facade_url: !state.credit_facade_url ? undefined : !facadeURL ? i18n.str`Invalid url` : !facadeURL.href.endsWith("/") - ? i18n.str`URL should end with a '/'` + ? i18n.str`URL must end with a '/'` : facadeURL.searchParams.size > 0 - ? i18n.str`URL should not contain params` + ? i18n.str`URL must not contain params` : facadeURL.hash - ? i18n.str`URL should not hash param` + ? i18n.str`URL must not hash param` : undefined, credit_facade_credentials: !state.credit_facade_credentials ? undefined @@ -116,27 +116,25 @@ export function UpdatePage({ replacingAccountId && // @ts-expect-error unedit is not in facade creds state.credit_facade_credentials?.type === "unedit" - ? i18n.str`required` + ? i18n.str`Required` : undefined, username: state.credit_facade_credentials?.type !== "basic" ? undefined : !state.credit_facade_credentials.username - ? i18n.str`required` + ? i18n.str`Required` : undefined, password: state.credit_facade_credentials?.type !== "basic" ? undefined : !state.credit_facade_credentials.password - ? i18n.str`required` + ? i18n.str`Required` : undefined, }), - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); @@ -281,7 +279,7 @@ export function UpdatePage({ <Input<Entity> name="credit_facade_url" label={i18n.str`Endpoint URL`} - help="https://bank.demo.taler.net/accounts/_username_/taler-revenue/" + 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`} /> @@ -352,7 +350,7 @@ export function UpdatePage({ data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx @@ -87,7 +87,7 @@ export default function UpdateValidator({ .then((updated) => { if (updated.type === "fail") { setNotif({ - message: i18n.str`could not update account`, + message: i18n.str`Could not update account`, type: "ERROR", description: updated.detail.hint, }); @@ -95,11 +95,11 @@ export default function UpdateValidator({ } onConfirm(); }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not update account`, + message: i18n.str`Could not update account`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} @@ -111,17 +111,17 @@ export default function UpdateValidator({ ); if (created.type === "fail") { setNotif({ - message: i18n.str`could not create account`, + message: i18n.str`Could not create account`, type: "ERROR", description: created.detail.hint, }); return; } - } catch (error: any) { + } catch (error) { setNotif({ - message: i18n.str`could not create account`, + message: i18n.str`Could not create account`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); return; } @@ -132,17 +132,17 @@ export default function UpdateValidator({ ); if (deleted.type === "fail") { setNotif({ - message: i18n.str`could not delete account`, + message: i18n.str`Could not delete account`, type: "ERROR", description: deleted.detail.hint, }); return; } - } catch (error: any) { + } catch (error) { setNotif({ - message: i18n.str`could not delete account`, + message: i18n.str`Could not delete account`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); return; } 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 @@ -147,15 +147,15 @@ export function CreatePage({ ? undefined : Amounts.parse(value.pricing.order_price); - const errors: FormErrors<Entity> = { + const errors = undefinedIfEmpty<FormErrors<Entity>>({ pricing: undefinedIfEmpty({ - summary: !value.pricing?.summary ? i18n.str`required` : undefined, + summary: !value.pricing?.summary ? i18n.str`Required` : undefined, order_price: !value.pricing?.order_price - ? i18n.str`required` + ? i18n.str`Required` : !parsedPrice - ? i18n.str`not valid` + ? i18n.str`Invalid` : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` + ? i18n.str`Must be greater than 0` : undefined, }), payments: undefinedIfEmpty({ @@ -166,58 +166,59 @@ export function CreatePage({ value.payments.refund_deadline, value.payments.pay_deadline, ) === -1 - ? i18n.str`refund deadline cannot be before pay deadline` + ? i18n.str`Refund deadline can't be before pay deadline` : value.payments.wire_transfer_deadline && Duration.cmp( value.payments.wire_transfer_deadline, value.payments.refund_deadline, ) === -1 - ? i18n.str`wire transfer deadline cannot be before refund deadline` + ? i18n.str`Wire transfer deadline can't be before refund deadline` : undefined, pay_deadline: !value.payments?.pay_deadline - ? i18n.str`required` + ? i18n.str`Required` : value.payments.wire_transfer_deadline && Duration.cmp( value.payments.wire_transfer_deadline, value.payments.pay_deadline, ) === -1 - ? i18n.str`wire transfer deadline cannot be before pay deadline` + ? i18n.str`Wire transfer deadline can't be before pay deadline` : undefined, wire_transfer_deadline: !value.payments?.wire_transfer_deadline - ? i18n.str`required` + ? i18n.str`Required` : undefined, auto_refund_deadline: !value.payments?.auto_refund_deadline ? undefined : !value.payments?.refund_deadline - ? i18n.str`should have a refund deadline` + ? i18n.str`Must have a refund deadline` : Duration.cmp( value.payments.refund_deadline, value.payments.auto_refund_deadline, ) == -1 - ? i18n.str`auto refund cannot be after refund deadline` + ? i18n.str`Auto refund can't be after refund deadline` : undefined, }), shipping: undefinedIfEmpty({ delivery_date: !value.shipping?.delivery_date ? undefined : !isFuture(value.shipping.delivery_date) - ? i18n.str`should be in the future` + ? i18n.str`Must be in the future` : undefined, }), - }; - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + }); + const hasErrors = errors !== undefined; const submit = (): void => { - const order = value as any; //schema.cast(value); + const order = value; + const price = order.pricing?.order_price as AmountString | undefined; + const summary = order.pricing?.summary; if (!value.payments) return; if (!value.shipping) return; + if (!price || ! summary) return; const request: TalerMerchantApi.PostOrderRequest = { order: { - amount: order.pricing.order_price, - summary: order.pricing.summary, + amount: price, + summary: summary, products: productList, extra: undefinedIfEmpty(value.extra), pay_deadline: AbsoluteTime.toProtocolTimestamp( @@ -453,7 +454,7 @@ export function CreatePage({ name="pricing.products_price" label={i18n.str`Total price`} readonly - tooltip={i18n.str`total product price added up`} + tooltip={i18n.str`Total product price added up`} /> <InputCurrency name="pricing.order_price" @@ -473,7 +474,7 @@ export function CreatePage({ <InputCurrency name="pricing.order_price" label={i18n.str`Order price`} - tooltip={i18n.str`final order price`} + tooltip={i18n.str`Final order price`} /> )} @@ -499,7 +500,7 @@ export function CreatePage({ <InputGroup name="shipping.delivery_location" label={i18n.str`Location`} - tooltip={i18n.str`address where the products will be delivered`} + tooltip={i18n.str`Address where the products will be delivered`} > <InputLocation name="shipping.delivery_location" /> </InputGroup> @@ -544,7 +545,7 @@ export function CreatePage({ valueHandler(c); }} > - <i18n.Translate>default</i18n.Translate> + <i18n.Translate>Default</i18n.Translate> </button> </span> } @@ -577,7 +578,7 @@ export function CreatePage({ }); }} > - <i18n.Translate>default</i18n.Translate> + <i18n.Translate>Default</i18n.Translate> </button> </span> } @@ -611,7 +612,7 @@ export function CreatePage({ }); }} > - <i18n.Translate>default</i18n.Translate> + <i18n.Translate>Default</i18n.Translate> </button> </span> } @@ -733,7 +734,7 @@ export function CreatePage({ e.preventDefault(); }} > - <i18n.Translate>add</i18n.Translate> + <i18n.Translate>Add</i18n.Translate> </button> </div> </InputGroup> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -103,7 +103,7 @@ export default function OrderCreate({ onConfirm, onBack }: Props): VNode { return onConfirm(r.body.order_id); } else { setNotif({ - message: "could not create order", + message: i18n.str`Could not create order`, type: "ERROR", description: r.case === HttpStatusCode.Gone @@ -112,11 +112,11 @@ export default function OrderCreate({ onConfirm, onBack }: Props): VNode { }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: "could not create order", + message: i18n.str`Could not create order`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -75,13 +75,13 @@ function ContractTerms({ value }: { value: CT }) { readonly name="summary" label={i18n.str`Summary`} - tooltip={i18n.str`human-readable description of the whole purchase`} + tooltip={i18n.str`Human-readable description of the whole purchase`} /> <InputCurrency<CT> readonly name="amount" label={i18n.str`Amount`} - tooltip={i18n.str`total price for the transaction`} + tooltip={i18n.str`Total price for the transaction`} /> {value.fulfillment_url && ( <Input<CT> @@ -95,43 +95,43 @@ function ContractTerms({ value }: { value: CT }) { readonly name="max_fee" label={i18n.str`Max fee`} - tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`} + tooltip={i18n.str`Maximum total deposit fee accepted by the merchant for this contract`} /> <InputDate<CT> readonly name="timestamp" label={i18n.str`Created at`} - tooltip={i18n.str`time when this contract was generated`} + tooltip={i18n.str`Time when this contract was generated`} /> <InputDate<CT> readonly name="refund_deadline" label={i18n.str`Refund deadline`} - tooltip={i18n.str`after this deadline has passed no refunds will be accepted`} + tooltip={i18n.str`After this deadline has passed no refunds will be accepted`} /> <InputDate<CT> readonly name="pay_deadline" label={i18n.str`Payment deadline`} - tooltip={i18n.str`after this deadline, the merchant won't accept payments for the contract`} + tooltip={i18n.str`After this deadline, the merchant won't accept payments for the contract`} /> <InputDate<CT> readonly name="wire_transfer_deadline" label={i18n.str`Wire transfer deadline`} - tooltip={i18n.str`transfer deadline for the exchange`} + tooltip={i18n.str`Transfer deadline for the exchange`} /> <InputDate<CT> readonly name="delivery_date" label={i18n.str`Delivery date`} - tooltip={i18n.str`time indicating when the order should be delivered`} + tooltip={i18n.str`Time indicating when the order should be delivered`} /> {value.delivery_date && ( <InputGroup name="delivery_location" label={i18n.str`Location`} - tooltip={i18n.str`where the order will be delivered`} + tooltip={i18n.str`Where the order will be delivered`} > <InputLocation name="payments.delivery_location" /> </InputGroup> @@ -140,13 +140,13 @@ function ContractTerms({ value }: { value: CT }) { readonly name="auto_refund" label={i18n.str`Auto-refund delay`} - tooltip={i18n.str`how long the wallet should try to get an automatic refund for the purchase`} + tooltip={i18n.str`How long the wallet should try to get an automatic refund for the purchase`} /> <Input<CT> readonly name="extra" label={i18n.str`Extra info`} - tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`} + tooltip={i18n.str`Extra data that is only interpreted by the merchant frontend`} /> </FormProvider> </InputGroup> @@ -221,7 +221,7 @@ function ClaimedPage({ <div class="level-item"> <i18n.Translate>Order</i18n.Translate> #{id} <div class="tag is-info ml-4"> - <i18n.Translate>claimed</i18n.Translate> + <i18n.Translate>Claimed</i18n.Translate> </div> </div> </div> @@ -248,7 +248,7 @@ function ClaimedPage({ > <p> <b> - <i18n.Translate>claimed at</i18n.Translate>: + <i18n.Translate>Claimed at</i18n.Translate>: </b>{" "} {order.contract_terms.timestamp.t_s === "never" ? "never" @@ -458,16 +458,16 @@ function PaidPage({ <div class="level-item"> <i18n.Translate>Order</i18n.Translate> #{id} <div class="tag is-success ml-4"> - <i18n.Translate>paid</i18n.Translate> + <i18n.Translate>Paid</i18n.Translate> </div> {order.wired ? ( <div class="tag is-success ml-4"> - <i18n.Translate>wired</i18n.Translate> + <i18n.Translate>Wired</i18n.Translate> </div> ) : null} {order.refunded ? ( <div class="tag is-danger ml-4"> - <i18n.Translate>refunded</i18n.Translate> + <i18n.Translate>Refunded</i18n.Translate> </div> ) : null} </div> @@ -487,8 +487,8 @@ function PaidPage({ class="has-tooltip-left" data-tooltip={ refundable - ? i18n.str`refund order` - : i18n.str`not refundable` + ? i18n.str`Refund order` + : i18n.str`Not refundable` } > <button @@ -496,7 +496,7 @@ function PaidPage({ disabled={!refundable} onClick={() => onRefund(id)} > - <i18n.Translate>refund</i18n.Translate> + <i18n.Translate>Refund</i18n.Translate> </button> </span> </div> @@ -638,7 +638,7 @@ function UnpaidPage({ </h1> </div> <div class="tag is-dark"> - <i18n.Translate>unpaid</i18n.Translate> + <i18n.Translate>Unpaid</i18n.Translate> </div> </div> </div> @@ -656,7 +656,7 @@ function UnpaidPage({ > <p> <b> - <i18n.Translate>pay at</i18n.Translate>: + <i18n.Translate>Pay at</i18n.Translate>: </b>{" "} <a href={order.order_status_url} @@ -668,7 +668,7 @@ function UnpaidPage({ </p> <p> <b> - <i18n.Translate>created at</i18n.Translate>: + <i18n.Translate>Created at</i18n.Translate>: </b>{" "} {order.creation_time.t_s === "never" ? "never" @@ -693,13 +693,13 @@ function UnpaidPage({ readonly name="summary" label={i18n.str`Summary`} - tooltip={i18n.str`human-readable description of the whole purchase`} + tooltip={i18n.str`Human-readable description of the whole purchase`} /> <InputCurrency<Unpaid> readonly name="total_amount" label={i18n.str`Amount`} - tooltip={i18n.str`total price for the transaction`} + tooltip={i18n.str`Total price for the transaction`} /> <Input<Unpaid> name="order_status" diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -85,22 +85,22 @@ export default function Update({ oid, onBack }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`refund created successfully`, + message: i18n.str`Refund created successfully`, type: "SUCCESS", }); } else { setNotif({ - message: i18n.str`could not create the refund`, + message: i18n.str`Could not create the refund`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => + .catch((error: unknown) => setNotif({ - message: i18n.str`could not create the refund`, + message: i18n.str`Could not create the refund`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx @@ -80,7 +80,7 @@ export function ListPage({ isWiredActive, }: ListPageProps): VNode { const { i18n } = useTranslationContext(); - const dateTooltip = i18n.str`select date to show nearby orders`; + const dateTooltip = i18n.str`Select date to show nearby orders`; const [pickDate, setPickDate] = useState(false); const [settings] = usePreference(); @@ -93,7 +93,7 @@ export function ListPage({ <li class={isNotPaidActive}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show paid orders`} + data-tooltip={i18n.str`Only show paid orders`} > <a onClick={onShowNotPaid}> <i18n.Translate>New</i18n.Translate> @@ -103,7 +103,7 @@ export function ListPage({ <li class={isPaidActive}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show paid orders`} + data-tooltip={i18n.str`Only show paid orders`} > <a onClick={onShowPaid}> <i18n.Translate>Paid</i18n.Translate> @@ -113,7 +113,7 @@ export function ListPage({ <li class={isRefundedActive}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show orders with refunds`} + data-tooltip={i18n.str`Only show orders with refunds`} > <a onClick={onShowRefunded}> <i18n.Translate>Refunded</i18n.Translate> @@ -123,7 +123,7 @@ export function ListPage({ <li class={isNotWiredActive}> <div class="has-tooltip-left" - data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`} + data-tooltip={i18n.str`Only show orders where customers paid, but wire payments from payment provider are still pending`} > <a onClick={onShowNotWired}> <i18n.Translate>Not wired</i18n.Translate> @@ -133,7 +133,7 @@ export function ListPage({ <li class={isWiredActive}> <div class="has-tooltip-left" - data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`} + data-tooltip={i18n.str`Only show orders where customers paid, but wire payments from payment provider are still pending`} > <a onClick={onShowWired}> <i18n.Translate>Completed</i18n.Translate> @@ -143,7 +143,7 @@ export function ListPage({ <li class={isAllActive}> <div class="has-tooltip-right" - data-tooltip={i18n.str`remove all filters`} + data-tooltip={i18n.str`Remove all filters`} > <a onClick={onShowAll}> <i18n.Translate>All</i18n.Translate> @@ -161,7 +161,7 @@ export function ListPage({ <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}> <span class="icon" - data-tooltip={i18n.str`clear date filter`} + data-tooltip={i18n.str`Clear date filter`} > <i class="mdi mdi-close" /> </span> @@ -175,7 +175,7 @@ export function ListPage({ type="text" readonly value={!jumpToDate || jumpToDate.t_ms === "never" ? "" : format(jumpToDate.t_ms, dateFormatForSettings(settings))} - placeholder={i18n.str`date (${dateFormatForSettings(settings)})`} + placeholder={i18n.str`Jump to date (${dateFormatForSettings(settings)})`} onClick={() => { setPickDate(true); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -80,7 +80,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options" /> <div class="card-header-icon" aria-label="more options"> - <span class="has-tooltip-left" data-tooltip={i18n.str`create order`}> + <span class="has-tooltip-left" data-tooltip={i18n.str`Create order`}> <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> <i class="mdi mdi-plus mdi-36px" /> @@ -137,7 +137,7 @@ function Table({ <div class="table-container"> {onLoadMoreBefore && ( <button class="button is-fullwidth" onClick={onLoadMoreBefore}> - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-striped is-hoverable is-fullwidth"> @@ -211,9 +211,9 @@ function Table({ </table> {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more orders after the last one`} + data-tooltip={i18n.str`Load more orders after the last one`} onClick={onLoadMoreAfter}> - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> @@ -277,20 +277,20 @@ export function RefundModal({ : orderPrice; const isRefundable = Amounts.isNonZero(totalRefundable); - const duplicatedText = i18n.str`duplicated`; + const duplicatedText = i18n.str`Duplicated`; const errors: FormErrors<State> = { - mainReason: !form.mainReason ? i18n.str`required` : undefined, + mainReason: !form.mainReason ? i18n.str`Required` : undefined, description: !form.description && form.mainReason !== duplicatedText - ? i18n.str`required` + ? i18n.str`Required` : undefined, refund: !form.refund - ? i18n.str`required` + ? i18n.str`Required` : !Amounts.parse(form.refund) - ? i18n.str`invalid format` + ? i18n.str`Invalid` : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1 - ? i18n.str`this value exceed the refundable amount` + ? i18n.str`This value exceed the refundable amount` : undefined, }; const hasErrors = Object.keys(errors).some( @@ -335,13 +335,13 @@ export function RefundModal({ <thead> <tr> <th> - <i18n.Translate>date</i18n.Translate> + <i18n.Translate>Date</i18n.Translate> </th> <th> - <i18n.Translate>amount</i18n.Translate> + <i18n.Translate>Amount</i18n.Translate> </th> <th> - <i18n.Translate>reason</i18n.Translate> + <i18n.Translate>Reason</i18n.Translate> </th> </tr> </thead> @@ -378,7 +378,7 @@ export function RefundModal({ <InputCurrency<State> name="refund" label={i18n.str`Refund`} - tooltip={i18n.str`amount to be refunded`} + tooltip={i18n.str`Amount to be refunded`} > <i18n.Translate>Max refundable:</i18n.Translate>{" "} {Amounts.stringify(totalRefundable)} @@ -389,16 +389,16 @@ export function RefundModal({ values={[ i18n.str`Choose one...`, duplicatedText, - i18n.str`requested by the customer`, - i18n.str`other`, + i18n.str`Requested by the customer`, + i18n.str`Other`, ]} - tooltip={i18n.str`why this order is being refunded`} + tooltip={i18n.str`Why this order is being refunded`} /> {form.mainReason && form.mainReason !== duplicatedText ? ( <Input<State> label={i18n.str`Description`} name="description" - tooltip={i18n.str`more information to give context`} + tooltip={i18n.str`More information to give context`} /> ) : undefined} </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -110,8 +110,8 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { return resp.type === "ok"; }} onSelect={onSelect} - description={i18n.str`jump to order with the given product ID`} - placeholder={i18n.str`order id`} + description={i18n.str`Jump to order with the given product ID`} + placeholder={i18n.str`Order id`} /> <ListPage @@ -160,22 +160,22 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`refund created successfully`, + message: i18n.str`Refund created successfully`, type: "SUCCESS", }); } else { setNotif({ - message: i18n.str`could not create the refund`, + message: i18n.str`Could not create the refund`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => + .catch((error: unknown) => setNotif({ - message: i18n.str`could not create the refund`, + message: i18n.str`Could not create the refund`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ) .then(() => setOrderToBeRefunded(undefined)); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx @@ -35,6 +35,7 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = TalerMerchantApi.OtpDeviceAddDetails; @@ -53,34 +54,32 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const [showKey, setShowKey] = useState(false); - const errors: FormErrors<Entity> = { + const errors = undefinedIfEmpty<FormErrors<Entity>>({ otp_device_id: !state.otp_device_id - ? i18n.str`required` + ? i18n.str`Required` : !/[a-zA-Z0-9]*/.test(state.otp_device_id) - ? i18n.str`no valid. only characters and numbers` + ? i18n.str`Invalid. Only characters and numbers` : undefined, - otp_algorithm: !state.otp_algorithm ? i18n.str`required` : undefined, + otp_algorithm: !state.otp_algorithm ? i18n.str`Required` : undefined, otp_key: !state.otp_key - ? i18n.str`required` + ? i18n.str`Required` : !isRfc3548Base32Charset(state.otp_key) - ? i18n.str`just letters and numbers from 2 to 7` + ? i18n.str`Just letters and numbers from 2 to 7` : state.otp_key.length !== 32 - ? i18n.str`size of the key should be 32` + ? i18n.str`Size of the key must be 32` : undefined, otp_device_description: !state.otp_device_description - ? i18n.str`required` + ? i18n.str`Required` : !/[a-zA-Z0-9]*/.test(state.otp_device_description) - ? i18n.str`no valid. only characters and numbers` + ? i18n.str`Invalid. Only characters and numbers` : undefined, - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); - return onCreate(state as any); + return onCreate(state as TalerMerchantApi.OtpDeviceAddDetails); }; return ( @@ -112,14 +111,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { toStr={(v) => algorithmsNames[v]} fromStr={(v) => Number(v)} /> - {state.otp_algorithm ? ( + {state.otp_algorithm ? ( <Fragment> <InputWithAddon<Entity> expand name="otp_key" label={i18n.str`Device key`} inputType={showKey ? "text" : "password"} - help="Be sure to be very hard to guess or use the random generator" + help={i18n.str`Be sure to be very hard to guess or use the random generator`} tooltip={i18n.str`Your device need to have exactly the same value`} fromStr={(v) => v.toUpperCase()} addonAfterAction={() => { @@ -136,7 +135,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { } side={ <button - data-tooltip={i18n.str`generate random secret key`} + data-tooltip={i18n.str`Generate random secret key`} class="button is-info mr-3" onClick={(e) => { setState((s) => ({ @@ -146,7 +145,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { e.preventDefault(); }} > - <i18n.Translate>random</i18n.Translate> + <i18n.Translate>Random</i18n.Translate> </button> } /> @@ -165,7 +164,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx @@ -57,23 +57,23 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`device added successfully`, + message: i18n.str`Device added successfully`, type: "SUCCESS", }); setCreated(request); } else { setNotif({ - message: i18n.str`could not add device`, + message: i18n.str`Could not add device`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not add device`, + message: i18n.str`Could not add device`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx @@ -59,7 +59,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new devices`} + data-tooltip={i18n.str`Add new devices`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -114,10 +114,10 @@ function Table({ {onLoadMoreBefore && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more devices before the first one`} + data-tooltip={i18n.str`Load more devices before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -152,7 +152,7 @@ function Table({ <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected devices from the database`} + data-tooltip={i18n.str`Delete selected devices from the database`} onClick={() => onDelete(i)} > Delete @@ -167,10 +167,10 @@ function Table({ {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more devices after the last one`} + data-tooltip={i18n.str`Load more devices after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx @@ -87,22 +87,22 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`device delete successfully`, + message: i18n.str`Device delete successfully`, type: "SUCCESS", }); } else { setNotif({ - message: i18n.str`could not delete the device`, + message: i18n.str`Could not delete the device`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => + .catch((error: unknown) => setNotif({ - message: i18n.str`could not delete the device`, + message: i18n.str`Could not delete the device`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx @@ -19,15 +19,15 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { randomRfc3548Base32Key, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + randomRfc3548Base32Key, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; @@ -48,15 +48,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>(device); const [showKey, setShowKey] = useState(false); - const errors: FormErrors<Entity> = {}; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onUpdate(state as any); + return onUpdate(state as Entity); }; return ( @@ -68,7 +61,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - Device: <b>{device.id}</b> + <i18n.Translate>Device:</i18n.Translate> + <b>{device.id}</b> </span> </div> </div> @@ -80,11 +74,7 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { <section class="section is-main-section"> <div class="columns"> <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > + <FormProvider object={state} valueHandler={setState}> <Input<Entity> name="otp_device_description" label={i18n.str`Description`} @@ -107,8 +97,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { inputType={showKey ? "text" : "password"} help={ state.otp_key === undefined - ? "Not modified" - : "Be sure to be very hard to guess or use the random generator" + ? i18n.str`Not modified` + : i18n.str`Be sure to be very hard to guess or use the random generator` } tooltip={i18n.str`Your device need to have exactly the same value`} fromStr={(v) => v.toUpperCase()} @@ -132,25 +122,25 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { side={ state.otp_key === undefined ? ( <button - onClick={(e) => { + onClick={() => { setState((s) => ({ ...s, otp_key: "" })); }} class="button" > - change key + <i18n.Translate>Change key</i18n.Translate> </button> ) : ( <button - data-tooltip={i18n.str`generate random secret key`} + data-tooltip={i18n.str`Generate random secret key`} class="button is-info mr-3" - onClick={(e) => { + onClick={() => { setState((s) => ({ ...s, otp_key: randomRfc3548Base32Key(), })); }} > - <i18n.Translate>random</i18n.Translate> + <i18n.Translate>Random</i18n.Translate> </button> ) } @@ -166,12 +156,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { </button> )} <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } + disabled={false} + data-tooltip={i18n.str`Confirm operation`} onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx @@ -133,11 +133,11 @@ export default function UpdateValidator({ } } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not update template`, + message: i18n.str`Could not update template`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx @@ -49,23 +49,23 @@ export default function CreateProduct({ onConfirm, onBack }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`product created successfully`, + message: i18n.str`Product created successfully`, type: "SUCCESS", }); onConfirm(); } else { setNotif({ - message: i18n.str`could not create product`, + message: i18n.str`Could not create product`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not create product`, + message: i18n.str`Could not create product`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} 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 @@ -75,7 +75,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add product to inventory`} + data-tooltip={i18n.str`Add product to inventory`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -138,7 +138,7 @@ function Table({ <div class="table-container"> {onLoadMoreBefore && ( <button class="button is-fullwidth" onClick={onLoadMoreBefore}> - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -227,7 +227,7 @@ function Table({ } style={{ cursor: "pointer" }} > - {isFree ? i18n.str`free` : `${i.price} / ${i.unit}`} + {isFree ? i18n.str`Free` : `${i.price} / ${i.unit}`} </td> <td onClick={() => @@ -268,7 +268,7 @@ function Table({ <div class="buttons is-right"> <span class="has-tooltip-bottom" - data-tooltip={i18n.str`go to product update page`} + data-tooltip={i18n.str`Go to product update page`} > <button class="button is-small is-success " @@ -280,7 +280,7 @@ function Table({ </span> <span class="has-tooltip-left" - data-tooltip={i18n.str`remove this product from the database`} + data-tooltip={i18n.str`Remove this product from the database`} > <button class="button is-small is-danger" @@ -315,9 +315,9 @@ function Table({ </table> {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more products after the last one`} + data-tooltip={i18n.str`Load more products after the last one`} onClick={onLoadMoreAfter}> - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> @@ -358,7 +358,7 @@ function FastProductWithInfiniteStockUpdateForm({ <InputCurrency<FastProductUpdate> name="price" label={i18n.str`Price`} - tooltip={i18n.str`update the product with new price`} + tooltip={i18n.str`Update the product with new price`} /> </FormProvider> @@ -370,7 +370,7 @@ function FastProductWithInfiniteStockUpdateForm({ </button> <span class="has-tooltip-left" - data-tooltip={i18n.str`update product with new price`} + data-tooltip={i18n.str`Update product with new price`} > <button class="button is-info" @@ -407,7 +407,7 @@ function FastProductWithManagedStockUpdateForm({ const errors: FormErrors<FastProductUpdate> = { lost: currentStock + value.incoming < value.lost - ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming + ? `lost can't be greater that current + incoming (max ${currentStock + value.incoming })` : undefined, }; @@ -428,17 +428,17 @@ function FastProductWithManagedStockUpdateForm({ <InputNumber<FastProductUpdate> name="incoming" label={i18n.str`Incoming`} - tooltip={i18n.str`add more elements to the inventory`} + tooltip={i18n.str`Add more elements to the inventory`} /> <InputNumber<FastProductUpdate> name="lost" label={i18n.str`Lost`} - tooltip={i18n.str`report elements lost in the inventory`} + tooltip={i18n.str`Report elements lost in the inventory`} /> <InputCurrency<FastProductUpdate> name="price" label={i18n.str`Price`} - tooltip={i18n.str`new price for the product`} + tooltip={i18n.str`New price for the product`} /> </FormProvider> @@ -450,8 +450,8 @@ function FastProductWithManagedStockUpdateForm({ class="has-tooltip-left" data-tooltip={ hasErrors - ? i18n.str`the are value with errors` - : i18n.str`update product with new stock and price` + ? i18n.str`The are value with errors` + : i18n.str`Update product with new stock and price` } > <button diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -83,8 +83,8 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { return resp.type === "ok"; }} onSelect={onSelect} - description={i18n.str`jump to product with the given product ID`} - placeholder={i18n.str`product id`} + description={i18n.str`Jump to product with the given product ID`} + placeholder={i18n.str`Product id`} /> <CardTable @@ -101,19 +101,19 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { ); if (resp.type === "ok") { setNotif({ - message: i18n.str`product updated successfully`, + message: i18n.str`Product updated successfully`, type: "SUCCESS", }); } else { setNotif({ - message: i18n.str`could not update the product`, + message: i18n.str`Could not update the product`, type: "ERROR", description: resp.detail.hint, }); } } catch (error) { setNotif({ - message: i18n.str`could not update the product`, + message: i18n.str`Could not update the product`, type: "ERROR", description: error instanceof Error ? error.message : undefined, }); @@ -146,14 +146,14 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { }); } else { setNotif({ - message: i18n.str`could not delete the product`, + message: i18n.str`Could not delete the product`, type: "ERROR", description: resp.detail.hint, }); } } catch (error) { setNotif({ - message: i18n.str`could not delete the product`, + message: i18n.str`Could not delete the product`, type: "ERROR", description: error instanceof Error ? error.message : undefined, }); @@ -171,7 +171,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { </p> <p class="warning"> <i18n.Translate> - Deleting an product cannot be undone. + Deleting an product can't be undone. </i18n.Translate> </p> </ConfirmModal> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx @@ -87,18 +87,18 @@ export default function UpdateProduct({ onConfirm() } else { setNotif({ - message: i18n.str`could not update product`, + message: i18n.str`Could not update product`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not update product`, + message: i18n.str`Could not update product`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -91,32 +91,32 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const errors: FormErrors<Entity> = { id: !state.id - ? i18n.str`should not be empty` + ? i18n.str`Required` : !/[a-zA-Z0-9]*/.test(state.id) - ? i18n.str`no valid. only characters and numbers` + ? i18n.str`Invalid. only characters and numbers` : undefined, - description: !state.description ? i18n.str`should not be empty` : undefined, + description: !state.description ? i18n.str`Required` : undefined, amount: !state.amount ? state.amount_editable ? undefined - : i18n.str`required` + : i18n.str`Required` : !parsedPrice - ? i18n.str`not valid` + ? i18n.str`Invalid` : Amounts.isZero(parsedPrice) ? state.amount_editable ? undefined - : i18n.str`must be greater than 0` + : i18n.str`Must be greater than 0` : undefined, minimum_age: state.minimum_age && state.minimum_age < 0 - ? i18n.str`should be greater that 0` + ? i18n.str`Must be greater that 0` : undefined, pay_duration: !state.pay_duration - ? i18n.str`can't be empty` + ? i18n.str`Required` : state.pay_duration.d_ms === "forever" ? undefined : state.pay_duration.d_ms < 1000 //less than one second - ? i18n.str`to short` + ? i18n.str`To short` : undefined, }; @@ -231,7 +231,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { /> <TextField name="sc" label={i18n.str`Supported currencies`}> <i18n.Translate> - supported currencies: {cList.join(", ")} + Supported currencies: {cList.join(", ")} </i18n.Translate> </TextField> </Fragment> @@ -289,7 +289,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx @@ -49,23 +49,23 @@ export default function CreateTemplate({ onConfirm, onBack }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`template has been created`, + message: i18n.str`Template has been created`, type: "SUCCESS", }); onConfirm(); } else { setNotif({ - message: i18n.str`could not create template`, + message: i18n.str`Could not create template`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not inform template`, + message: i18n.str`Could not create template`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx @@ -63,7 +63,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new templates`} + data-tooltip={i18n.str`Add new templates`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -124,10 +124,10 @@ function Table({ {onLoadMoreBefore && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more templates before the first one`} + data-tooltip={i18n.str`Load more templates before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -162,21 +162,21 @@ function Table({ <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected templates from the database`} + data-tooltip={i18n.str`Delete selected templates from the database`} onClick={() => onDelete(i)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> <button class="button is-info is-small has-tooltip-left" - data-tooltip={i18n.str`use template to create new order`} + data-tooltip={i18n.str`Use template to create new order`} onClick={() => onNewOrder(i)} > - Use template + <i18n.Translate>Use template</i18n.Translate> </button> <button class="button is-info is-small has-tooltip-left" - data-tooltip={i18n.str`create qr code for the template`} + data-tooltip={i18n.str`Create qr code for the template`} onClick={() => onQR(i)} > QR @@ -191,10 +191,10 @@ function Table({ {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more templates after the last one`} + data-tooltip={i18n.str`Load more templates after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -88,8 +88,8 @@ export default function ListTemplates({ return resp.type === "ok"; }} onSelect={onSelect} - description={i18n.str`jump to template with the given template ID`} - placeholder={i18n.str`template id`} + description={i18n.str`Jump to template with the given template ID`} + placeholder={i18n.str`Template identification`} /> <CardTable @@ -159,7 +159,7 @@ export default function ListTemplates({ <p class="warning"> <i18n.Translate>Deleting an template </i18n.Translate> <b> - <i18n.Translate>cannot be undone</i18n.Translate> + <i18n.Translate>can't be undone</i18n.Translate> </b> . </p> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -21,11 +21,9 @@ import { TalerMerchantApi, - stringifyPayTemplateUri + stringifyPayTemplateUri, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { QR } from "../../../../components/exception/QR.js"; import { useSessionContext } from "../../../../context/session.js"; @@ -56,7 +54,9 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { <section id="printThis"> <QR text={payTemplateUri} /> <pre style={{ textAlign: "center" }}> - <a target="_blank" rel="noreferrer" href={payTemplateUri}>{payTemplateUri}</a> + <a target="_blank" rel="noreferrer" href={payTemplateUri}> + {payTemplateUri} + </a> </pre> </section> @@ -64,35 +64,6 @@ export function QrPage({ id: templateId, onBack }: Props): VNode { <div class="columns"> <div class="column" /> <div class="column is-four-fifths"> - {/* <p class="is-size-5 mt-5 mb-5"> - <i18n.Translate> - Here you can specify a default value for fields that are not - fixed. Default values can be edited by the customer before the - payment. - </i18n.Translate> - </p> */} - - <p></p> - {/* <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputCurrency<Entity> - name="amount" - label={i18n.str`Amount`} - readonly - tooltip={i18n.str`Amount of the order`} - /> - <Input<Entity> - name="summary" - inputType="multiline" - readonly - label={i18n.str`Summary`} - tooltip={i18n.str`Title of the order to be shown to the customer`} - /> - </FormProvider> */} - <div class="buttons is-right mt-5"> {onBack && ( <button class="button" onClick={onBack}> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -115,24 +115,24 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount); const errors: FormErrors<Entity> = { - description: !state.description ? i18n.str`should not be empty` : undefined, + description: !state.description ? i18n.str`Required` : undefined, amount: !state.amount - ? state.amount_editable ? undefined : i18n.str`required` + ? state.amount_editable ? undefined : i18n.str`Required` : !parsedPrice - ? i18n.str`not valid` + ? i18n.str`Invalid` : Amounts.isZero(parsedPrice) - ? state.amount_editable ? undefined : i18n.str`must be greater than 0` + ? state.amount_editable ? undefined : i18n.str`Must be greater than 0` : undefined, minimum_age: state.minimum_age && state.minimum_age < 0 - ? i18n.str`should be greater that 0` + ? i18n.str`Must be greater that 0` : undefined, pay_duration: !state.pay_duration - ? i18n.str`can't be empty` + ? i18n.str`Required` : state.pay_duration.d_ms === "forever" ? undefined : state.pay_duration.d_ms < 1000 // less than one second - ? i18n.str`to short` + ? i18n.str`Too short` : undefined, }; @@ -236,7 +236,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { /> <TextField name="sc" label={i18n.str`Supported currencies`}> <i18n.Translate> - supported currencies: {cList.join(", ")} + Supported currencies: {cList.join(", ")} </i18n.Translate> </TextField> </Fragment> @@ -294,7 +294,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx @@ -93,17 +93,17 @@ export default function UpdateTemplate({ onConfirm(); } else { setNotif({ - message: i18n.str`could not update template`, + message: i18n.str`Could not update template`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not update template`, + message: i18n.str`Could not update template`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx @@ -88,7 +88,7 @@ export default function TemplateUsePage({ onOrderCreated(res.body.order_id) } else { setNotif({ - message: i18n.str`could not create order from template`, + message: i18n.str`Could not create order from template`, type: "ERROR", description: res.case === HttpStatusCode.Gone @@ -97,11 +97,11 @@ export default function TemplateUsePage({ }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not create order from template`, + message: i18n.str`Could not create order from template`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx @@ -28,6 +28,7 @@ import { Input } from "../../../components/form/Input.js"; import { NotificationCard } from "../../../components/menu/index.js"; import { useSessionContext } from "../../../context/session.js"; import { AccessToken, createRFC8959AccessTokenPlain } from "@gnu-taler/taler-util"; +import { undefinedIfEmpty } from "../../../utils/table.js"; interface Props { hasToken: boolean | undefined; @@ -50,25 +51,23 @@ export function DetailPage({ }); const { i18n } = useTranslationContext(); - const errors = { + const errors = undefinedIfEmpty({ old_token: hasToken && !form.old_token - ? i18n.str`you need your access token to perform the operation` + ? i18n.str`You need your access token to perform the operation` : undefined, new_token: !form.new_token - ? i18n.str`cannot be empty` + ? i18n.str`Required` : form.new_token === form.old_token - ? i18n.str`cannot be the same as the old token` + ? i18n.str`Can't be the same as the old token` : undefined, repeat_token: form.new_token !== form.repeat_token - ? i18n.str`is not the same` + ? i18n.str`Is not the same` : undefined, - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as Record<string, unknown>)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const { state } = useSessionContext(); @@ -120,7 +119,7 @@ export function DetailPage({ <Input<State> name="old_token" label={i18n.str`Current access token`} - tooltip={i18n.str`access token currently in use`} + tooltip={i18n.str`Access token currently in use`} inputType="password" /> <p> @@ -149,13 +148,13 @@ export function DetailPage({ <Input<State> name="new_token" label={i18n.str`New access token`} - tooltip={i18n.str`next access token to be used`} + tooltip={i18n.str`Next access token to be used`} inputType="password" /> <Input<State> name="repeat_token" label={i18n.str`Repeat access token`} - tooltip={i18n.str`confirm the same access token`} + tooltip={i18n.str`Confirm the same access token`} inputType="password" /> </Fragment> @@ -171,7 +170,7 @@ export function DetailPage({ data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx @@ -18,9 +18,7 @@ import { TalerError, assertUnreachable, } from "@gnu-taler/taler-util"; -import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; @@ -88,13 +86,11 @@ export default function Token({ onChange, onCancel }: Props): VNode { }); } } catch (error) { - if (error instanceof Error) { - return setNotif({ - message: i18n.str`Failed to clear token`, - type: "ERROR", - description: error.message, - }); - } + return setNotif({ + message: i18n.str`Failed to clear token`, + type: "ERROR", + description: error instanceof Error ? error.message : String(error), + }); } }} onNewToken={async (currentToken, newToken): Promise<void> => { @@ -136,13 +132,11 @@ export default function Token({ onChange, onCancel }: Props): VNode { }); } } catch (error) { - if (error instanceof Error) { return setNotif({ message: i18n.str`Failed to set new token`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); - } } }} /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx @@ -48,23 +48,23 @@ export default function CreateTokenFamily({ onConfirm, onBack }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`token familty created successfully`, + message: i18n.str`Token familty created successfully`, type: "SUCCESS", }); onConfirm(); } else { setNotif({ - message: i18n.str`could not create token family`, + message: i18n.str`Could not create token family`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not create token family`, + message: i18n.str`Could not create token family`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx @@ -62,7 +62,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add token family`} + data-tooltip={i18n.str`Add token family`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -189,7 +189,7 @@ function Table({ <div class="buttons is-right"> <span class="has-tooltip-bottom" - data-tooltip={i18n.str`go to token family update page`} + data-tooltip={i18n.str`Go to token family update page`} > <button class="button is-small is-success " @@ -201,7 +201,7 @@ function Table({ </span> <span class="has-tooltip-left" - data-tooltip={i18n.str`remove this token family from the database`} + data-tooltip={i18n.str`Remove this token family from the database`} > <button class="button is-small is-danger" diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx @@ -88,19 +88,19 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { ); if (resp.type === "ok") { setNotif({ - message: i18n.str`token family updated successfully`, + message: i18n.str`Token family updated successfully`, type: "SUCCESS", }); } else { setNotif({ - message: i18n.str`could not update the token family`, + message: i18n.str`Could not update the token family`, type: "ERROR", description: resp.detail.hint, }); } } catch (error) { setNotif({ - message: i18n.str`could not update the token family`, + message: i18n.str`Could not update the token family`, type: "ERROR", description: error instanceof Error ? error.message : undefined, }); @@ -157,7 +157,7 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { <i18n.Translate> Deleting a token family{" "} <b> - <i18n.Translate>cannot be undone</i18n.Translate> + <i18n.Translate>can't be undone</i18n.Translate> </b> . </i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx @@ -28,6 +28,7 @@ import { FormErrors, FormProvider } from "../../../../components/form/FormProvid import { Input } from "../../../../components/form/Input.js"; import { InputDate } from "../../../../components/form/InputDate.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = Omit<TalerMerchantApi.TokenFamilyUpdateRequest, "duration"> & { duration: Duration, @@ -51,17 +52,15 @@ function convert(from: TalerMerchantApi.TokenFamilyUpdateRequest) { export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { const [value, valueHandler] = useState<Partial<Entity>>(convert(tokenFamily)); const { i18n } = useTranslationContext(); - const errors: FormErrors<Entity> = { - name: !value.name ? i18n.str`required` : undefined, - description: !value.description ? i18n.str`required` : undefined, - valid_after: !value.valid_after ? i18n.str`required` : undefined, - valid_before: !value.valid_before ? i18n.str`required` : undefined, - duration: !value.duration ? i18n.str`required` : undefined, - }; + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + name: !value.name ? i18n.str`Required` : undefined, + description: !value.description ? i18n.str`Required` : undefined, + valid_after: !value.valid_after ? i18n.str`Required` : undefined, + valid_before: !value.valid_before ? i18n.str`Required` : undefined, + duration: !value.duration ? i18n.str`Required` : undefined, + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); @@ -107,30 +106,30 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { name="name" inputType="text" label={i18n.str`Name`} - tooltip={i18n.str`user-readable token family name`} + tooltip={i18n.str`User-readable token family name`} /> <Input<Entity> name="description" inputType="multiline" label={i18n.str`Description`} - tooltip={i18n.str`token family description for customers`} + tooltip={i18n.str`Token family description for customers`} /> <InputDate<Entity> name="valid_after" label={i18n.str`Valid After`} - tooltip={i18n.str`token family can issue tokens after this date`} + tooltip={i18n.str`Token family can issue tokens after this date`} withTimestampSupport /> <InputDate<Entity> name="valid_before" label={i18n.str`Valid Before`} - tooltip={i18n.str`token family can issue tokens until this date`} + tooltip={i18n.str`Token family can issue tokens until this date`} withTimestampSupport /> <InputDuration<Entity> name="duration" label={i18n.str`Duration`} - tooltip={i18n.str`validity duration of a issued token`} + tooltip={i18n.str`Validity duration of a issued token`} withForever /> </FormProvider> @@ -146,7 +145,7 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx @@ -103,17 +103,17 @@ export default function UpdateTokenFamily({ onConfirm(); } else { setNotif({ - message: i18n.str`could not update token family`, + message: i18n.str`Could not update token family`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not update token family`, + message: i18n.str`Could not update token family`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx @@ -35,6 +35,7 @@ import { CROCKFORD_BASE32_REGEX, URL_REGEX, } from "../../../../utils/constants.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = TalerMerchantApi.TransferInformation; @@ -54,26 +55,24 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { credit_amount: `` as AmountString, }); - const errors: FormErrors<Entity> = { + const errors = undefinedIfEmpty<FormErrors<Entity>>({ wtid: !state.wtid - ? i18n.str`cannot be empty` + ? i18n.str`Required` : !CROCKFORD_BASE32_REGEX.test(state.wtid) - ? i18n.str`check the id, does not look valid` - : state.wtid.length !== 52 - ? i18n.str`should have 52 characters, current ${state.wtid.length}` - : undefined, - payto_uri: !state.payto_uri ? i18n.str`cannot be empty` : undefined, - credit_amount: !state.credit_amount ? i18n.str`cannot be empty` : undefined, + ? i18n.str`Check the id, does not look valid` + : state.wtid.length !== 52 + ? i18n.str`Must have 52 characters, current ${state.wtid.length}` + : undefined, + payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, + credit_amount: !state.credit_amount ? i18n.str`Required` : undefined, exchange_url: !state.exchange_url - ? i18n.str`cannot be empty` + ? i18n.str`Required` : !URL_REGEX.test(state.exchange_url) - ? i18n.str`URL doesn't have the right format` - : undefined, - }; + ? i18n.str`URL doesn't have the right format` + : undefined, + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); @@ -102,7 +101,7 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { name="wtid" label={i18n.str`Wire transfer ID`} help="" - tooltip={i18n.str`unique identifier of the wire transfer used by the exchange, must be 52 characters long`} + tooltip={i18n.str`Unique identifier of the wire transfer used by the exchange, must be 52 characters long`} /> <Input<Entity> name="exchange_url" @@ -128,7 +127,7 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx @@ -59,23 +59,23 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`wire transfer informed successfully`, + message: i18n.str`Wire transfer informed successfully`, type: "SUCCESS", }); onConfirm() } else { setNotif({ - message: i18n.str`could not inform transfer`, + message: i18n.str`Could not inform transfer`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not inform transfer`, + message: i18n.str`Could not inform transfer`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx @@ -81,7 +81,7 @@ export function ListPage({ return d }} placeholder={i18n.str`All accounts`} - tooltip={i18n.str`filter by account address`} + tooltip={i18n.str`Filter by account address`} /> </FormProvider> </div> @@ -92,7 +92,7 @@ export function ListPage({ <li class={isAllTransfers ? "is-active" : ""}> <div class="has-tooltip-right" - data-tooltip={i18n.str`remove all filters`} + data-tooltip={i18n.str`Remove all filters`} > <a onClick={onShowAll}> <i18n.Translate>All</i18n.Translate> @@ -102,7 +102,7 @@ export function ListPage({ <li class={isVerifiedTransfers ? "is-active" : ""}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show wire transfers confirmed by the merchant`} + data-tooltip={i18n.str`Only show wire transfers confirmed by the merchant`} > <a onClick={onShowVerified}> <i18n.Translate>Verified</i18n.Translate> @@ -112,7 +112,7 @@ export function ListPage({ <li class={isNonVerifiedTransfers ? "is-active" : ""}> <div class="has-tooltip-right" - data-tooltip={i18n.str`only show wire transfers claimed by the exchange`} + data-tooltip={i18n.str`Only show wire transfers claimed by the exchange`} > <a onClick={onShowUnverified}> <i18n.Translate>Unverified</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -61,7 +61,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new transfer`} + data-tooltip={i18n.str`Add new transfer`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -114,10 +114,10 @@ function Table({ {onLoadMoreBefore && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more transfers before the first one`} + data-tooltip={i18n.str`Load more transfers before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -163,7 +163,7 @@ function Table({ {i.verified !== true ? ( <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected transfer from the database`} + data-tooltip={i18n.str`Delete selected transfer from the database`} onClick={() => onDelete(i)} > <i18n.Translate>Delete</i18n.Translate> @@ -178,10 +178,10 @@ function Table({ {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more transfers after the last one`} + data-tooltip={i18n.str`Load more transfers after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -37,7 +37,6 @@ export type Entity = Omit<Omit<TalerMerchantApi.InstanceReconfigurationMessage, default_wire_transfer_delay: Duration, }; -//TalerMerchantApi.InstanceAuthConfigurationMessage interface Props { onUpdate: (d: TalerMerchantApi.InstanceReconfigurationMessage) => void; selected: TalerMerchantApi.QueryInstancesResponse; @@ -69,40 +68,38 @@ export function UpdatePage({ const { i18n } = useTranslationContext(); - const errors: FormErrors<Entity> = { - name: !value.name ? i18n.str`required` : undefined, + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + name: !value.name ? i18n.str`Required` : undefined, user_type: !value.user_type - ? i18n.str`required` + ? i18n.str`Required` : value.user_type !== "business" && value.user_type !== "individual" - ? i18n.str`should be business or individual` + ? i18n.str`Must be business or individual` : undefined, default_pay_delay: !value.default_pay_delay - ? i18n.str`required` + ? i18n.str`Required` : !!value.default_wire_transfer_delay && value.default_wire_transfer_delay.d_ms !== "forever" && value.default_pay_delay.d_ms !== "forever" && value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ? - i18n.str`pay delay can't be greater than wire transfer delay` : undefined, + i18n.str`Pay delay can't be greater than wire transfer delay` : undefined, default_wire_transfer_delay: !value.default_wire_transfer_delay - ? i18n.str`required` + ? i18n.str`Required` : undefined, address: undefinedIfEmpty({ address_lines: value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` + ? i18n.str`Max 7 lines` : undefined, }), jurisdiction: undefinedIfEmpty({ address_lines: value.address?.address_lines && value.address?.address_lines.length > 7 - ? i18n.str`max 7 lines` + ? i18n.str`Max 7 lines` : undefined, }), - }; + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submit = async (): Promise<void> => { const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>; @@ -111,7 +108,7 @@ export function UpdatePage({ default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay), ...rest, } - await onUpdate(result); + onUpdate(result); }; // const [active, setActive] = useState(false); @@ -159,7 +156,7 @@ export function UpdatePage({ data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } disabled={hasErrors} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -103,11 +103,11 @@ function CommonUpdate( } return updateInstance(state.token, d) .then(onConfirm) - .catch((error: Error) => + .catch((error: unknown) => setNotif({ message: i18n.str`Failed to update instance`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx @@ -30,6 +30,7 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = TalerMerchantApi.WebhookAddDetails; @@ -45,26 +46,26 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>({}); - const errors: FormErrors<Entity> = { - webhook_id: !state.webhook_id ? i18n.str`required` : undefined, - event_type: !state.event_type ? i18n.str`required` - : state.event_type !== "pay" && state.event_type !== "refund" ? i18n.str`it should be "pay" or "refund"` + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + webhook_id: !state.webhook_id ? i18n.str`Required` : undefined, + event_type: !state.event_type + ? i18n.str`Required` + : state.event_type !== "pay" && state.event_type !== "refund" + ? i18n.str`Must be "pay" or "refund"` : undefined, http_method: !state.http_method - ? i18n.str`required` + ? i18n.str`Required` : !validMethod.includes(state.http_method) - ? i18n.str`should be one of '${validMethod.join(", ")}'` + ? i18n.str`Must be one of '${validMethod.join(", ")}'` : undefined, - url: !state.url ? i18n.str`required` : undefined, - }; + url: !state.url ? i18n.str`Required` : undefined, + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); - return onCreate(state as any); + return onCreate(state as TalerMerchantApi.WebhookAddDetails); }; return ( @@ -88,8 +89,8 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { label={i18n.str`Event`} values={[ i18n.str`Choose one...`, - i18n.str`pay`, - i18n.str`refund`, + i18n.str`Pay`, + i18n.str`Refund`, ]} tooltip={i18n.str`The event of the webhook: why the webhook is used`} /> @@ -114,28 +115,79 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { /> <p> - The text below support <a target="_blank" rel="noreferrer" href="https://mustache.github.io/mustache.5.html">mustache</a> template engine. Any string - between <pre style={{ display: "inline", padding: 0 }}>{{</pre> and <pre style={{ display: "inline", padding: 0 }}>}}</pre> will - be replaced with replaced with the value of the corresponding variable. + {/* prettier will add some nodes which we don't want because of i18n */} + {/* prettier-ignore */} + <i18n.Translate> + The text below support <a + target="_blank" + rel="noreferrer" + href="https://mustache.github.io/mustache.5.html" + > + mustache + </a> template engine. Any string between <pre style={{ display: "inline", padding: 0 }}> + {{ + </pre> and <pre style={{ display: "inline", padding: 0 }}> + }} + </pre> will be replaced with replaced with the value of the + corresponding variable. + </i18n.Translate> </p> <p> - For example <pre style={{ display: "inline", padding: 0 }}>{{contract_terms.amount}}</pre> will be replaced - with the the order's price + {/* prettier will add some nodes which we don't want because of i18n */} + {/* prettier-ignore */} + <i18n.Translate> + For example <pre style={{ display: "inline", padding: 0 }}> + {{contract_terms.amount}} + </pre> will be replaced with the the order's price + </i18n.Translate> </p> <p> - The short list of variables are: + <i18n.Translate> + The short list of variables are: + </i18n.Translate> </p> <div class="menu"> - - <ul class="menu-list" style={{ listStyleType: "disc", marginLeft: 20 }}> - <li><b>contract_terms.summary:</b> order's description </li> - <li><b>contract_terms.amount:</b> order's price </li> - <li><b>order_id:</b> order's unique identification </li> - {state.event_type === "refund" && <Fragment> - <li><b>refund_amout:</b> the amount that was being refunded</li> - <li><b>reason:</b> the reason entered by the merchant staff for granting the refund</li> - <li><b>timestamp:</b> time of the refund in nanoseconds since 1970</li> - </Fragment>} + <ul + class="menu-list" + style={{ listStyleType: "disc", marginLeft: 20 }} + > + <li> + <b>contract_terms.summary:</b>{" "} + <i18n.Translate>order's description</i18n.Translate> + </li> + <li> + <b>contract_terms.amount:</b>{" "} + <i18n.Translate>order's price</i18n.Translate> + </li> + <li> + <b>order_id:</b>{" "} + <i18n.Translate> + order's unique identification + </i18n.Translate> + </li> + {state.event_type === "refund" && ( + <Fragment> + <li> + <b>refund_amout:</b>{" "} + <i18n.Translate> + the amount that was being refunded + </i18n.Translate> + </li> + <li> + <b>reason:</b>{" "} + <i18n.Translate> + the reason entered by the merchant staff for granting + the refund + </i18n.Translate> + </li> + <li> + <b>timestamp:</b>{" "} + <i18n.Translate> + time of the refund in nanoseconds since 1970 + </i18n.Translate> + </li> + </Fragment> + )} </ul> </div> {/* <Input<Entity> @@ -163,7 +215,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx @@ -49,23 +49,23 @@ export default function CreateWebhook({ onConfirm, onBack }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`webhook create successfully`, + message: i18n.str`Webhook create successfully`, type: "SUCCESS", }); onConfirm() } else { setNotif({ - message: i18n.str`could not create the webhook`, + message: i18n.str`Could not create the webhook`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not create webhook`, + message: i18n.str`Could not create webhook`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -59,7 +59,7 @@ export function CardTable({ <div class="card-header-icon" aria-label="more options"> <span class="has-tooltip-left" - data-tooltip={i18n.str`add new webhooks`} + data-tooltip={i18n.str`Add new webhooks`} > <button class="button is-info" type="button" onClick={onCreate}> <span class="icon is-small"> @@ -114,10 +114,10 @@ function Table({ {onLoadMoreBefore && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more webhooks before the first one`} + data-tooltip={i18n.str`Load more webhooks before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load first page</i18n.Translate> + <i18n.Translate>Load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -152,18 +152,11 @@ function Table({ <div class="buttons is-right"> <button class="button is-danger is-small has-tooltip-left" - data-tooltip={i18n.str`delete selected webhook from the database`} + data-tooltip={i18n.str`Delete selected webhook from the database`} onClick={() => onDelete(i)} > - Delete + <i18n.Translate>Delete</i18n.Translate> </button> - {/* <button - class="button is-info is-small has-tooltip-left" - data-tooltip={i18n.str`test webhook`} - onClick={() => onNewOrder(i)} - > - Test - </button> */} </div> </td> </tr> @@ -174,10 +167,10 @@ function Table({ {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more webhooks after the last one`} + data-tooltip={i18n.str`Load more webhooks after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load next page</i18n.Translate> + <i18n.Translate>Load next page</i18n.Translate> </button> )} </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx @@ -85,22 +85,22 @@ export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`webhook delete successfully`, + message: i18n.str`Webhook delete successfully`, type: "SUCCESS", }); } else { setNotif({ - message: i18n.str`could not delete the webhook`, + message: i18n.str`Could not delete the webhook`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => + .catch((error: unknown) => setNotif({ - message: i18n.str`could not delete the webhook`, + message: i18n.str`Could not delete the webhook`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }), ); }} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -28,8 +29,8 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { WithId } from "../../../../declaration.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; type Entity = TalerMerchantApi.WebhookPatchDetails & WithId; @@ -45,23 +46,21 @@ export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>(webhook); - const errors: FormErrors<Entity> = { - event_type: !state.event_type ? i18n.str`required` : undefined, + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + event_type: !state.event_type ? i18n.str`Required` : undefined, http_method: !state.http_method - ? i18n.str`required` + ? i18n.str`Required` : !validMethod.includes(state.http_method) - ? i18n.str`should be one of '${validMethod.join(", ")}'` - : undefined, - url: !state.url ? i18n.str`required` : undefined, - }; + ? i18n.str`Must be one of '${validMethod.join(", ")}'` + : undefined, + url: !state.url ? i18n.str`Required` : undefined, + }); - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + const hasErrors = errors !== undefined; const submitForm = () => { if (hasErrors) return Promise.reject(); - return onUpdate(state as any); + return onUpdate(state as Entity); }; return ( @@ -130,7 +129,7 @@ export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode { data-tooltip={ hasErrors ? i18n.str`Need to complete marked fields` - : "confirm operation" + : i18n.str`Confirm operation` } onClick={submitForm} > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx @@ -29,6 +29,7 @@ import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchan import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; import { useSessionContext } from "../../../../context/session.js"; +import { WithId } from "../../../../declaration.js"; import { useWebhookDetails, } from "../../../../hooks/webhooks.js"; @@ -36,7 +37,6 @@ import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; -import { WithId } from "../../../../declaration.js"; export type Entity = TalerMerchantApi.WebhookPatchDetails & WithId; @@ -80,29 +80,29 @@ export default function UpdateWebhook({ <UpdatePage webhook={{ ...result.body, id: tid }} onBack={onBack} - onUpdate={(data) => { + onUpdate={async (data) => { return lib.instance.updateWebhook(state.token, tid, data) .then((resp) => { if (resp.type === "ok") { setNotif({ - message: i18n.str`webhook updated`, + message: i18n.str`Webhook updated`, type: "SUCCESS", }); onConfirm() } else { setNotif({ - message: i18n.str`could not update webhook`, + message: i18n.str`Could not update webhook`, type: "ERROR", description: resp.detail.hint, }); } }) - .catch((error) => { + .catch((error: unknown) => { setNotif({ - message: i18n.str`could not update webhook`, + message: i18n.str`Could not update webhook`, type: "ERROR", - description: error.message, + description: error instanceof Error ? error.message : String(error), }); }); }} diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -19,10 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode, createRFC8959AccessTokenEncoded } from "@gnu-taler/taler-util"; import { - useTranslationContext -} from "@gnu-taler/web-util/browser"; + HttpStatusCode, + createRFC8959AccessTokenEncoded, +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../components/menu/index.js"; @@ -59,14 +60,14 @@ export function LoginPage(_p: Props): VNode { switch (result.case) { case HttpStatusCode.Unauthorized: { setNotif({ - message: "Your password is incorrect", + message: i18n.str`Your password is incorrect`, type: "ERROR", }); return; } case HttpStatusCode.NotFound: { setNotif({ - message: "Your instance not found", + message: i18n.str`Your instance not found`, type: "ERROR", }); return; diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { AbsoluteTime } from "@gnu-taler/taler-util"; import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { @@ -24,7 +25,6 @@ import { InputSelector } from "../../components/form/InputSelector.js"; import { InputToggle } from "../../components/form/InputToggle.js"; import { LangSelector } from "../../components/menu/LangSelector.js"; import { Preferences, usePreference } from "../../hooks/preference.js"; -import { AbsoluteTime } from "@gnu-taler/taler-util"; function getBrowserLang(): string | undefined { if (typeof window === "undefined") return undefined; @@ -46,6 +46,7 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { const v: Preferences = { advanceOrderMode: next.advanceOrderMode ?? false, advanceInstanceMode: next.advanceInstanceMode ?? false, + developerMode: next.developerMode ?? false, hideMissingAccountUntil: next.hideMissingAccountUntil ?? AbsoluteTime.never(), hideKycUntil: next.hideKycUntil ?? AbsoluteTime.never(), dateFormat: next.dateFormat ?? "ymd", @@ -85,7 +86,7 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { {borwserLang !== undefined && ( <button - data-tooltip={i18n.str`generate random secret key`} + data-tooltip={i18n.str`Generate random secret key`} class="button is-info mr-2" onClick={(e) => { update(borwserLang.substring(0, 2)); @@ -127,7 +128,12 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { return "choose one"; }} values={["ymd", "mdy", "dmy"]} - tooltip={i18n.str`how the date is going to be displayed`} + tooltip={i18n.str`How the date is going to be displayed`} + /> + <InputToggle<Preferences> + label={i18n.str`Developer mode`} + tooltip={i18n.str`Shows more options and tools which are not intended for general audience.`} + name="developerMode" /> </FormProvider> </div> diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts @@ -1,293 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { isAfter, isFuture } from "date-fns"; -import * as yup from "yup"; -import { PAYTO_REGEX } from "../utils/constants.js"; -// import { MerchantBackend } from "../declaration.js"; - -yup.setLocale({ - mixed: { - default: "field_invalid", - }, - number: { - min: ({ min }: any) => ({ key: "field_too_short", values: { min } }), - max: ({ max }: any) => ({ key: "field_too_big", values: { max } }), - }, -}); - -function listOfPayToUrisAreValid(values?: (string | undefined)[]): boolean { - return !!values && values.every((v) => v && PAYTO_REGEX.test(v)); -} - -function currencyWithAmountIsValid(value?: string): boolean { - return !!value && Amounts.parse(value) !== undefined; -} -function currencyGreaterThan0(value?: string) { - if (value) { - try { - const [, amount] = value.split(":"); - const intAmount = parseInt(amount, 10); - return intAmount > 0; - } catch { - return false; - } - } - return true; -} - -export const InstanceSchema = yup.object().shape({ - id: yup.string().required().meta({ type: "url" }), - name: yup.string().required(), - auth: yup.object().shape({ - method: yup.string().matches(/^(external|token)$/), - token: yup.string().optional().nullable(), - }), - payto_uris: yup - .array() - .of(yup.string()) - .min(1) - .meta({ type: "array" }) - .test("payto", "{path} is not valid", listOfPayToUrisAreValid), - default_max_deposit_fee: yup - .string() - .required() - .test("amount", "the amount is not valid", currencyWithAmountIsValid) - .meta({ type: "amount" }), - default_max_wire_fee: yup - .string() - .required() - .test("amount", "{path} is not valid", currencyWithAmountIsValid) - .meta({ type: "amount" }), - default_wire_fee_amortization: yup.number().required(), - address: yup - .object() - .shape({ - country: yup.string().optional(), - address_lines: yup.array().of(yup.string()).max(7).optional(), - building_number: yup.string().optional(), - building_name: yup.string().optional(), - street: yup.string().optional(), - post_code: yup.string().optional(), - town_location: yup.string().optional(), - town: yup.string(), - district: yup.string().optional(), - country_subdivision: yup.string().optional(), - }) - .meta({ type: "group" }), - jurisdiction: yup - .object() - .shape({ - country: yup.string().optional(), - address_lines: yup.array().of(yup.string()).max(7).optional(), - building_number: yup.string().optional(), - building_name: yup.string().optional(), - street: yup.string().optional(), - post_code: yup.string().optional(), - town_location: yup.string().optional(), - town: yup.string(), - district: yup.string().optional(), - country_subdivision: yup.string().optional(), - }) - .meta({ type: "group" }), - // default_pay_delay: yup.object() - // .shape({ d_us: yup.number() }) - // .required() - // .meta({ type: 'duration' }), - // .transform(numberToDuration), - default_wire_transfer_delay: yup - .object() - .shape({ d_us: yup.number() }) - .required() - .meta({ type: "duration" }), - // .transform(numberToDuration), -}); - -export const InstanceUpdateSchema = InstanceSchema.clone().omit(["id"]); -export const InstanceCreateSchema = InstanceSchema.clone(); - -export const OrderCreateSchema = yup.object().shape({ - pricing: yup - .object() - .required() - .shape({ - summary: yup.string().ensure().required(), - order_price: yup - .string() - .ensure() - .required() - .test("amount", "the amount is not valid", currencyWithAmountIsValid) - .test( - "amount_positive", - "the amount should be greater than 0", - currencyGreaterThan0, - ), - }), - // extra: yup.object().test("extra", "is not a JSON format", stringIsValidJSON), - payments: yup - .object() - .required() - .shape({ - refund_deadline: yup - .date() - .test("future", "should be in the future", (d) => - d ? isFuture(d) : true, - ), - pay_deadline: yup - .date() - .test("future", "should be in the future", (d) => - d ? isFuture(d) : true, - ), - auto_refund_deadline: yup - .date() - .test("future", "should be in the future", (d) => - d ? isFuture(d) : true, - ), - delivery_date: yup - .date() - .test("future", "should be in the future", (d) => - d ? isFuture(d) : true, - ), - }) - .test("payment", "dates", (d) => { - if ( - d.pay_deadline && - d.refund_deadline && - isAfter(d.refund_deadline, d.pay_deadline) - ) { - return new yup.ValidationError( - "pay deadline should be greater than refund", - "asd", - "payments.pay_deadline", - ); - } - return true; - }), -}); - -export const ProductCreateSchema = yup.object().shape({ - product_id: yup.string().ensure().required(), - description: yup.string().required(), - unit: yup.string().ensure().required(), - price: yup - .string() - .required() - .test("amount", "the amount is not valid", currencyWithAmountIsValid), - stock: yup.object({}).optional(), - minimum_age: yup.number().optional().min(0), -}); - -export const ProductUpdateSchema = yup.object().shape({ - description: yup.string().required(), - price: yup - .string() - .required() - .test("amount", "the amount is not valid", currencyWithAmountIsValid), - stock: yup.object({}).optional(), - minimum_age: yup.number().optional().min(0), -}); - -export const TaxSchema = yup.object().shape({ - name: yup.string().required().ensure(), - tax: yup - .string() - .required() - .test("amount", "the amount is not valid", currencyWithAmountIsValid), -}); - -export const NonInventoryProductSchema = yup.object().shape({ - quantity: yup.number().required().positive(), - description: yup.string().required(), - unit: yup.string().ensure().required(), - price: yup - .string() - .required() - .test("amount", "the amount is not valid", currencyWithAmountIsValid), -}); - -const timestampSchema = yup - .object() - .shape({ - t_s: yup - .mixed() - .test( - "is-timestamp", - "Invalid timestamp", - (value) => typeof value === "number" || value === "never", - ), - }) - .required(); - -const durationSchema = yup - .object() - .shape({ - d_us: yup - .mixed() - .test( - "is-duration", - "Invalid duration", - (value) => typeof value === "number" || value === "forever", - ), - }) - .required(); - -const tokenFamilyKindSchema = yup - .mixed() - .oneOf<TalerMerchantApi.TokenFamilyKind>([ - TalerMerchantApi.TokenFamilyKind.Discount, - TalerMerchantApi.TokenFamilyKind.Subscription, - ]) - .required(); - -export const TokenFamilyCreateSchema = yup.object().shape({ - slug: yup.string().ensure().required(), - name: yup.string().required(), - description: yup.string().required(), - // description_i18n: yup.lazy((obj) => - // yup.object().shape( - // Object.keys(obj || {}).reduce((acc, key) => { - // acc[key] = yup.string().required(); - // return acc; - // }, {}) - // ) - // ).optional(), - valid_after: timestampSchema.optional(), - valid_before: timestampSchema, - duration: durationSchema, - kind: tokenFamilyKindSchema, -}); - -export const TokenFamilyUpdateSchema = yup.object().shape({ - name: yup.string().required(), - description: yup.string().required(), - // description_i18n: yup.lazy((obj) => - // yup.object().shape( - // Object.keys(obj).reduce((acc, key) => { - // acc[key] = yup.string().required(); - // return acc; - // }, {}) - // ) - // ), - valid_after: timestampSchema, - valid_before: timestampSchema, - duration: durationSchema, -}); diff --git a/packages/merchant-backoffice-ui/src/settings.json b/packages/merchant-backoffice-ui/src/settings.json @@ -0,0 +1,4 @@ +{ + "backendBaseURL": "http://merchant.taler.test:1180/" +} +