taler-typescript-core

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

commit 74c60e6f4c1b91e987506634e40a6dd4c48480f9
parent 2eaf72068694d26e17020299245a202ef9d0ad29
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Wed, 11 Feb 2026 15:38:47 -0300

better error message for users

Diffstat:
Mpackages/aml-backoffice-ui/src/Routing.tsx | 4++--
Dpackages/aml-backoffice-ui/src/components/CreateAccount.tsx | 126-------------------------------------------------------------------------------
Apackages/aml-backoffice-ui/src/components/CreateSession.tsx | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/aml-backoffice-ui/src/components/HandleAccountNotReady.tsx | 10+++++-----
Mpackages/aml-backoffice-ui/src/components/MeasureList.tsx | 6+++---
Dpackages/aml-backoffice-ui/src/components/UnlockAccount.tsx | 119-------------------------------------------------------------------------------
Apackages/aml-backoffice-ui/src/components/UnlockSession.tsx | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/aml-backoffice-ui/src/context/ui-settings.ts | 3---
Mpackages/aml-backoffice-ui/src/hooks/account.ts | 6+++---
Mpackages/aml-backoffice-ui/src/hooks/decisions.ts | 18+++++++++---------
Mpackages/aml-backoffice-ui/src/hooks/legitimizations.ts | 6+++---
Mpackages/aml-backoffice-ui/src/hooks/officer.ts | 42+++++++++++++++++++++---------------------
Mpackages/aml-backoffice-ui/src/hooks/server-info.ts | 14+++++++-------
Mpackages/aml-backoffice-ui/src/hooks/transfers.ts | 10+++++-----
Mpackages/aml-backoffice-ui/src/pages/AccountDetails.tsx | 20++++++++++++++++++--
Mpackages/aml-backoffice-ui/src/pages/AccountList.tsx | 38+++++++++++++++++++++++++++++++-------
Mpackages/aml-backoffice-ui/src/pages/Dashboard.tsx | 4++--
Mpackages/aml-backoffice-ui/src/pages/Profile.tsx | 18+++++++++---------
Mpackages/aml-backoffice-ui/src/pages/Search.tsx | 10+++++-----
Mpackages/aml-backoffice-ui/src/pages/ShowCollectedInfo.tsx | 6+++---
Mpackages/aml-backoffice-ui/src/pages/Transfers.tsx | 6+++---
Mpackages/aml-backoffice-ui/src/pages/decision/Summary.tsx | 9+++++----
Mpackages/bank-ui/src/Routing.tsx | 7+++++--
Mpackages/bank-ui/src/pages/ConversionRateClassDetails.tsx | 12+++++++++---
Mpackages/bank-ui/src/pages/LoginForm.tsx | 21+++++++++++++++------
Mpackages/bank-ui/src/pages/NewConversionRateClass.tsx | 4++++
Mpackages/bank-ui/src/pages/OperationState/views.tsx | 9++++++++-
Mpackages/bank-ui/src/pages/PaytoWireTransferForm.tsx | 2+-
Mpackages/bank-ui/src/pages/QrCodeSection.tsx | 5++++-
Mpackages/bank-ui/src/pages/RegistrationPage.tsx | 6++++--
Mpackages/bank-ui/src/pages/SolveMFA.tsx | 47++++++++++++++++++++++++++++++-----------------
Mpackages/bank-ui/src/pages/WalletWithdrawForm.tsx | 2+-
Mpackages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx | 34+++++++++++++++++++++++++++++++++-
Mpackages/bank-ui/src/pages/account/ShowAccountDetails.tsx | 3+++
Mpackages/bank-ui/src/pages/account/UpdateAccountPassword.tsx | 5++++-
Mpackages/bank-ui/src/pages/admin/CreateNewAccount.tsx | 1+
Mpackages/bank-ui/src/pages/admin/DownloadStats.tsx | 8++++++--
Mpackages/bank-ui/src/pages/admin/RemoveAccount.tsx | 4+++-
Mpackages/bank-ui/src/pages/regional/ConversionConfig.tsx | 8+++++++-
Mpackages/bank-ui/src/pages/regional/CreateCashout.tsx | 11+++++++----
Mpackages/bank-ui/src/settings.json | 8++++----
Mpackages/bank-ui/src/utils.ts | 92-------------------------------------------------------------------------------
Mpackages/challenger-ui/src/pages/AnswerChallenge.tsx | 4++++
Mpackages/challenger-ui/src/pages/AskChallenge.tsx | 11+++++------
Mpackages/challenger-ui/src/pages/Setup.tsx | 4++++
Mpackages/kyc-ui/src/pages/FillForm.tsx | 1+
Mpackages/kyc-ui/src/pages/Start.tsx | 14++++++++++----
Mpackages/kyc-ui/src/pages/TriggerKyc.tsx | 1+
Mpackages/merchant-backoffice-ui/src/Application.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/Routing.tsx | 15+++++++--------
Mpackages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx | 26+++++++++++++-------------
Mpackages/merchant-backoffice-ui/src/components/SolveMFA.tsx | 32+++++++++++++++++++++-----------
Mpackages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx | 7++++---
Mpackages/merchant-backoffice-ui/src/components/menu/index.tsx | 39++-------------------------------------
Mpackages/merchant-backoffice-ui/src/components/notifications/index.tsx | 9+++++----
Mpackages/merchant-backoffice-ui/src/hooks/notifications.ts | 16++++++++--------
Mpackages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx | 17++++++++++++++---
Mpackages/merchant-backoffice-ui/src/paths/admin/list/View.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 24+++++++++++++++---------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx | 8+++++---
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 29+++++++++++++++++------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx | 16+++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx | 7++++++-
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx | 5-----
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx | 8++------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/create/CreatePage.tsx | 7++++++-
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/list/UpdatePage.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 125++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx | 6++++--
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx | 33++++++++++++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/password/index.tsx | 2++
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/create/CreatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/update/UpdatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx | 3++-
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx | 3++-
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx | 7++++---
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/create/CreatePage.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/update/UpdatePage.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/paths/instance/statistics/list/OrdersChart.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/statistics/list/RevenueChart.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx | 6++----
Mpackages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx | 8++++++--
Mpackages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/newAccount/index.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/notfound/index.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx | 1+
Mpackages/merchant-backoffice-ui/src/paths/settings/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/utils/types.ts | 7-------
Mpackages/taler-harness/src/harness/tops.ts | 12++++++------
Mpackages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-postal.ts | 5+----
Mpackages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-sms.ts | 4++--
Mpackages/taler-util/src/aml/reporting.ts | 6+++---
Mpackages/taler-util/src/http-client/exchange-client.ts | 27++++++++++++++-------------
Mpackages/taler-util/src/http-client/officer-account.ts | 6+++---
Mpackages/taler-util/src/types-taler-common.ts | 2+-
Mpackages/web-util/src/components/Attention.tsx | 2+-
Mpackages/web-util/src/components/ErrorLoadingMerchant.tsx | 4++--
Mpackages/web-util/src/components/NotificationBanner.tsx | 33++++++++++++++++++++++++++++++---
Apackages/web-util/src/components/NotificationCardBulma.tsx | 45+++++++++++++++++++++++++++++++++++++++++++++
Mpackages/web-util/src/components/index.ts | 1+
Mpackages/web-util/src/hooks/useNotifications.ts | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mpackages/web-util/src/utils/http-impl.sw.ts | 19+++++++++++++++----
126 files changed, 1121 insertions(+), 855 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/Routing.tsx b/packages/aml-backoffice-ui/src/Routing.tsx @@ -25,7 +25,7 @@ import { Fragment, h, VNode } from "preact"; import { assertUnreachable } from "@gnu-taler/taler-util"; import { useEffect } from "preact/hooks"; -import { HandleAccountNotReady } from "./components/HandleAccountNotReady.js"; +import { HandleSessionNotReady } from "./components/HandleAccountNotReady.js"; import { ExchangeAmlFrame } from "./ExchangeAmlFrame.js"; import { useCurrentDecisionRequest } from "./hooks/decision-request.js"; import { @@ -80,7 +80,7 @@ function PublicRounting(): VNode { switch (location.name) { case undefined: { if (session.state !== "ready") { - return <HandleAccountNotReady officer={session} />; + return <HandleSessionNotReady officer={session} />; } else { return <div />; } diff --git a/packages/aml-backoffice-ui/src/components/CreateAccount.tsx b/packages/aml-backoffice-ui/src/components/CreateAccount.tsx @@ -1,126 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022-2025 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 { - ButtonBetter, - FormDesign, - FormUI, - InternationalizationAPI, - LocalNotificationBanner, - useForm, - useLocalNotificationBetter, - useTranslationContext -} from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; -import { OfficerNotFound } from "../hooks/officer.js"; -import { usePreferences } from "../hooks/preferences.js"; -import { HttpStatusCode, opKnownFailure } from "@gnu-taler/taler-util"; - -type FormType = { - password: string; - repeat: string; -}; - -const TALER_SCREEN_ID = 125; - -const createAccountForm = ( - i18n: InternationalizationAPI, - allowInsecurePassword: boolean, -): FormDesign => ({ - type: "single-column", - fields: [ - { - id: "password", - type: "secret", - label: i18n.str`Password`, - required: true, - validator(value) { - return !value - ? i18n.str`Required` - : allowInsecurePassword - ? undefined - : value.length < 12 - ? i18n.str`should have at least 12 characters` - : !value.match(/[a-z]/) && value.match(/[A-Z]/) - ? i18n.str`should have lowercase and uppercase characters` - : !value.match(/\d/) - ? i18n.str`should have numbers` - : !value.match(/[^a-zA-Z\d]/) - ? i18n.str`should have at least one character which is not a number or letter` - : undefined; - }, - }, - { - id: "repeat", - type: "secret", - label: i18n.str`Repeat password`, - required: true, - validator(value, state) { - return !value - ? i18n.str`Required` - : state.password !== value - ? i18n.str`doesn't match` - : undefined; - }, - }, - ], -}); - -export function CreateAccount({ - officer, -}: { - officer: OfficerNotFound; -}): VNode { - const { i18n } = useTranslationContext(); - const [settings] = usePreferences(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - - const design = createAccountForm(i18n, settings.allowInsecurePassword); - - const { model: handler, status } = useForm<FormType>(design, { - password: undefined, - repeat: undefined, - }); - - const create = safeFunctionHandler( - officer.create, - status.status === "fail" ? undefined : [status.result.password], - ); - - return ( - <div class="flex min-h-full flex-col "> - <LocalNotificationBanner notification={notification} /> - - <div class="sm:mx-auto sm:w-full sm:max-w-md"> - <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> - <i18n.Translate>Create account</i18n.Translate> - </h2> - </div> - - <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> - <FormUI design={design} model={handler} /> - <div class="mt-8"> - <ButtonBetter - type="submit" - class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - onClick={create} - > - <i18n.Translate>Create</i18n.Translate> - </ButtonBetter> - </div> - </div> - </div> - ); -} diff --git a/packages/aml-backoffice-ui/src/components/CreateSession.tsx b/packages/aml-backoffice-ui/src/components/CreateSession.tsx @@ -0,0 +1,126 @@ +/* + This file is part of GNU Taler + (C) 2022-2025 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 { + ButtonBetter, + FormDesign, + FormUI, + InternationalizationAPI, + LocalNotificationBanner, + useForm, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { OfficerNotFound } from "../hooks/officer.js"; +import { usePreferences } from "../hooks/preferences.js"; + +type FormType = { + password: string; + repeat: string; +}; + +const TALER_SCREEN_ID = 125; + +const createAccountForm = ( + i18n: InternationalizationAPI, + allowInsecurePassword: boolean, +): FormDesign => ({ + type: "single-column", + fields: [ + { + id: "password", + type: "secret", + label: i18n.str`Password`, + required: true, + validator(value) { + return !value + ? i18n.str`Required` + : allowInsecurePassword + ? undefined + : value.length < 12 + ? i18n.str`should have at least 12 characters` + : !value.match(/[a-z]/) && value.match(/[A-Z]/) + ? i18n.str`should have lowercase and uppercase characters` + : !value.match(/\d/) + ? i18n.str`should have numbers` + : !value.match(/[^a-zA-Z\d]/) + ? i18n.str`should have at least one character which is not a number or letter` + : undefined; + }, + }, + { + id: "repeat", + type: "secret", + label: i18n.str`Repeat password`, + required: true, + validator(value, state) { + return !value + ? i18n.str`Required` + : state.password !== value + ? i18n.str`doesn't match` + : undefined; + }, + }, + ], +}); + +export function CreateSession({ + officer, +}: { + officer: OfficerNotFound; +}): VNode { + const { i18n } = useTranslationContext(); + const [settings] = usePreferences(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const design = createAccountForm(i18n, settings.allowInsecurePassword); + + const { model: handler, status } = useForm<FormType>(design, { + password: undefined, + repeat: undefined, + }); + + const create = safeFunctionHandler( + i18n.str`create session`, + officer.create, + status.status === "fail" ? undefined : [status.result.password], + ); + + return ( + <div class="flex min-h-full flex-col "> + <LocalNotificationBanner notification={notification} /> + + <div class="sm:mx-auto sm:w-full sm:max-w-md"> + <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> + <i18n.Translate>Create session</i18n.Translate> + </h2> + </div> + + <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> + <FormUI design={design} model={handler} /> + <div class="mt-8"> + <ButtonBetter + type="submit" + class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + onClick={create} + > + <i18n.Translate>Create</i18n.Translate> + </ButtonBetter> + </div> + </div> + </div> + ); +} diff --git a/packages/aml-backoffice-ui/src/components/HandleAccountNotReady.tsx b/packages/aml-backoffice-ui/src/components/HandleAccountNotReady.tsx @@ -16,20 +16,20 @@ import { assertUnreachable } from "@gnu-taler/taler-util"; import { VNode, h } from "preact"; import { OfficerNotReady } from "../hooks/officer.js"; -import { CreateAccount } from "./CreateAccount.js"; -import { UnlockAccount } from "./UnlockAccount.js"; +import { CreateSession } from "./CreateSession.js"; +import { UnlockSession } from "./UnlockSession.js"; -export function HandleAccountNotReady({ +export function HandleSessionNotReady({ officer, }: { officer: OfficerNotReady; }): VNode { if (officer.state === "not-found") { - return <CreateAccount officer={officer}/>; + return <CreateSession officer={officer}/>; } if (officer.state === "locked") { - return <UnlockAccount officer={officer}/>; + return <UnlockSession officer={officer}/>; } assertUnreachable(officer); } diff --git a/packages/aml-backoffice-ui/src/components/MeasureList.tsx b/packages/aml-backoffice-ui/src/components/MeasureList.tsx @@ -53,7 +53,7 @@ export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) { <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is invalid, contact administrator or + This session signature is invalid, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -65,7 +65,7 @@ export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) { <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not known, contact administrator + The designated AML session is not known, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -77,7 +77,7 @@ export function MeasureList({ routeToNew }: { routeToNew: RouteDefinition }) { <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not enabled, contact administrator + The designated AML session is not enabled, contact administrator or create a new one. </i18n.Translate> </Attention> diff --git a/packages/aml-backoffice-ui/src/components/UnlockAccount.tsx b/packages/aml-backoffice-ui/src/components/UnlockAccount.tsx @@ -1,119 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022-2025 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 { - ButtonBetter, - FormDesign, - InputLine, - InternationalizationAPI, - LocalNotificationBanner, - useForm, - useLocalNotificationBetter, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; -import { OfficerLocked } from "../hooks/officer.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -type FormType = { - password: string; -}; - -const TALER_SCREEN_ID = 119; - -const unlockAccountForm = (i18n: InternationalizationAPI): FormDesign => ({ - type: "single-column", - fields: [ - { - id: "password", - type: "text", - label: i18n.str`Password`, - required: true, - }, - ], -}); - -export function UnlockAccount({ officer }: { officer: OfficerLocked }): VNode { - const { i18n } = useTranslationContext(); - - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - - const design = unlockAccountForm(i18n); - - const { model: handler, status } = useForm<FormType>(design, { - password: undefined, - }); - - const unlock = safeFunctionHandler( - officer.tryUnlock, - status.status === "fail" ? undefined : [status.result.password], - ); - unlock.onFail = (fail) => { - switch (fail.case) { - case HttpStatusCode.Forbidden: - return i18n.str`Couldn't unlock the account, the password may be wrong.`; - } - }; - const forget = safeFunctionHandler(async () => officer.forget(), []); - - return ( - <div class="flex min-h-full flex-col "> - <LocalNotificationBanner notification={notification} /> - - <div class="sm:mx-auto sm:w-full sm:max-w-md"> - <h1 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> - <i18n.Translate>Account locked</i18n.Translate> - </h1> - <p class="mt-6 text-lg leading-8 text-gray-600"> - <i18n.Translate> - Your account is normally locked anytime you reload. To unlock type - your password again. - </i18n.Translate> - </p> - </div> - - <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> - <form class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12"> - <div class="mb-4"> - <InputLine - label={i18n.str`Password`} - name="password" - type="password" - required - handler={handler.getHandlerForAttributeKey("password")} - /> - </div> - - <div class="mt-8"> - <ButtonBetter - type="submit" - onClick={unlock} - class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" - > - <i18n.Translate>Unlock</i18n.Translate> - </ButtonBetter> - </div> - </form> - <ButtonBetter - type="button" - onClick={forget} - class="disabled:opacity-50 disabled:cursor-default m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " - > - <i18n.Translate>Forget account</i18n.Translate> - </ButtonBetter> - </div> - </div> - ); -} diff --git a/packages/aml-backoffice-ui/src/components/UnlockSession.tsx b/packages/aml-backoffice-ui/src/components/UnlockSession.tsx @@ -0,0 +1,127 @@ +/* + This file is part of GNU Taler + (C) 2022-2025 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 { + ButtonBetter, + FormDesign, + InputLine, + InternationalizationAPI, + LocalNotificationBanner, + useForm, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { OfficerLocked } from "../hooks/officer.js"; +import { assertUnreachable, HttpStatusCode } from "@gnu-taler/taler-util"; + +type FormType = { + password: string; +}; + +const TALER_SCREEN_ID = 119; + +const unlockAccountForm = (i18n: InternationalizationAPI): FormDesign => ({ + type: "single-column", + fields: [ + { + id: "password", + type: "text", + label: i18n.str`Password`, + required: true, + }, + ], +}); + +export function UnlockSession({ officer }: { officer: OfficerLocked }): VNode { + const { i18n } = useTranslationContext(); + + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const design = unlockAccountForm(i18n); + + const { model: handler, status } = useForm<FormType>(design, { + password: undefined, + }); + + const unlock = safeFunctionHandler( + i18n.str`unlock session`, + officer.tryUnlock, + status.status === "fail" ? undefined : [status.result.password], + ); + unlock.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Forbidden: + return i18n.str`Wrong password.`; + + default: + assertUnreachable(fail.case); + } + }; + const forget = safeFunctionHandler( + i18n.str`forget session`, + async () => officer.forget(), + [], + ); + + return ( + <div class="flex min-h-full flex-col "> + <LocalNotificationBanner notification={notification} /> + + <div class="sm:mx-auto sm:w-full sm:max-w-md"> + <h1 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900"> + <i18n.Translate>Session locked</i18n.Translate> + </h1> + <p class="mt-6 text-lg leading-8 text-gray-600"> + <i18n.Translate> + Your session is normally locked anytime you reload. To unlock type + your password again. + </i18n.Translate> + </p> + </div> + + <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px] "> + <form class="bg-gray-100 px-6 py-6 shadow sm:rounded-lg sm:px-12"> + <div class="mb-4"> + <InputLine + label={i18n.str`Password`} + name="password" + type="password" + required + handler={handler.getHandlerForAttributeKey("password")} + /> + </div> + + <div class="mt-8"> + <ButtonBetter + type="submit" + onClick={unlock} + class="disabled:opacity-50 disabled:cursor-default flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" + > + <i18n.Translate>Unlock</i18n.Translate> + </ButtonBetter> + </div> + </form> + <ButtonBetter + type="button" + onClick={forget} + class="disabled:opacity-50 disabled:cursor-default m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " + > + <i18n.Translate>Forget session</i18n.Translate> + </ButtonBetter> + </div> + </div> + ); +} diff --git a/packages/aml-backoffice-ui/src/context/ui-settings.ts b/packages/aml-backoffice-ui/src/context/ui-settings.ts @@ -60,9 +60,6 @@ export interface UiSettings { // Where libeufin backend is localted // default: window.origin without "webui/" backendBaseURL?: string; - // Shows a button "create random account" in the registration form - // Useful for testing - // default: false signupEmail?: string; } diff --git a/packages/aml-backoffice-ui/src/hooks/account.ts b/packages/aml-backoffice-ui/src/hooks/account.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - OfficerAccount, + OfficerSession, PaytoString, TalerExchangeResultByMethod2, TalerHttpError, @@ -38,14 +38,14 @@ export function useAccountInformation(paytoHash?: string) { const officer = useOfficer(); const session = officer.state === "ready" && paytoHash !== undefined - ? officer.account + ? officer.session : undefined; const { lib: { exchange: api }, } = useExchangeApiContext(); - async function fetcher([officer, account]: [OfficerAccount, PaytoString]) { + async function fetcher([officer, account]: [OfficerSession, PaytoString]) { return await api.getAmlAttributesForAccount(officer, account); } diff --git a/packages/aml-backoffice-ui/src/hooks/decisions.ts b/packages/aml-backoffice-ui/src/hooks/decisions.ts @@ -17,7 +17,7 @@ import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { - OfficerAccount, + OfficerSession, opFixedSuccess, TalerExchangeResultByMethod2, TalerHttpError @@ -50,7 +50,7 @@ export function useAmlAccounts({ open?: boolean; } = {}) { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib: { exchange: api }, } = useExchangeApiContext(); @@ -58,7 +58,7 @@ export function useAmlAccounts({ const [offset, setOffset] = useState<string>(); async function fetcher([officer, offset, investigation, highRisk, open]: [ - OfficerAccount, + OfficerSession, string | undefined, boolean | undefined, boolean | undefined, @@ -108,7 +108,7 @@ export function useCurrentDecisions({ investigated?: boolean; } = {}) { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib: { exchange: api }, } = useExchangeApiContext(); @@ -116,7 +116,7 @@ export function useCurrentDecisions({ const [offset, setOffset] = useState<string>(); async function fetcher([officer, offset, investigation]: [ - OfficerAccount, + OfficerSession, string | undefined, boolean | undefined, ]) { @@ -164,7 +164,7 @@ export function revalidateAccountDecisions() { */ export function useAccountDecisions(accountStr: string) { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib: { exchange: api }, } = useExchangeApiContext(); @@ -172,7 +172,7 @@ export function useAccountDecisions(accountStr: string) { const [offset, setOffset] = useState<string>(); async function fetcher([officer, account, offset]: [ - OfficerAccount, + OfficerSession, string, string | undefined, ]) { @@ -216,14 +216,14 @@ export function useAccountActiveDecision(accountStr?: string) { const officer = useOfficer(); const session = accountStr !== undefined && officer.state === "ready" - ? officer.account + ? officer.session : undefined; const { lib: { exchange: api }, } = useExchangeApiContext(); async function fetcher([officer, account]: [ - OfficerAccount, + OfficerSession, string, string | undefined, ]) { diff --git a/packages/aml-backoffice-ui/src/hooks/legitimizations.ts b/packages/aml-backoffice-ui/src/hooks/legitimizations.ts @@ -17,7 +17,7 @@ import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { - OfficerAccount, + OfficerSession, opFixedSuccess, TalerExchangeResultByMethod2, TalerHttpError, @@ -51,7 +51,7 @@ export function revalidateCurrentLegitimizations() { */ export function useCurrentLegitimizations(accoutnStr: string) { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib: { exchange: api }, } = useExchangeApiContext(); @@ -59,7 +59,7 @@ export function useCurrentLegitimizations(accoutnStr: string) { const [offset, setOffset] = useState<string>(); async function fetcher([officer, account, offset]: [ - OfficerAccount, + OfficerSession, string | undefined, string | undefined, boolean | undefined, diff --git a/packages/aml-backoffice-ui/src/hooks/officer.ts b/packages/aml-backoffice-ui/src/hooks/officer.ts @@ -20,7 +20,7 @@ import { EddsaPrivP, HttpStatusCode, LockedAccount, - OfficerAccount, + OfficerSession, OfficerId, OperationFail, OperationOk, @@ -48,7 +48,7 @@ const DEFAULT_SESSION_DURATION = Duration.fromSpec({ }); export interface Officer { - account: LockedAccount; + session: LockedAccount; when: AbsoluteTime; } @@ -69,7 +69,7 @@ export const codecForOfficerAccount = (): Codec<OfficerAccountString> => export const codecForOfficer = (): Codec<Officer> => buildCodecForObject<Officer>() - .property("account", codecForLockedAccount) + .property("session", codecForLockedAccount) .property("when", codecForAbsoluteTime) .build("Officer"); @@ -86,7 +86,7 @@ export interface OfficerLocked { } export interface OfficerReady { state: "ready"; - account: OfficerAccount; + session: OfficerSession; forget: () => OperationOk<void>; lock: () => OperationOk<void>; expiration: AbsoluteTime; @@ -105,22 +105,22 @@ export function useOfficer(): OfficerState { const [pref] = usePreferences(); pref.keepSessionAfterReload; // dev account, is kept on reloaded. - const accountStorage = useLocalStorage(DEV_ACCOUNT_KEY); - const account = useMemo(() => { - if (!accountStorage.value) return undefined; + const sessionStorage = useLocalStorage(DEV_ACCOUNT_KEY); + const session = useMemo(() => { + if (!sessionStorage.value) return undefined; return { - id: accountStorage.value.id as OfficerId, - signingKey: decodeCrock(accountStorage.value.strKey) as EddsaPrivP, - unlocked: accountStorage.value.unlocked, + id: sessionStorage.value.id as OfficerId, + signingKey: decodeCrock(sessionStorage.value.strKey) as EddsaPrivP, + unlocked: sessionStorage.value.unlocked, }; - }, [accountStorage.value?.id, accountStorage.value?.strKey]); + }, [sessionStorage.value?.id, sessionStorage.value?.strKey]); const officerStorage = useLocalStorage(OFFICER_KEY); const officer = useMemo(() => { if (!officerStorage.value) return undefined; return officerStorage.value; - }, [officerStorage.value?.account, officerStorage.value?.when.t_ms]); + }, [officerStorage.value?.session, officerStorage.value?.when.t_ms]); if (officer === undefined) { return { @@ -134,20 +134,20 @@ export function useOfficer(): OfficerState { extraEntropy, ); officerStorage.update({ - account: safe, + session: safe, when: AbsoluteTime.now(), }); // accountStorage.update({ id, signingKey }); const strKey = encodeCrock(signingKey); - accountStorage.update({ id, strKey, unlocked: AbsoluteTime.now() }); + sessionStorage.update({ id, strKey, unlocked: AbsoluteTime.now() }); return opFixedSuccess(id); }, }; } - if (account === undefined) { + if (session === undefined) { return { state: "locked", forget: () => { @@ -156,9 +156,9 @@ export function useOfficer(): OfficerState { }, tryUnlock: async (pwd: string) => { try { - const ac = await unlockOfficerAccount(officer.account, pwd); + const ac = await unlockOfficerAccount(officer.session, pwd); // accountStorage.update(ac); - accountStorage.update({ + sessionStorage.update({ id: ac.id, strKey: encodeCrock(ac.signingKey), unlocked: AbsoluteTime.now(), @@ -173,21 +173,21 @@ export function useOfficer(): OfficerState { } const expiration = AbsoluteTime.addDuration( - account.unlocked, + session.unlocked, DEFAULT_SESSION_DURATION, ); return { state: "ready", - account, + session: session, expiration, lock: () => { - accountStorage.reset(); + sessionStorage.reset(); return opFixedSuccess(undefined); }, forget: () => { officerStorage.reset(); - accountStorage.reset(); + sessionStorage.reset(); return opFixedSuccess(undefined); }, }; diff --git a/packages/aml-backoffice-ui/src/hooks/server-info.ts b/packages/aml-backoffice-ui/src/hooks/server-info.ts @@ -20,7 +20,7 @@ import { EventReporting_VQF_queries, fetchTopsInfoFromServer, fetchVqfInfoFromServer, - OfficerAccount, + OfficerSession, OperationOk, opFixedSuccess, TalerExchangeResultByMethod2, @@ -35,13 +35,13 @@ const useSWR = _useSWR as unknown as SWRHook; export function useServerMeasures() { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib: { exchange: api }, } = useExchangeApiContext(); - async function fetcher([officer]: [OfficerAccount]) { + async function fetcher([officer]: [OfficerSession]) { return await api.getAmlMeasures(officer); } @@ -67,13 +67,13 @@ export function useServerMeasures() { export function useTopsServerStatistics() { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { unthrottledApi: { exchange: api }, } = useExchangeApiContext(); - async function fetcher([officer]: [OfficerAccount]) { + async function fetcher([officer]: [OfficerSession]) { const final = await fetchTopsInfoFromServer(api, officer); return opFixedSuccess(final); @@ -101,13 +101,13 @@ export function useTopsServerStatistics() { export function useVqfServerStatistics(year: number) { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { unthrottledApi: { exchange: api }, } = useExchangeApiContext(); - async function fetcher([officer, year]: [OfficerAccount, number]) { + async function fetcher([officer, year]: [OfficerSession, number]) { const date = setYear(new Date(), year); const jan_1st = AbsoluteTime.fromMilliseconds(startOfYear(date).getTime()); const dec_31st = AbsoluteTime.fromMilliseconds(endOfYear(date).getTime()); diff --git a/packages/aml-backoffice-ui/src/hooks/transfers.ts b/packages/aml-backoffice-ui/src/hooks/transfers.ts @@ -18,7 +18,7 @@ import { useState } from "preact/hooks"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import { AmountJson, - OfficerAccount, + OfficerSession, PaytoHash, TalerExchangeResultByMethod2, TalerHttpError, @@ -52,7 +52,7 @@ export function revalidateAccountDecisions() { */ export function useTransferDebit() { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib: { exchange: api }, } = useExchangeApiContext(); @@ -60,7 +60,7 @@ export function useTransferDebit() { const [offset, setOffset] = useState<string>(); async function fetcher([officer, offset]: [ - OfficerAccount, + OfficerSession, string, string | undefined, ]) { @@ -104,7 +104,7 @@ export function useTransferList({ account?: PaytoHash; } = {}) { const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib: { exchange: api }, } = useExchangeApiContext(); @@ -112,7 +112,7 @@ export function useTransferList({ const [offset, setOffset] = useState<string>(); async function fetcher([officer, offset, direction, threshold, account]: [ - OfficerAccount, + OfficerSession, string, Direction, AmountJson | undefined, diff --git a/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx b/packages/aml-backoffice-ui/src/pages/AccountDetails.tsx @@ -92,7 +92,7 @@ export function AccountDetails({ const history = useAccountDecisions(account); const legistimizations = useCurrentLegitimizations(account); const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib } = useExchangeApiContext(); const [exported, setExported] = useState<{ content: string; file: string }>(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); @@ -197,7 +197,7 @@ export function AccountDetails({ const time = format(new Date(), "yyyyMMdd_HHmmss"); - const downloadPdf = safeFunctionHandler( + const downloadPdf = safeFunctionHandler(i18n.str`download pdf`, lib.exchange.getAmlAttributesForAccountAsPdf.bind(lib.exchange), session ? [session, account] : undefined, ); @@ -210,6 +210,22 @@ export function AccountDetails({ file: `account_${time}_${account}.pdf`, }); }; + downloadPdf.onFail = (fail) =>{ + switch(fail.case) { + case HttpStatusCode.NoContent: + return i18n.str`The account has no KYC info.` + case HttpStatusCode.Forbidden: + return i18n.str`Invalid session.`; + case HttpStatusCode.NotFound: + return i18n.str`Session not found. Contact the administrator.`; + case HttpStatusCode.Conflict: + return i18n.str`The session is disabled. Contact the administrator.`; + case HttpStatusCode.NotImplemented: + return i18n.str`The server doesn't support PDF download. Contact the administrator.`; + default: + assertUnreachable(fail.case); + } + } return ( <div class="min-w-60"> diff --git a/packages/aml-backoffice-ui/src/pages/AccountList.tsx b/packages/aml-backoffice-ui/src/pages/AccountList.tsx @@ -60,7 +60,7 @@ export function AccountList({ const list = useAmlAccounts({ investigated, open: opened, highRisk }); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib } = useExchangeApiContext(); if (!list) { @@ -77,7 +77,7 @@ export function AccountList({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is invalid, contact administrator or + This session signature is invalid, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -89,7 +89,7 @@ export function AccountList({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not known, contact administrator + The designated AML session is not known, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -101,7 +101,7 @@ export function AccountList({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not enabled, contact administrator + The designated AML session is not enabled, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -168,14 +168,38 @@ export function AccountList({ : `not-investigated`; const time = format(new Date(), "yyyyMMdd_HHmmss"); - const downloadCsv = safeFunctionHandler( + const downloadCsv = safeFunctionHandler(i18n.str`download csv`, lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange), session ? [session, "text/csv"] : undefined, ); - const downloadXls = safeFunctionHandler( + downloadCsv.onFail = (fail) => { + switch(fail.case) { + case HttpStatusCode.NoContent: + return i18n.str`Ther are no accounts in the resultset.` + case HttpStatusCode.Forbidden: + return i18n.str`Invalid session.`; + case HttpStatusCode.NotFound: + return i18n.str`Session not found. Contact the administrator.`; + case HttpStatusCode.Conflict: + return i18n.str`The session is disabled. Contact the administrator`; + } + } + const downloadXls = safeFunctionHandler(i18n.str`download xls`, lib.exchange.getAmlAccountsAsOtherFormat.bind(lib.exchange), session ? [session, "application/vnd.ms-excel"] : undefined, ); + downloadXls.onFail = (fail) => { + switch(fail.case) { + case HttpStatusCode.NoContent: + return i18n.str`Ther are no accounts in the resultset.` + case HttpStatusCode.Forbidden: + return i18n.str`Invalid session.`; + case HttpStatusCode.NotFound: + return i18n.str`Session not found. Contact the administrator.`; + case HttpStatusCode.Conflict: + return i18n.str`The session is disabled. Contact the administrator`; + } + } downloadCsv.onSuccess = (result) => { setExported({ @@ -558,7 +582,7 @@ function JumpByIdForm({ const { i18n } = useTranslationContext(); const [account, setAccount] = useState<string>(""); const officer = useOfficer(); - const session = officer.state === "ready" ? officer.account : undefined; + const session = officer.state === "ready" ? officer.session : undefined; const { lib } = useExchangeApiContext(); const [valid, setValid] = useState(false); const [error, setError] = useState<string>(); diff --git a/packages/aml-backoffice-ui/src/pages/Dashboard.tsx b/packages/aml-backoffice-ui/src/pages/Dashboard.tsx @@ -28,7 +28,7 @@ import { useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; -import { HandleAccountNotReady } from "../components/HandleAccountNotReady.js"; +import { HandleSessionNotReady } from "../components/HandleAccountNotReady.js"; import { useOfficer } from "../hooks/officer.js"; import { usePreferences } from "../hooks/preferences.js"; import { useTopsServerStatistics } from "../hooks/server-info.js"; @@ -38,7 +38,7 @@ export function Dashboard() { const { i18n } = useTranslationContext(); if (officer.state !== "ready") { - return <HandleAccountNotReady officer={officer} />; + return <HandleSessionNotReady officer={officer} />; } return ( diff --git a/packages/aml-backoffice-ui/src/pages/Profile.tsx b/packages/aml-backoffice-ui/src/pages/Profile.tsx @@ -19,7 +19,7 @@ import { } from "@gnu-taler/web-util/browser"; import { h } from "preact"; import { useOfficer } from "../hooks/officer.js"; -import { HandleAccountNotReady } from "../components/HandleAccountNotReady.js"; +import { HandleSessionNotReady } from "../components/HandleAccountNotReady.js"; import { useUiSettingsContext } from "../context/ui-settings.js"; const TALER_SCREEN_ID = 112; @@ -31,7 +31,7 @@ export function Profile() { const { i18n } = useTranslationContext(); if (officer.state !== "ready") { - return <HandleAccountNotReady officer={officer} />; + return <HandleSessionNotReady officer={officer} />; } const url = new URL("./", lib.exchange.baseUrl); @@ -40,23 +40,23 @@ export function Profile() { return ( <div> <h1 class="my-2 text-3xl font-bold tracking-tight text-gray-900 "> - <i18n.Translate>Public key</i18n.Translate> + <i18n.Translate>Session public key</i18n.Translate> </h1> <div class="max-w-xl text-base leading-7 text-gray-700 lg:max-w-lg"> - <p class="mt-6 font-mono break-all">{officer.account.id}</p> + <p class="mt-6 font-mono break-all">{officer.session.id}</p> </div> <p> <a href={`mailto:${signupEmail}?subject=${encodeURIComponent( - "Request AML signup", + "AML activation request", )}&body=${encodeURIComponent( - `I want my AML account\n\n\nPubKey: ${officer.account.id}`, + `Please activate my AML session\n\nSession PubKey: ${officer.session.id}`, )}`} target="_blank" rel="noreferrer" class="m-4 block rounded-md w-fit border-0 px-3 py-2 text-center text-sm bg-indigo-700 text-white shadow-sm hover:bg-indigo-700" > - <i18n.Translate>Request account activation</i18n.Translate> + <i18n.Translate>Request session activation</i18n.Translate> </a> </p> <p> @@ -67,7 +67,7 @@ export function Profile() { }} class="m-4 block rounded-md border-0 bg-gray-200 px-3 py-2 text-center text-sm text-black shadow-sm " > - <i18n.Translate>Lock account</i18n.Translate> + <i18n.Translate>Lock session</i18n.Translate> </button> </p> <p> @@ -78,7 +78,7 @@ export function Profile() { }} class="m-4 block rounded-md bg-red-600 px-3 py-2 text-center text-sm text-white shadow-sm hover:bg-red-500 " > - <i18n.Translate>Forget account</i18n.Translate> + <i18n.Translate>Forget session</i18n.Translate> </button> </p> </div> diff --git a/packages/aml-backoffice-ui/src/pages/Search.tsx b/packages/aml-backoffice-ui/src/pages/Search.tsx @@ -50,7 +50,7 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { HandleAccountNotReady } from "../components/HandleAccountNotReady.js"; +import { HandleSessionNotReady } from "../components/HandleAccountNotReady.js"; import { useAccountDecisions } from "../hooks/decisions.js"; import { useOfficer } from "../hooks/officer.js"; import { ToInvestigateIcon } from "./AccountList.js"; @@ -71,7 +71,7 @@ export function Search({ const [paytoUri, setPayto] = useState<Paytos.URI | undefined>(undefined); if (officer.state !== "ready") { - return <HandleAccountNotReady officer={officer} />; + return <HandleSessionNotReady officer={officer} />; } const design: FormDesign = { @@ -156,7 +156,7 @@ function ShowResult({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is invalid, contact administrator or + This session signature is invalid, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -168,7 +168,7 @@ function ShowResult({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not known, contact administrator + The designated AML session is not known, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -180,7 +180,7 @@ function ShowResult({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not enabled, contact administrator + The designated AML session is not enabled, contact administrator or create a new one. </i18n.Translate> </Attention> diff --git a/packages/aml-backoffice-ui/src/pages/ShowCollectedInfo.tsx b/packages/aml-backoffice-ui/src/pages/ShowCollectedInfo.tsx @@ -65,7 +65,7 @@ export function ShowCollectedInfo({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is invalid, contact administrator or + This session signature is invalid, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -77,7 +77,7 @@ export function ShowCollectedInfo({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not known, contact administrator + The designated AML session is not known, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -89,7 +89,7 @@ export function ShowCollectedInfo({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not enabled, contact administrator + The designated AML session is not enabled, contact administrator or create a new one. </i18n.Translate> </Attention> diff --git a/packages/aml-backoffice-ui/src/pages/Transfers.tsx b/packages/aml-backoffice-ui/src/pages/Transfers.tsx @@ -117,7 +117,7 @@ export function Transfers({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - This account signature is invalid, contact administrator or + This session signature is invalid, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -129,7 +129,7 @@ export function Transfers({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not known, contact administrator + The designated AML session is not known, contact administrator or create a new one. </i18n.Translate> </Attention> @@ -141,7 +141,7 @@ export function Transfers({ <Fragment> <Attention type="danger" title={i18n.str`Operation denied`}> <i18n.Translate> - The designated AML account is not enabled, contact administrator + The designated AML session is not enabled, contact administrator or create a new one. </i18n.Translate> </Attention> diff --git a/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx b/packages/aml-backoffice-ui/src/pages/decision/Summary.tsx @@ -78,7 +78,7 @@ export function Summary({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const session = officer.account; + const session = officer.session; const allMeasures = computeMeasureInformation( !measures || measures instanceof TalerError || measures.type === "fail" ? undefined @@ -169,6 +169,7 @@ export function Summary({ const [submitConfirmation, setSubmitConfirmation] = useState<boolean>(false); const requiresConfirmation = MROS_REPORT_COMPLETED; const submit = safeFunctionHandler( + i18n.str`make aml decision`, async (req) => { if (requiresConfirmation && !submitConfirmation) { setSubmitConfirmation(true); @@ -183,11 +184,11 @@ export function Summary({ submit.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Forbidden: - return i18n.str`Wrong credentials for "${session}"`; + return i18n.str`Invalid credentials.`; case HttpStatusCode.NotFound: - return i18n.str`The account was not found`; + return i18n.str`Session not found. Contact the administrator.`; case HttpStatusCode.Conflict: - return i18n.str`Officer disabled or more recent decision was already submitted.`; + return i18n.str`The session is disabled or a more recent decision was already submitted.`; default: assertUnreachable(fail.case); } diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx @@ -22,7 +22,7 @@ import { useCurrentLocation, useLocalNotificationBetter, useNavigationContext, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -134,6 +134,7 @@ function PublicRounting({ } as TokenRequest; const login = safeFunctionHandler( + i18n.str`login`, (username: string, password: string, challengeIds: string[]) => lib.bank.createAccessToken( username, @@ -164,6 +165,8 @@ function PublicRounting({ return i18n.str`This account is locked. If you have a active session you can change the password or contact the administrator.`; case HttpStatusCode.NotFound: return i18n.str`Account not found`; + default: + assertUnreachable(fail); } }; @@ -214,7 +217,7 @@ function PublicRounting({ <LocalNotificationBanner notification={notification} /> <RegistrationPage onRegistrationSuccesful={(usr, pwd) => { - login.withArgs(usr,pwd, []).call() + login.withArgs(usr, pwd, []).call(); }} routeCancel={publicPages.login} /> diff --git a/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx b/packages/bank-ui/src/pages/ConversionRateClassDetails.tsx @@ -165,7 +165,7 @@ function Form({ ), ); - const deleteClass = safeFunctionHandler( + const deleteClass = safeFunctionHandler(i18n.str`delete conversion rate class`, (token: AccessToken) => lib.bank.deleteConversionRateClass(token, classId), !creds || section !== "delete" || detailsResult.num_users > 0 ? undefined @@ -182,6 +182,8 @@ function Form({ return i18n.str`NotFound`; case HttpStatusCode.NotImplemented: return i18n.str`NotImplemented`; + default: + assertUnreachable(fail); } }; @@ -203,7 +205,7 @@ function Form({ cashout_rounding_mode: status.result.conv.cashout_rounding_mode, }; - const updateClass = safeFunctionHandler( + const updateClass = safeFunctionHandler(i18n.str`update conversion rate class`, lib.bank.updateConversionRateClass.bind(lib.bank), !creds || !input ? undefined : [creds.token, classId, input], ); @@ -222,6 +224,8 @@ function Form({ return i18n.str`Not implemented`; case TalerErrorCode.BANK_NAME_REUSE: return i18n.str`The name of the conversion is already used.`; + default: + assertUnreachable(fail); } }; @@ -903,7 +907,7 @@ function TestConversionClass({ const in_fee = Amounts.parseOrThrow(info.conversion_rate.cashin_fee); const out_fee = Amounts.parseOrThrow(info.conversion_rate.cashout_fee); - const calculate = safeFunctionHandler( + const calculate = safeFunctionHandler(i18n.str`calculate cashout fee`, async (amount: AmountJson) => { const respCashin = await calculateCashinFromDebit(amount, in_fee); if (respCashin.type === "fail") { @@ -938,6 +942,8 @@ function TestConversionClass({ return i18n.str`The amount is malfored`; case TalerErrorCode.GENERIC_CURRENCY_MISMATCH: return i18n.str`The currency is not supported`; + default: + assertUnreachable(fail); } }; diff --git a/packages/bank-ui/src/pages/LoginForm.tsx b/packages/bank-ui/src/pages/LoginForm.tsx @@ -20,6 +20,7 @@ import { HttpStatusCode, TalerErrorCode, TokenRequest, + assertUnreachable, createRFC8959AccessTokenEncoded, } from "@gnu-taler/taler-util"; import { @@ -90,11 +91,16 @@ export function LoginForm({ password: !password ? i18n.str`Missing password` : undefined, }); - const logout = safeFunctionHandler(async () => { - session.logOut(); - return opEmptySuccess(); - }, []); + const logout = safeFunctionHandler( + i18n.str`logout`, + async () => { + session.logOut(); + return opEmptySuccess(); + }, + [], + ); logout.onSuccess = session.logOut; + logout.onFail = (fail) => { return undefined } const tokenRequest = { scope: "readwrite", @@ -103,6 +109,7 @@ export function LoginForm({ } as TokenRequest; const login = safeFunctionHandler( + i18n.str`login`, (username: string, password: string, challengeIds: string[]) => api.createAccessToken( username, @@ -128,13 +135,15 @@ export function LoginForm({ return i18n.str`A second factor authentication is required.`; } case TalerErrorCode.GENERIC_FORBIDDEN: - return i18n.str`You have no permission to this account.`; + return i18n.str`The account has no rights to login.`; case TalerErrorCode.BANK_ACCOUNT_LOCKED: - return i18n.str`You have no permission to this account.`; + return i18n.str`The account is locked and cannot login. Contact administrator.`; case HttpStatusCode.Unauthorized: return i18n.str`Wrong credentials for "${username}"`; case HttpStatusCode.NotFound: return i18n.str`Account not found`; + default: + assertUnreachable(fail); } }; diff --git a/packages/bank-ui/src/pages/NewConversionRateClass.tsx b/packages/bank-ui/src/pages/NewConversionRateClass.tsx @@ -1,5 +1,6 @@ import { AccessToken, + assertUnreachable, HttpStatusCode, TalerCorebankApi, TalerErrorCode, @@ -42,6 +43,7 @@ export function NewConversionRateClass({ >(); const create = safeFunctionHandler( + i18n.str`create conversion rate class`, (token: AccessToken, data: TalerCorebankApi.ConversionRateClassInput) => api.createConversionRateClass(token, data), !submitData || !token ? undefined : [token, submitData], @@ -62,6 +64,8 @@ export function NewConversionRateClass({ return i18n.str`Not implemented`; case TalerErrorCode.BANK_NAME_REUSE: return i18n.str`The name of the conversion is already used.`; + default: + assertUnreachable(fail); } }; diff --git a/packages/bank-ui/src/pages/OperationState/views.tsx b/packages/bank-ui/src/pages/OperationState/views.tsx @@ -84,6 +84,7 @@ export function NeedConfirmationView({ : Amounts.parseOrThrow(config.wire_transfer_fees); const abort = safeFunctionHandler( + i18n.str`abort withdrawal`, (creds: LoggedIn) => bank.abortWithdrawalById(creds, operationId), !creds ? undefined : [creds], ); @@ -96,10 +97,13 @@ export function NeedConfirmationView({ return i18n.str`The operation ID is invalid.`; case HttpStatusCode.NotFound: return i18n.str`The operation was not found.`; + default: + assertUnreachable(fail); } }; const confirm = safeFunctionHandler( + i18n.str`confirm withdrawal`, (creds: LoggedIn, challengeIds: string[]) => bank.confirmWithdrawalById(creds, {}, operationId, { challengeIds }), !creds ? undefined : [creds, []], @@ -129,6 +133,8 @@ export function NeedConfirmationView({ return i18n.str`The starting withdrawal amount and the confirmation amount differs.`; case TalerErrorCode.BANK_AMOUNT_REQUIRED: return i18n.str`The bank requires a bank account which has not been specified yet.`; + default: + assertUnreachable(fail); } }; @@ -309,7 +315,7 @@ export function NeedConfirmationView({ <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0"> <dt class="text-sm font-medium leading-6 text-gray-900"> <i18n.Translate> - Payment Service Provider's account cyclos + Payment Service Provider's account cyclos hostname </i18n.Translate> </dt> @@ -569,6 +575,7 @@ export function ReadyView({ }, []); const abort = safeFunctionHandler( + i18n.str`abort withdrawal`, (creds: LoggedIn) => bank.abortWithdrawalById(creds, operationId), !creds ? undefined : [creds], ); diff --git a/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx b/packages/bank-ui/src/pages/PaytoWireTransferForm.tsx @@ -178,7 +178,7 @@ export function PaytoWireTransferForm({ } const sAmount = sendingAmount; - const send = safeFunctionHandler( + const send = safeFunctionHandler(i18n.str`send transaction`, ( creds: LoggedIn, amount: AmountString, diff --git a/packages/bank-ui/src/pages/QrCodeSection.tsx b/packages/bank-ui/src/pages/QrCodeSection.tsx @@ -15,6 +15,7 @@ */ import { + assertUnreachable, HttpStatusCode, TalerUris, WithdrawUriResult, @@ -59,7 +60,7 @@ export function QrCodeSection({ lib: { bank: api }, } = useBankCoreApiContext(); - const abort = safeFunctionHandler( + const abort = safeFunctionHandler(i18n.str`abort withdrawal`, (creds: UserAndToken) => api.abortWithdrawalById(creds, withdrawUri.withdrawalOperationId), !creds ? undefined : [creds], @@ -74,6 +75,8 @@ export function QrCodeSection({ return i18n.str`The operation was not found.`; case HttpStatusCode.Conflict: return i18n.str`The reserve operation has been confirmed previously and can't be aborted`; + default: + assertUnreachable(fail); } }; diff --git a/packages/bank-ui/src/pages/RegistrationPage.tsx b/packages/bank-ui/src/pages/RegistrationPage.tsx @@ -13,7 +13,7 @@ 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 { HttpStatusCode, TalerErrorCode } from "@gnu-taler/taler-util"; +import { assertUnreachable, HttpStatusCode, TalerErrorCode } from "@gnu-taler/taler-util"; import { ButtonBetter, LocalNotificationBanner, @@ -116,7 +116,7 @@ function RegistrationForm({ password, }; - const register = safeFunctionHandler( + const register = safeFunctionHandler(i18n.str`register new account`, (account: TalerCorebankApi.RegisterAccountRequest) => api.createAccount(undefined, account), !!errors || !reg ? undefined : [reg], @@ -160,6 +160,8 @@ function RegistrationForm({ return i18n.str`The password is too short. Can't have less than 8 characters.`; case TalerErrorCode.BANK_PASSWORD_TOO_LONG: return i18n.str`The password is too long. Can't have more than 64 characters.`; + default: + assertUnreachable(fail); } }; diff --git a/packages/bank-ui/src/pages/SolveMFA.tsx b/packages/bank-ui/src/pages/SolveMFA.tsx @@ -1,5 +1,6 @@ import { AbsoluteTime, + assertUnreachable, Challenge, ChallengeResponse, HttpStatusCode, @@ -73,12 +74,13 @@ function SolveChallenge({ }, []); const doVerification = safeFunctionHandler( + i18n.str`confirm MFA challenge`, (tan: string) => api.confirmChallenge(username, challenge.challenge_id, { tan }), !errors ? [tanCode!] : undefined, ); - doVerification.onFail = (resp) => { - switch (resp.case) { + doVerification.onFail = (fail) => { + switch (fail.case) { case TalerErrorCode.BANK_TRANSACTION_NOT_FOUND: return i18n.str`Unknown challenge.`; case HttpStatusCode.Unauthorized: @@ -89,6 +91,8 @@ function SolveChallenge({ return i18n.str`Wrong authentication number.`; case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: return i18n.str`Expired challenge.`; + default: + assertUnreachable(fail); } }; doVerification.onSuccess = onSolved; @@ -122,8 +126,8 @@ function SolveChallenge({ case TanChannel.SMS: return ( <i18n.Translate> - The verification code sent to the phone number ending - with <b>"{c.tan_info}"</b> + The verification code sent to the phone number ending with{" "} + <b>"{c.tan_info}"</b> </i18n.Translate> ); } @@ -272,8 +276,9 @@ export function SolveMFAChallenges({ ? currentSolved.length === currentChallenge.challenges.length : currentSolved.length > 0; - const sendMessage = safeFunctionHandler((ch: Challenge) => - api.sendChallenge(username, ch.challenge_id), + const sendMessage = safeFunctionHandler( + i18n.str`send MFA challenge`, + (ch: Challenge) => api.sendChallenge(username, ch.challenge_id), ); sendMessage.onSuccess = (success, ch) => { if (success.earliest_retransmission) { @@ -304,19 +309,27 @@ export function SolveMFAChallenges({ return i18n.str`It is too early to request another transmission of the challenge.`; case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return i18n.str`Code transmission failed.`; + default: + assertUnreachable(fail); } }; const complete = onCompleted.withArgs(solved); - const selectChallenge = safeFunctionHandler(async (ch: Challenge) => { - setSelected({ - ch, - expiration: AbsoluteTime.never(), - }); - return opEmptySuccess(); - }); - + const selectChallenge = safeFunctionHandler( + i18n.str`select challenge`, + async (ch: Challenge) => { + setSelected({ + ch, + expiration: AbsoluteTime.never(), + }); + return opEmptySuccess(); + }, + ); + selectChallenge.onFail = (fail) => { + return undefined; + }; + return ( <Fragment> <LocalNotificationBanner notification={notification} /> @@ -375,7 +388,8 @@ export function SolveMFAChallenges({ </span> </h2> {currentChallenge.challenges.map((challenge) => { - const time = retransmission[challenge.challenge_id] ?? AbsoluteTime.now(); + const time = + retransmission[challenge.challenge_id] ?? AbsoluteTime.now(); const alreadySent = !AbsoluteTime.isExpired(time); const noNeedToComplete = hasSolvedEnough || @@ -400,8 +414,7 @@ export function SolveMFAChallenges({ case TanChannel.SMS: return ( <i18n.Translate> - To an phone ending with " - {challenge.tan_info}" + To an phone ending with "{challenge.tan_info}" </i18n.Translate> ); case TanChannel.EMAIL: diff --git a/packages/bank-ui/src/pages/WalletWithdrawForm.tsx b/packages/bank-ui/src/pages/WalletWithdrawForm.tsx @@ -142,7 +142,7 @@ function OldWithdrawalForm({ : undefined, }); - const start = safeFunctionHandler( + const start = safeFunctionHandler(i18n.str`create withdrawal`, (creds: UserAndToken, amount: AmountString) => api.createWithdrawal( creds, diff --git a/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx b/packages/bank-ui/src/pages/WithdrawalConfirmationQuestion.tsx @@ -56,7 +56,7 @@ function useComponentState(opid: string) { const { state: credentials } = useSessionState(); const creds = credentials.status !== "loggedIn" ? undefined : credentials; const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - + const { i18n } = useTranslationContext(); const mfa = useChallengeHandler(); const { @@ -70,6 +70,7 @@ function useComponentState(opid: string) { : Amounts.parseOrThrow(config.wire_transfer_fees); const confirm = safeFunctionHandler( + i18n.str`confirm withdrawal`, (creds: LoggedIn, challengeIds: string[]) => api.confirmWithdrawalById(creds, {}, opid, { challengeIds, @@ -80,16 +81,47 @@ function useComponentState(opid: string) { confirm.onSuccess = () => { mutate(() => true); // clean any info that we have }; + confirm.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Accepted: + case HttpStatusCode.BadRequest: + case HttpStatusCode.NotFound: + case TalerErrorCode.BANK_UNALLOWED_DEBIT: + case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT: + case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: + case TalerErrorCode.BANK_AMOUNT_DIFFERS: + case TalerErrorCode.BANK_AMOUNT_REQUIRED: + return i18n.str`cambiar`; + default: + assertUnreachable(fail); + } + }; const repeat = confirm.lambda((ids: string[]) => { return [confirm.args![0], ids]; }); const abort = safeFunctionHandler( + i18n.str`abort withdrawal`, api.abortWithdrawalById.bind(api), !creds ? undefined : [creds, opid], ); + abort.onSuccess = () => { + mutate(() => true); // clean any info that we have + }; + + abort.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.BadRequest: + case HttpStatusCode.NotFound: + case HttpStatusCode.Conflict: + return i18n.str`cambiar`; + default: + assertUnreachable(fail); + } + }; + const spec = config.currency_specification; return { diff --git a/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/bank-ui/src/pages/account/ShowAccountDetails.tsx @@ -110,6 +110,7 @@ export function ShowAccountDetails({ } const update = safeFunctionHandler( + i18n.str`update account`, ( creds: LoggedIn, account: TalerCorebankApi.AccountReconfiguration, @@ -151,6 +152,8 @@ export function ShowAccountDetails({ return i18n.str`The password is too short. Can't have less than 8 characters.`; case TalerErrorCode.BANK_PASSWORD_TOO_LONG: return i18n.str`The password is too long. Can't have more than 64 characters.`; + default: + assertUnreachable(fail); } }; diff --git a/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx b/packages/bank-ui/src/pages/account/UpdateAccountPassword.tsx @@ -13,7 +13,7 @@ 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 { AccessToken, TalerCorebankApi } from "@gnu-taler/taler-util"; +import { AccessToken, assertUnreachable, TalerCorebankApi } from "@gnu-taler/taler-util"; import { ButtonBetter, LocalNotificationBanner, @@ -94,6 +94,7 @@ export function UpdateAccountPassword({ const mfa = useChallengeHandler(); const update = safeFunctionHandler( + i18n.str`update password`, ( token: AccessToken, request: TalerCorebankApi.AccountPasswordChange, @@ -138,6 +139,8 @@ export function UpdateAccountPassword({ return i18n.str`The password is too short. Can't have less than 8 characters.`; case TalerErrorCode.BANK_PASSWORD_TOO_LONG: return i18n.str`The password is too long. Can't have more than 64 characters.`; + default: + assertUnreachable(fail); } }; const repeatUpdate = update.lambda((ids: string[]) => { diff --git a/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx b/packages/bank-ui/src/pages/admin/CreateNewAccount.tsx @@ -60,6 +60,7 @@ export function CreateNewAccount({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const create = safeFunctionHandler( + i18n.str`create account`, api.createAccount.bind(api), !submitAccount || !token ? undefined : [token, submitAccount], ); diff --git a/packages/bank-ui/src/pages/admin/DownloadStats.tsx b/packages/bank-ui/src/pages/admin/DownloadStats.tsx @@ -20,7 +20,7 @@ import { OperationOk, TalerCoreBankHttpClient, TalerCorebankApi, - opFixedSuccess + opFixedSuccess, } from "@gnu-taler/taler-util"; import { Attention, @@ -29,7 +29,7 @@ import { RouteDefinition, useBankCoreApiContext, useLocalNotificationBetter, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -82,6 +82,7 @@ export function DownloadStats({ routeCancel }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const download = safeFunctionHandler( + i18n.str`download statistics`, async (token) => { setDownloaded(undefined); return fetchAllStatus( @@ -100,6 +101,9 @@ export function DownloadStats({ routeCancel }: Props): VNode { setDownloaded(success); setLastStep(undefined); }; + download.onFail = (fail) => { + return undefined; + }; if (!creds) { return <i18n.Translate>only admin can download stats</i18n.Translate>; diff --git a/packages/bank-ui/src/pages/admin/RemoveAccount.tsx b/packages/bank-ui/src/pages/admin/RemoveAccount.tsx @@ -130,7 +130,7 @@ export function RemoveAccount({ : undefined, }); - const deleteAccount = safeFunctionHandler( + const deleteAccount = safeFunctionHandler(i18n.str`delete account`, (auth: UserAndToken, challengeIds: string[]) => api.deleteAccount(auth, { challengeIds }), !!errors || !token ? undefined : [{ username: account, token }, []], @@ -155,6 +155,8 @@ export function RemoveAccount({ mfa.onChallengeRequired(fail.body); return i18n.str`A second factor authentication is required.`; } + default: + assertUnreachable(fail); } }; diff --git a/packages/bank-ui/src/pages/regional/ConversionConfig.tsx b/packages/bank-ui/src/pages/regional/ConversionConfig.tsx @@ -20,7 +20,7 @@ import { HttpStatusCode, TalerBankConversionApi, TalerError, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { Attention, @@ -173,6 +173,7 @@ function useComponentState({ const out_fee = Amounts.parseOrThrow(info.conversion_rate.cashout_fee); const calculate = safeFunctionHandler( + i18n.str`calculate cashout fee`, async (amount: AmountJson) => { const respCashin = await calculateCashinFromDebit(amount, in_fee); if (respCashin.type === "fail") { @@ -206,6 +207,8 @@ function useComponentState({ return i18n.str`The amount is malfored`; case TalerErrorCode.GENERIC_CURRENCY_MISMATCH: return i18n.str`The currency is not supported`; + default: + assertUnreachable(fail); } }; @@ -224,6 +227,7 @@ function useComponentState({ const cashoutCalc = calculationResult?.cashout; const update = safeFunctionHandler( + i18n.str`update conversion rate`, conversion.updateConversionRate.bind(conversion), !creds || status.status === "fail" ? undefined @@ -239,6 +243,8 @@ function useComponentState({ return i18n.str`Wrong credentials`; case HttpStatusCode.NotImplemented: return i18n.str`Conversion is disabled`; + default: + assertUnreachable(fail); } }; diff --git a/packages/bank-ui/src/pages/regional/CreateCashout.tsx b/packages/bank-ui/src/pages/regional/CreateCashout.tsx @@ -59,10 +59,7 @@ import { import { LoggedIn, useSessionState } from "../../hooks/session.js"; import { TanChannel, undefinedIfEmpty } from "../../utils.js"; import { LoginForm } from "../LoginForm.js"; -import { - InputAmount, - doAutoFocus, -} from "../PaytoWireTransferForm.js"; +import { InputAmount, doAutoFocus } from "../PaytoWireTransferForm.js"; import { SolveMFAChallenges } from "../SolveMFA.js"; const TALER_SCREEN_ID = 127; @@ -270,6 +267,7 @@ function CreateCashoutInternal({ const notZero = Amounts.isNonZero(inputAmount); const conversionCalculator = safeFunctionHandler( + i18n.str`calculate conversion fee`, async (isDebit: boolean, input: AmountJson, fee: AmountJson) => { if (notZero && higerThanMin) { return isDebit @@ -296,6 +294,8 @@ function CreateCashoutInternal({ return i18n.str`The amount is malfored`; case TalerErrorCode.GENERIC_CURRENCY_MISMATCH: return i18n.str`The currency is not supported`; + default: + assertUnreachable(fail); } }; @@ -343,6 +343,7 @@ function CreateCashoutInternal({ const subject = form.subject; const cashout = safeFunctionHandler( + i18n.str`create cashout`, (calc: TransCalc, subject: string, challengeIds: string[]) => api.createCashout( session, @@ -385,6 +386,8 @@ function CreateCashoutInternal({ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED: { return i18n.str`The server doesn't support the current TAN channel.`; } + default: + assertUnreachable(fail); } }; diff --git a/packages/bank-ui/src/settings.json b/packages/bank-ui/src/settings.json @@ -1,12 +1,12 @@ { - "backendBaseURL": "http://bank.taler.test/", + "backendBaseURL": "https://bank.taler/", "allowRandomAccountCreation": true, "fastWithdrawalForm": true, "defaultSuggestedAmount": 11, "bankName": "Taler DEVELOPMENT Bank", "topNavSites": { - "Exchange": "http://Exchnage.taler.test/", - "Bank": "http://bank-ui.taler.test/", - "Merchant": "http://merchant.taler.test/" + "Exchange": "https://Exchnage.taler/", + "Bank": "https://bank.taler/", + "Merchant": "https://merchant.taler/" } } diff --git a/packages/bank-ui/src/utils.ts b/packages/bank-ui/src/utils.ts @@ -126,98 +126,6 @@ export const PAGINATED_LIST_SIZE = 5; // and use it to know if there are more to request export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1; -type Translator = ReturnType<typeof useTranslationContext>["i18n"]; - -export async function withRuntimeErrorHandling<T>( - i18n: Translator, - cb: () => Promise<T>, -): Promise<void> { - try { - await cb(); - } catch (error) { - if (error instanceof TalerError) { - notify(buildRequestErrorMessage(i18n, error)); - } else { - notifyError( - i18n.str`Operation failed, please report`, - (error instanceof Error - ? error.message - : (error)) as TranslatedString, - ); - } - } -} - -export function buildRequestErrorMessage( - i18n: Translator, - cause: TalerError, -): ErrorNotification { - let result: ErrorNotification; - switch (cause.errorDetail.code) { - case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { - result = { - type: "error", - title: i18n.str`Request timeout`, - description: cause.message as TranslatedString, - debug: cause.errorDetail, - when: AbsoluteTime.now(), - }; - break; - } - case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { - result = { - type: "error", - title: i18n.str`Request throttled`, - description: cause.message as TranslatedString, - debug: cause.errorDetail, - when: AbsoluteTime.now(), - }; - break; - } - case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { - result = { - type: "error", - title: i18n.str`Malformed response`, - description: cause.message as TranslatedString, - debug: cause.errorDetail, - when: AbsoluteTime.now(), - }; - break; - } - case TalerErrorCode.WALLET_NETWORK_ERROR: { - result = { - type: "error", - title: i18n.str`Network error`, - description: cause.message as TranslatedString, - debug: cause.errorDetail, - when: AbsoluteTime.now(), - }; - break; - } - case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { - result = { - type: "error", - title: i18n.str`Unexpected request error`, - description: cause.message as TranslatedString, - debug: cause.errorDetail, - when: AbsoluteTime.now(), - }; - break; - } - default: { - result = { - type: "error", - title: i18n.str`Unexpected error`, - description: cause.message as TranslatedString, - debug: cause.errorDetail, - when: AbsoluteTime.now(), - }; - break; - } - } - return result; -} - export const COUNTRY_TABLE = { AE: "U.A.E.", AF: "Afghanistan", diff --git a/packages/challenger-ui/src/pages/AnswerChallenge.tsx b/packages/challenger-ui/src/pages/AnswerChallenge.tsx @@ -131,6 +131,7 @@ export function AnswerChallenge({ const contact = lastStatus?.last_address; const sendAgain = safeFunctionHandler( + i18n.str`create challenge`, lib.challenger.challenge.bind(lib.challenger), contact === undefined || lastStatus === undefined || @@ -158,10 +159,13 @@ export function AnswerChallenge({ return i18n.str`There have been too many attempts to request challenge transmissions.`; case HttpStatusCode.InternalServerError: return i18n.str`Server is unable to respond due to internal problems.`; + default: + assertUnreachable(fail); } }; const check = safeFunctionHandler( + i18n.str`solve challenge`, lib.challenger.solve.bind(lib.challenger), errors !== undefined || lastStatus == undefined || diff --git a/packages/challenger-ui/src/pages/AskChallenge.tsx b/packages/challenger-ui/src/pages/AskChallenge.tsx @@ -35,7 +35,7 @@ import { useChallengerApiContext, useForm, useLocalNotificationBetter, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -162,6 +162,7 @@ export function AskChallenge({ const info = lastStatus.fix_address ? lastStatus.last_address! : contact; const send = safeFunctionHandler( + i18n.str`create challenge`, lib.challenger.challenge.bind(lib.challenger), form.status.errors || !info ? undefined : [session.nonce, info], ); @@ -185,6 +186,8 @@ export function AskChallenge({ return i18n.str`There have been too many attempts to request challenge transmissions.`; case HttpStatusCode.InternalServerError: return i18n.str`Server is unable to respond due to internal problems.`; + default: + assertUnreachable(fail); } }; @@ -369,11 +372,7 @@ export function AskChallenge({ </div> </form> */} <div class="mx-auto mt-4 max-w-xl "> - <FormUI - design={design} - model={form.model} - onSubmit={send.call} - /> + <FormUI design={design} model={form.model} onSubmit={send.call} /> </div> {lastStatus === undefined ? undefined : ( diff --git a/packages/challenger-ui/src/pages/Setup.tsx b/packages/challenger-ui/src/pages/Setup.tsx @@ -15,6 +15,7 @@ */ import { HttpStatusCode, + assertUnreachable, createRFC8959AccessTokenEncoded, encodeCrock, randomBytes, @@ -66,6 +67,7 @@ export function Setup({ }); const doStart = safeFunctionHandler( + i18n.str`setup challenge`, (token: AccessToken, url) => lib.challenger.setup(clientId, token), !!errors || password === undefined || url === undefined ? undefined @@ -87,6 +89,8 @@ export function Setup({ switch (fail.case) { case HttpStatusCode.NotFound: return i18n.str`The server doesn't know about this client. Either the URL or the secret is wrong.`; + default: + assertUnreachable(fail.case); } }; diff --git a/packages/kyc-ui/src/pages/FillForm.tsx b/packages/kyc-ui/src/pages/FillForm.tsx @@ -132,6 +132,7 @@ function ShowForm({ } const submit = safeFunctionHandler( + i18n.str`upload kyc form`, lib.exchange.uploadKycForm.bind(lib.exchange), !validatedForm ? undefined : [reqId, validatedForm], ); diff --git a/packages/kyc-ui/src/pages/Start.tsx b/packages/kyc-ui/src/pages/Start.tsx @@ -18,7 +18,7 @@ import { HttpStatusCode, KycRequirementInformation, TalerError, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { Attention, @@ -171,9 +171,13 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { lib } = useExchangeApiContext(); - const start = safeFunctionHandler(async (id: string) => { - return lib.exchange.startExternalKycProcess(id); - },[req.id!]); + const start = safeFunctionHandler( + i18n.str`start external kyc`, + async (id: string) => { + return lib.exchange.startExternalKycProcess(id); + }, + [req.id!], + ); start.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.NotFound: @@ -182,6 +186,8 @@ function LinkGenerator({ req }: { req: KycRequirementInformation }): VNode { return i18n.str`conflict`; case HttpStatusCode.PayloadTooLarge: return i18n.str`payload is too large`; + default: + assertUnreachable(fail.case); } }; start.onSuccess = (success) => { diff --git a/packages/kyc-ui/src/pages/TriggerKyc.tsx b/packages/kyc-ui/src/pages/TriggerKyc.tsx @@ -99,6 +99,7 @@ export function TriggerKyc({ onKycStarted }: Props): VNode { }, [1]); const send = safeFunctionHandler( + i18n.str`trigger kyc process`, async (balance: AmountString) => { const account = await accountPromise; const limit: WalletKycRequest = { diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx @@ -40,7 +40,7 @@ import { useEffect, useState } from "preact/hooks"; import { SWRConfig } from "swr"; import { Routing } from "./Routing.js"; import { Loading } from "./components/exception/loading.js"; -import { NotificationCard } from "./components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { SessionContextProvider } from "./context/session.js"; import { SettingsProvider } from "./context/settings.js"; import { revalidateInstanceAccessTokens } from "./hooks/access-tokens.js"; @@ -218,7 +218,7 @@ function OnConfigError({ switch (state.type) { case "error": { return ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`Contacting the server failed`, description: state.error.message, @@ -230,7 +230,7 @@ function OnConfigError({ } case "incompatible": { return ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`The server version is not supported`, description: i18n.str`Supported version "${state.supported}", server version "${state.result.version}".`, diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -25,7 +25,7 @@ import { TalerError, TranslatedString, } from "@gnu-taler/taler-util"; -import { urlPattern, useTranslationContext } from "@gnu-taler/web-util/browser"; + import { NotificationCard, urlPattern, useTranslationContext } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; import { Fragment, VNode, h } from "preact"; import { Route, Router, route } from "preact-router"; @@ -34,8 +34,8 @@ import { Loading } from "./components/exception/loading.js"; import { Menu, NotConnectedAppMenu, - NotificationCard, } from "./components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { useSessionContext } from "./context/session.js"; import { useInstanceBankAccounts } from "./hooks/bank.js"; import { useInstanceKYCDetails } from "./hooks/instance.js"; @@ -95,7 +95,6 @@ import { LoginPage } from "./paths/login/index.js"; import { NewAccount } from "./paths/newAccount/index.js"; import { ResetAccount } from "./paths/resetAccount/index.js"; import { Settings } from "./paths/settings/index.js"; -import { Notification } from "./utils/types.js"; const TALER_SCREEN_ID = 3; @@ -190,7 +189,7 @@ export function Routing(_p: Props): VNode { const { i18n } = useTranslationContext(); type GlobalNotifState = - | (Notification & { to: string | undefined }) + | (NotificationCard & { to: string | undefined }) | undefined; const [globalNotification, setGlobalNotification] = useState<GlobalNotifState>(undefined); @@ -290,10 +289,10 @@ export function Routing(_p: Props): VNode { <Fragment> <Menu /> <KycBanner /> - <NotificationCard notification={globalNotification} /> + <NotificationCardBulma notification={globalNotification} /> {error && ( <Fragment> - <NotificationCard + <NotificationCardBulma notification={{ message: "Internal error.", type: "ERROR", @@ -896,7 +895,7 @@ function BankAccountBanner(): VNode { const tomorrow = AbsoluteTime.addDuration(now, oneDay); return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "INFO", message: i18n.str`You need to associate a bank account to receive revenue.`, @@ -947,7 +946,7 @@ function KycBanner(): VNode { const tomorrow = AbsoluteTime.addDuration(now, oneDay); return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "WARN", message: i18n.str`KYC verification needed`, diff --git a/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx @@ -17,7 +17,7 @@ import { TalerError, TalerErrorCode } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { NotificationCard } from "./menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; const TALER_SCREEN_ID = 4; @@ -41,7 +41,7 @@ export function ErrorLoadingMerchant({ if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) { const { requestMethod, requestUrl, timeoutMs } = error.errorDetail; return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "ERROR", message: i18n.str`The request reached a timeout, check your connection.`, @@ -61,7 +61,7 @@ export function ErrorLoadingMerchant({ if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) { const { requestMethod, requestUrl, timeoutMs } = error.errorDetail; return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "ERROR", message: i18n.str`The request was cancelled.`, @@ -83,7 +83,7 @@ export function ErrorLoadingMerchant({ ) { const { requestMethod, requestUrl, timeoutMs } = error.errorDetail; return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "ERROR", message: i18n.str`The request reached a timeout, check your connection.`, @@ -103,10 +103,10 @@ export function ErrorLoadingMerchant({ if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { const { requestMethod, requestUrl, throttleStats } = error.errorDetail; return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "ERROR", - message: i18n.str`Too many requests were made to the server, and this action was throttled.`, + message: i18n.str`Too many requests were made to the server and this action was throttled.`, description: error.message, details: JSON.stringify( { requestMethod, requestUrl, throttleStats }, @@ -126,10 +126,10 @@ export function ErrorLoadingMerchant({ const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail; return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "ERROR", - message: i18n.str`The server's response was malformed.`, + message: i18n.str`The server's response was malformed, please report.`, description: error.message, details: JSON.stringify( { requestMethod, requestUrl, httpStatusCode, validationError }, @@ -146,7 +146,7 @@ export function ErrorLoadingMerchant({ if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) { const { requestMethod, requestUrl } = error.errorDetail; return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "ERROR", message: i18n.str`Could not complete the request due to a network problem.`, @@ -167,10 +167,10 @@ export function ErrorLoadingMerchant({ const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail; return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "ERROR", - message: i18n.str`Unexpected request error.`, + message: i18n.str`Unexpected request error, please report.`, description: error.message, details: JSON.stringify( { requestMethod, requestUrl, httpStatusCode, errorResponse }, @@ -195,7 +195,7 @@ export function ErrorLoadingMerchant({ ////////////////// default: { return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "ERROR", message: i18n.str`Unexpected error.`, @@ -215,7 +215,7 @@ export function ErrorLoadingMerchant({ * TODO: add a better check */ return ( - <NotificationCard + <NotificationCardBulma notification={{ type: "ERROR", message: i18n.str`Unexpected error.`, diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx @@ -92,6 +92,7 @@ function SolveChallenge({ const data = !value.code || !!errors ? undefined : { tan: value.code }; const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const verify = safeFunctionHandler( + i18n.str`verify code`, lib.instance.confirmChallenge.bind(lib.instance), !data ? undefined : [challenge.challenge_id, data], ); @@ -232,8 +233,9 @@ export function SolveMFAChallenges({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const [preferences] = usePreference(); - const sendMessage = safeFunctionHandler((ch: Challenge) => - lib.instance.sendChallenge(ch.challenge_id), + const sendMessage = safeFunctionHandler( + i18n.str`send challenge`, + (ch: Challenge) => lib.instance.sendChallenge(ch.challenge_id), ); sendMessage.onSuccess = (success, ch) => { @@ -273,13 +275,16 @@ export function SolveMFAChallenges({ }; const doComplete = onCompleted.withArgs(solved); - const selectChallenge = safeFunctionHandler(async (ch: Challenge) => { - setSelected({ - ch, - expiration: AbsoluteTime.never(), - }); - return opEmptySuccess(); - }); + const selectChallenge = safeFunctionHandler( + i18n.str`select challenge`, + async (ch: Challenge) => { + setSelected({ + ch, + expiration: AbsoluteTime.never(), + }); + return opEmptySuccess(); + }, + ); if (selected) { return ( @@ -397,8 +402,13 @@ export function SolveMFAChallenges({ <p> <i18n.Translate> You have to wait until{" "} - <span>{format(time.t_ms, datetimeFormatForPreferences(preferences))}</span> - {" "}to receive a new code. + <span> + {format( + time.t_ms, + datetimeFormatForPreferences(preferences), + )} + </span>{" "} + to receive a new code. </i18n.Translate> </p> ) : undefined} diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx @@ -55,6 +55,7 @@ export function JumpToElementById({ const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const checkExist = safeFunctionHandler( + i18n.str`get product details`, (token: AccessToken, id: string) => lib.instance.getProductDetails(token, id), !session.token || !id ? undefined : [session.token, id], @@ -66,7 +67,7 @@ export function JumpToElementById({ return ( <Fragment> - <div class="level" style={{margin:0}}> + <div class="level" style={{ margin: 0 }}> <div class="level-left"> <div class="level-item"> <FormProvider> @@ -77,8 +78,8 @@ export function JumpToElementById({ type="text" value={id ?? ""} onChange={(e) => { - setId(e.currentTarget.value) - if (notification) notification.acknowledge() + setId(e.currentTarget.value); + if (notification) notification.acknowledge(); }} placeholder={placeholder} /> diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx @@ -18,11 +18,11 @@ import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { AdminPaths } from "../../AdminRoutes.js"; import { InstancePaths } from "../../Routing.js"; -import { Notification } from "../../utils/types.js"; import { NavigationBar } from "./NavigationBar.js"; import { Sidebar } from "./SideBar.js"; import { useSessionContext } from "../../context/session.js"; import { + NotificationCardBulma, useNavigationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -205,41 +205,6 @@ interface NotYetReadyAppMenuProps { isPasswordOk: boolean; } -interface NotifProps { - notification?: Notification; -} -export function NotificationCard({ - notification: n, -}: NotifProps): VNode | null { - if (!n) return null; - return ( - <div class="notification"> - <div class="columns is-vcentered"> - <div class="column is-12"> - <article - class={ - n.type === "ERROR" - ? "message is-danger" - : n.type === "WARN" - ? "message is-warning" - : "message is-info" - } - > - <div class="message-header"> - <p>{n.message}</p> - </div> - {n.description && ( - <div class="message-body"> - <div>{n.description}</div> - {n.details && <pre>{n.details}</pre>} - </div> - )} - </article> - </div> - </div> - </div> - ); -} const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; interface NotConnectedAppMenuProps { @@ -276,7 +241,7 @@ export function NotConnectedAppMenu({ > <div>Version {VERSION}</div> {!isTestingEnvironment ? undefined : ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`Testing environment`, description: i18n.str`This server is meant for testing features and configurations. Don't use your personal information here.`, diff --git a/packages/merchant-backoffice-ui/src/components/notifications/index.tsx b/packages/merchant-backoffice-ui/src/components/notifications/index.tsx @@ -20,12 +20,12 @@ */ import { h, VNode } from "preact"; -import { MessageType, Notification } from "../../utils/types.js"; import { assertUnreachable } from "@gnu-taler/taler-util"; +import { MessageType, NotificationCard } from "@gnu-taler/web-util/browser"; interface Props { - notifications: Notification[]; - removeNotification?: (n: Notification) => void; + notifications: NotificationCard[]; + removeNotification?: (n: NotificationCard) => void; } function messageStyle(type: MessageType): string { @@ -54,7 +54,8 @@ export function Notifications({ <article key={i} class={messageStyle(n.type)}> <div class="message-header"> <p>{n.message}</p> - <button type="button" + <button + type="button" class="delete" onClick={() => removeNotification && removeNotification(n)} /> diff --git a/packages/merchant-backoffice-ui/src/hooks/notifications.ts b/packages/merchant-backoffice-ui/src/hooks/notifications.ts @@ -19,26 +19,26 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { NotificationCard } from "@gnu-taler/web-util/browser"; import { useState } from "preact/hooks"; -import { Notification } from "../utils/types.js"; interface Result { - notifications: Notification[]; - pushNotification: (n: Notification) => void; - removeNotification: (n: Notification) => void; + notifications: NotificationCard[]; + pushNotification: (n: NotificationCard) => void; + removeNotification: (n: NotificationCard) => void; } -type NotificationWithDate = Notification & { since: Date }; +type NotificationWithDate = NotificationCard & { since: Date }; export function useNotifications( - initial: Notification[] = [], + initial: NotificationCard[] = [], timeout = 3000, ): Result { const [notifications, setNotifications] = useState<NotificationWithDate[]>( initial.map((i) => ({ ...i, since: new Date() })), ); - const pushNotification = (n: Notification): void => { + const pushNotification = (n: NotificationCard): void => { const entry = { ...n, since: new Date() }; setNotifications((ns) => [...ns, entry]); if (n.type !== "ERROR") @@ -47,7 +47,7 @@ export function useNotifications( }, timeout); }; - const removeNotification = (notif: Notification) => { + const removeNotification = (notif: NotificationCard) => { setNotifications((ns: NotificationWithDate[]) => ns.filter((n) => n !== notif), ); diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -163,6 +163,7 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { }, }; const create = safeFunctionHandler( + i18n.str`create instance and login`, async ( token: AccessToken | undefined, data: TalerMerchantApi.InstanceConfigurationMessage, @@ -244,13 +245,23 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { readonlyId={!!forceId} showId={!forceId} setDefaultPayDelay={() => { - valueHandler( v => ({...v, default_pay_delay: config.default_pay_delay})) + valueHandler((v) => ({ + ...v, + default_pay_delay: config.default_pay_delay, + })); }} setDefaultRefundDelay={() => { - valueHandler( v => ({...v, default_refund_delay: config.default_refund_delay})) + valueHandler((v) => ({ + ...v, + default_refund_delay: config.default_refund_delay, + })); }} setDefaultWireDelay={() => { - valueHandler( v => ({...v, default_wire_transfer_delay: config.default_wire_transfer_delay})) + valueHandler((v) => ({ + ...v, + default_wire_transfer_delay: + config.default_wire_transfer_delay, + })); }} /> <InputPassword<Entity> diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx @@ -73,6 +73,7 @@ export function View({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); const deleteAction = safeFunctionHandler( + i18n.str`delete instance`, ( token: AccessToken, instance: TalerMerchantApi.Instance, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx @@ -43,7 +43,7 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; import { useSessionContext } from "../../../../context/session.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; @@ -122,6 +122,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : Duration.toTalerProtocolDuration(state.duration), }; const create = safeFunctionHandler( + i18n.str`create access token`, ( pwd: string, request: TalerMerchantApi.LoginTokenRequest, @@ -187,7 +188,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { tooltip={i18n.str`Time the access token will be valid.`} /> {state.scope?.endsWith(":refreshable") && ( - <NotificationCard + <NotificationCardBulma notification={{ type: "WARN", message: i18n.str`Refreshable access tokens can pose a security risk!`, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx @@ -77,6 +77,7 @@ export default function AccessTokenListPage({ onCreate }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const deleteToken = safeFunctionHandler( + i18n.str`delete access token`, lib.instance.deleteAccessToken.bind(lib.instance), !session.token || !deleting ? undefined : [session.token, deleting.serial], ); 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 @@ -153,9 +153,10 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); const add = safeFunctionHandler( + i18n.str`add bank account`, (token: AccessToken, request: Entity, challengeIds: string[]) => lib.instance.addBankAccount(token, request, { challengeIds }), - !session.token || hasErrors ? undefined : [session.token, request, []], + !session.token ? undefined : [session.token, request, []], ); add.onSuccess = onCreated; add.onFail = (fail) => { @@ -185,6 +186,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : new URL("./", state.credit_facade_url); const test = safeFunctionHandler( + i18n.str`test revenue api`, testRevenueAPI, !state.credit_facade_credentials || !revenueAPI ? undefined @@ -347,14 +349,18 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { onCancel={() => { setRevenuePayto(undefined); }} - confirm={safeFunctionHandler(async () => { - setState({ - ...state, - payto_uri: Paytos.toFullString(revenuePayto), - }); - setRevenuePayto(undefined); - return opEmptySuccess(); - }, [])} + confirm={safeFunctionHandler( + i18n.str`parse revenue payto`, + async () => { + setState({ + ...state, + payto_uri: Paytos.toFullString(revenuePayto), + }); + setRevenuePayto(undefined); + return opEmptySuccess(); + }, + [], + )} formPayto={safeParsed} testPayto={revenuePayto} /> 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 @@ -27,7 +27,7 @@ import { PaytoType, PaytoUri, succeedOrValue, - TalerMerchantApi + TalerMerchantApi, } from "@gnu-taler/taler-util"; import { LocalNotificationBannerBulma, @@ -59,6 +59,7 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { useState<TalerMerchantApi.BankAccountEntry | null>(null); const remove = safeFunctionHandler( + i18n.str`delete bank account`, lib.instance.deleteBankAccount.bind(lib.instance), !session.token || !deleting ? undefined : [session.token, deleting.h_wire], ); @@ -284,8 +285,9 @@ function Table({ accounts, onDelete, onSelect }: TableProps): VNode { style={{ cursor: "pointer" }} > { - succeedOrValue(Paytos.fromString(acc.payto_uri),{displayName: i18n.str`Invalid payto: "${acc.payto_uri}"`}) - .displayName + succeedOrValue(Paytos.fromString(acc.payto_uri), { + displayName: i18n.str`Invalid payto: "${acc.payto_uri}"`, + }).displayName } </td> <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 @@ -28,7 +28,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { useSessionContext } from "../../../../context/session.js"; import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; import { LoginPage } from "../../../login/index.js"; @@ -68,7 +68,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { return ( <Fragment> {result.body.accounts.length < 1 && ( - <NotificationCard + <NotificationCardBulma notification={{ type: "WARN", message: i18n.str`You must provide a bank account to receive payments.`, 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 @@ -34,7 +34,6 @@ import { ButtonBetterBulma, LocalNotificationBanner, useChallengeHandler, - useCommonPreferences, useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -45,18 +44,18 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; +import { InputPassword } from "../../../../components/form/InputPassword.js"; import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; +import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; import { CompareAccountsModal } from "../../../../components/modal/index.js"; import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; +import { UIElement, usePreference } from "../../../../hooks/preference.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { TestRevenueErrorType, testRevenueAPI } from "../create/index.js"; -import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; -import { UIElement, usePreference } from "../../../../hooks/preference.js"; -import { InputPassword } from "../../../../components/form/InputPassword.js"; const TALER_SCREEN_ID = 36; @@ -208,6 +207,7 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { const mfa = useChallengeHandler(); const update = safeFunctionHandler( + i18n.str`change bank account`, changeBankAccount, !session.token ? undefined @@ -251,6 +251,7 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { : new URL("./", state.credit_facade_url); const test = safeFunctionHandler( + i18n.str`test revenue api`, testRevenueAPI, !revenueAPI || !state.credit_facade_url ? undefined @@ -437,14 +438,18 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { onCancel={() => { setRevenuePayto(undefined); }} - confirm={safeFunctionHandler(async () => { - setState({ - ...state, - payto_uri: Paytos.toFullString(revenuePayto), - }); - setRevenuePayto(undefined); - return opEmptySuccess(); - }, [])} + confirm={safeFunctionHandler( + i18n.str`parse revenue payto`, + async () => { + setState({ + ...state, + payto_uri: Paytos.toFullString(revenuePayto), + }); + setRevenuePayto(undefined); + return opEmptySuccess(); + }, + [], + )} formPayto={safeParsed} testPayto={revenuePayto} /> 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 @@ -20,33 +20,23 @@ */ import { - AccountAddDetails, - BankAccountDetail, - ChallengeResponse, HttpStatusCode, TalerError, TalerMerchantApi, - assertUnreachable, + assertUnreachable } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, - useChallengeHandler, - useLocalNotificationBetter, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; 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 { useBankAccountDetails } from "../../../../hooks/bank.js"; -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"; -import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; export type Entity = TalerMerchantApi.AccountPatchDetails & WithId; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx @@ -19,7 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { assertUnreachable, HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + assertUnreachable, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { ButtonBetterBulma, LocalNotificationBannerBulma, @@ -66,6 +70,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { ? undefined : (state as TalerMerchantApi.CategoryCreateRequest); const create = safeFunctionHandler( + i18n.str`add category`, lib.instance.addCategory.bind(lib.instance), !session.token || !data ? undefined : [session.token, data], ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx @@ -20,12 +20,7 @@ */ import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, 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"; -import { useSessionContext } from "../../../../context/session.js"; -import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; type Entity = TalerMerchantApi.CategoryCreateRequest; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx @@ -61,6 +61,7 @@ export function CardTable({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const remove = safeFunctionHandler( + i18n.str`delete category`, lib.instance.deleteCategory.bind(lib.instance), ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); remove.onSuccess = () => i18n.str`Category deleted`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx @@ -22,18 +22,14 @@ import { HttpStatusCode, TalerError, - TalerMerchantApi, - assertUnreachable, + assertUnreachable } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, 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"; import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; import { useSessionContext } from "../../../../context/session.js"; import { useInstanceCategories } from "../../../../hooks/category.js"; -import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CardTable } from "./Table.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx @@ -99,6 +99,7 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const data = state as TalerMerchantApi.CategoryCreateRequest; const update = safeFunctionHandler( + i18n.str`update category`, lib.instance.updateCategory.bind(lib.instance), !token ? undefined : [token, category.id, data], ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/create/CreatePage.tsx @@ -19,7 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { assertUnreachable, HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + assertUnreachable, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { ButtonBetterBulma, LocalNotificationBannerBulma, @@ -68,6 +72,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : (state as TalerMerchantApi.GroupAddRequest); const create = safeFunctionHandler( + i18n.str`create product group`, lib.instance.createProductGroup.bind(lib.instance), !session.token || !data ? undefined : [session.token, data], ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx @@ -60,7 +60,7 @@ export function CardTable({ const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const remove = safeFunctionHandler( + const remove = safeFunctionHandler(i18n.str`delete product group`, lib.instance.deleteProductGroup.bind(lib.instance), ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); remove.onSuccess = () => i18n.str`Product group deleted`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/UpdatePage.tsx @@ -100,6 +100,7 @@ export function UpdatePage({ group, onUpdated, onBack }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const data = state as TalerMerchantApi.GroupAddRequest; const update = safeFunctionHandler( + i18n.str`update product group`, lib.instance.updateProductGroup.bind(lib.instance), !token ? undefined : [token, group.id, data], ); 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 @@ -189,16 +189,18 @@ export function CreatePage({ wire_transfer_delay: !value.payments?.wire_transfer_delay ? i18n.str`Required` : undefined, - refund_delay: !value.payments?.refund_delay ? i18n.str`Required` : undefined, + refund_delay: !value.payments?.refund_delay + ? i18n.str`Required` + : undefined, pay_delay: !value.payments?.pay_delay ? i18n.str`Required` : undefined, auto_refund_delay: !value.payments?.auto_refund_delay ? undefined : !value.payments?.refund_delay ? i18n.str`Must have a refund deadline` : Duration.cmp( - value.payments.refund_delay, - value.payments.auto_refund_delay, - ) == -1 + value.payments.refund_delay, + value.payments.auto_refund_delay, + ) == -1 ? i18n.str`Auto refund can't be after refund deadline` : undefined, }), @@ -210,15 +212,15 @@ export function CreatePage({ : undefined, fulfillment_message: !!value.shipping?.fulfillment_message && - !!value.shipping?.fulfillment_url + !!value.shipping?.fulfillment_url ? i18n.str`Either fulfillment url or fulfillment message must be specified.` : undefined, fulfillment_url: !!value.shipping?.fulfillment_message && - !!value.shipping?.fulfillment_url + !!value.shipping?.fulfillment_url ? i18n.str`Either fulfillment url or fulfillment message must be specified.` : !!value.shipping?.fulfillment_url && - isInvalidUrl(value.shipping.fulfillment_url) + isInvalidUrl(value.shipping.fulfillment_url) ? i18n.str`Invalid URL` : undefined, }), @@ -235,89 +237,90 @@ export function CreatePage({ const refundDelay = !value.payments?.refund_delay ? Duration.getZero() : Duration.add( - payDelay, - !value.payments?.refund_delay - ? Duration.getZero() - : value.payments.refund_delay, - ); + payDelay, + !value.payments?.refund_delay + ? Duration.getZero() + : value.payments.refund_delay, + ); const autoRefundDelay = !value.payments?.auto_refund_delay ? Duration.getZero() : Duration.add( - payDelay, - !value.payments?.auto_refund_delay - ? Duration.getZero() - : value.payments.auto_refund_delay, - ); + payDelay, + !value.payments?.auto_refund_delay + ? Duration.getZero() + : value.payments.auto_refund_delay, + ); const wireDelay = !value.payments?.wire_transfer_delay ? Duration.getZero() : Duration.add( - refundDelay, - !value.payments?.wire_transfer_delay - ? Duration.getZero() - : value.payments.wire_transfer_delay, - ); + refundDelay, + !value.payments?.wire_transfer_delay + ? Duration.getZero() + : value.payments.wire_transfer_delay, + ); const pay_deadline = !value.payments?.pay_delay ? undefined : AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration(AbsoluteTime.now(), payDelay), - ); + AbsoluteTime.addDuration(AbsoluteTime.now(), payDelay), + ); const refund_deadline = !value.payments?.refund_delay ? undefined : AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration(AbsoluteTime.now(), refundDelay), - ); + AbsoluteTime.addDuration(AbsoluteTime.now(), refundDelay), + ); const wire_transfer_deadline = !value.payments || !value.payments.wire_transfer_delay ? undefined : AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration(AbsoluteTime.now(), wireDelay), - ); + AbsoluteTime.addDuration(AbsoluteTime.now(), wireDelay), + ); const request: undefined | TalerMerchantApi.PostOrderRequest = !value.payments || !value.shipping || !price || !summary ? undefined : { - order: { - version: OrderVersion.V0, - amount: price, - // version: OrderVersion.V1, - // choices: [{ - // amount: price, - // max_fee: value.payments.max_fee as AmountString, - // }], - summary: summary, - products: productList, - extra: undefinedIfEmpty(value.extra), - pay_deadline, - refund_deadline, - wire_transfer_deadline, - auto_refund: value.payments.auto_refund_delay - ? Duration.toTalerProtocolDuration( - value.payments.auto_refund_delay, - ) - : undefined, - delivery_date: value.shipping.delivery_date - ? { t_s: value.shipping.delivery_date.getTime() / 1000 } - : undefined, - delivery_location: value.shipping.delivery_location, - fulfillment_url: value.shipping.fulfillment_url, - fulfillment_message: value.shipping.fulfillment_message, - minimum_age: value.payments.minimum_age, - }, - inventory_products: inventoryList.map((p) => ({ - product_id: p.product.id, - quantity: p.quantity, - })), - create_token: value.payments.createToken, - }; + order: { + version: OrderVersion.V0, + amount: price, + // version: OrderVersion.V1, + // choices: [{ + // amount: price, + // max_fee: value.payments.max_fee as AmountString, + // }], + summary: summary, + products: productList, + extra: undefinedIfEmpty(value.extra), + pay_deadline, + refund_deadline, + wire_transfer_deadline, + auto_refund: value.payments.auto_refund_delay + ? Duration.toTalerProtocolDuration( + value.payments.auto_refund_delay, + ) + : undefined, + delivery_date: value.shipping.delivery_date + ? { t_s: value.shipping.delivery_date.getTime() / 1000 } + : undefined, + delivery_location: value.shipping.delivery_location, + fulfillment_url: value.shipping.fulfillment_url, + fulfillment_message: value.shipping.fulfillment_message, + minimum_age: value.payments.minimum_age, + }, + inventory_products: inventoryList.map((p) => ({ + product_id: p.product.id, + quantity: p.quantity, + })), + create_token: value.payments.createToken, + }; const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const create = safeFunctionHandler( + i18n.str`create order`, lib.instance.createOrder.bind(lib.instance), !session.token || !request ? undefined : [session.token, request], ); 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 @@ -41,7 +41,7 @@ import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputLocation } from "../../../../components/form/InputLocation.js"; import { TextField } from "../../../../components/form/TextField.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { ProductList } from "../../../../components/product/ProductList.js"; import { useSessionContext } from "../../../../context/session.js"; import { @@ -725,7 +725,7 @@ function PaidPage({ </section> {!hasUnconfirmedWireTransfer ? undefined : ( - <NotificationCard + <NotificationCardBulma notification={{ type: "INFO", message: i18n.str`The order was wired.`, 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 @@ -165,8 +165,9 @@ function Table({ const [preferences] = usePreference(); const { state: session, lib, config } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const copyUrl = safeFunctionHandler((token: AccessToken, id: string) => - lib.instance.getOrderDetails(token, id), + const copyUrl = safeFunctionHandler( + i18n.str`copy order URL`, + (token: AccessToken, id: string) => lib.instance.getOrderDetails(token, id), ); copyUrl.onSuccess = (success) => { copyToClipboard(success.order_status_url); @@ -406,6 +407,7 @@ export function RefundModal({ }; const refund = safeFunctionHandler( + i18n.str`authorize refund`, (token: AccessToken, id: string, request: TalerMerchantApi.RefundRequest) => lib.instance.addRefund(token, id, request), !session.token || !req ? undefined : [session.token, id, req], 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 @@ -48,7 +48,10 @@ import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; import { RefundModal } from "./Table.js"; import { InputDate } from "../../../../components/form/InputDate.js"; -import { dateFormatForPreferences, usePreference } from "../../../../hooks/preference.js"; +import { + dateFormatForPreferences, + usePreference, +} from "../../../../hooks/preference.js"; import { format } from "date-fns"; import { DatePicker } from "../../../../components/picker/DatePicker.js"; @@ -68,7 +71,7 @@ export enum OrderListSection { PENDING = "pending", // paid = true, wired = false INCOMING = "incoming", // wired = true, SETTLED = "settled", // wired = true, - ALL = "all", // + ALL = "all", // } function sectionToFilter(s?: OrderListSection): { @@ -114,9 +117,9 @@ export default function OrderList({ const setNewDate = (date?: AbsoluteTime): void => setFilter((prev) => ({ ...prev, date })); - - const result = useInstanceOrders({ ...sectionToFilter(section), ...filter }, (d) => - setFilter({ ...filter, position: d }), + const result = useInstanceOrders( + { ...sectionToFilter(section), ...filter }, + (d) => setFilter({ ...filter, position: d }), ); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { state: session, lib } = useSessionContext(); @@ -143,6 +146,7 @@ export default function OrderList({ const data = {} as TalerMerchantApi.RefundRequest; const refund = safeFunctionHandler( + i18n.str`authorize refund`, lib.instance.addRefund.bind(lib.instance), !session.token || !orderToBeRefunded ? undefined @@ -177,7 +181,13 @@ export default function OrderList({ <section class="section is-main-section"> <LocalNotificationBannerBulma notification={notification} /> - <div style={{ display: "flex", justifyContent: "space-between", width: "100%" }}> + <div + style={{ + display: "flex", + justifyContent: "space-between", + width: "100%", + }} + > <JumpToElementById onSelect={onSelect} description={i18n.str`Jump to order with the given product ID`} @@ -192,10 +202,7 @@ export default function OrderList({ class="button is-fullwidth" onClick={() => setNewDate(undefined)} > - <span - class="icon" - data-tooltip={i18n.str`Clear date filter`} - > + <span class="icon" data-tooltip={i18n.str`Clear date filter`}> <i class="mdi mdi-close" /> </span> </a> @@ -211,9 +218,9 @@ export default function OrderList({ !filter.date || filter.date.t_ms === "never" ? "" : format( - filter.date.t_ms, - dateFormatForPreferences(preferences), - ) + filter.date.t_ms, + dateFormatForPreferences(preferences), + ) } placeholder={i18n.str`Jump to date (${dateFormatForPreferences( preferences, 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 @@ -97,7 +97,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const data = hasErrors ? undefined : (state as TalerMerchantApi.OtpDeviceAddDetails); - const create = safeFunctionHandler( + const create = safeFunctionHandler(i18n.str`add otp device`, lib.instance.addOtpDevice.bind(lib.instance), !session.token || !data ? undefined : [session.token, data], ); 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 @@ -60,7 +60,7 @@ export function CardTable({ const { i18n } = useTranslationContext(); - const remove = safeFunctionHandler( + const remove = safeFunctionHandler(i18n.str`delete otp device`, lib.instance.deleteOtpDevice.bind(lib.instance), ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); 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 @@ -60,6 +60,7 @@ export function UpdatePage({ device, onUpdated, onBack }: Props): VNode { const { state: session, lib } = useSessionContext(); const update = safeFunctionHandler( + i18n.str`update otp device`, lib.instance.updateOtpDevice.bind(lib.instance), !session.token ? undefined : [session.token, device.id, state as Entity], ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx @@ -74,6 +74,7 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { const mfa = useChallengeHandler(); const changePassword = safeFunctionHandler( + i18n.str`change password`, async ( token: AccessToken, current: string, @@ -193,6 +194,7 @@ export function AdminPassword({ const { i18n } = useTranslationContext(); const mfa = useChallengeHandler(); const changePassword = safeFunctionHandler( + i18n.str`change instance password`, async ( token: AccessToken, id: string, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/create/CreatePage.tsx @@ -66,7 +66,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const data = !!errors ? undefined : (state as TalerMerchantApi.PotAddRequest); - const create = safeFunctionHandler( + const create = safeFunctionHandler(i18n.str`create money pot`, lib.instance.createMoneyPot.bind(lib.instance), !session.token || !data ? undefined : [session.token, data], ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx @@ -60,7 +60,7 @@ export function CardTable({ const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const remove = safeFunctionHandler( + const remove = safeFunctionHandler(i18n.str`delete money pot`, lib.instance.deleteMoneyPot.bind(lib.instance), ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); remove.onSuccess = () => i18n.str`Money pot deleted`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/update/UpdatePage.tsx @@ -108,7 +108,7 @@ export function UpdatePage({ moneyPot, onUpdated, onBack }: Props): VNode { : undefined, }); - const update = safeFunctionHandler( + const update = safeFunctionHandler(i18n.str`update money pot`, lib.instance.updateMoneyPot.bind(lib.instance), !token || errors ? undefined : [token, moneyPot.id, data], ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx @@ -53,9 +53,11 @@ export interface Props { export function CreatePage({ onCreate, onBack }: Props): VNode { const { state: session, lib } = useSessionContext(); const [form, setForm] = useState<TalerMerchantApi.ProductAddDetailRequest>(); + const { i18n } = useTranslationContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const create = safeFunctionHandler( + i18n.str`add product`, lib.instance.addProduct.bind(lib.instance), !session.token || !form ? undefined : [session.token, form], ); @@ -79,7 +81,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { } }; - const { i18n } = useTranslationContext(); const potsResult = useInstanceMoneyPots(); const groupsResults = useInstanceProductGroups(); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -28,7 +28,7 @@ import { } from "@gnu-taler/taler-util"; import { useLocalNotificationBetter, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { ComponentChildren, Fragment, VNode, h } from "preact"; @@ -151,6 +151,7 @@ function Table({ const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const update = safeFunctionHandler( + i18n.str`update product`, lib.instance.updateProduct.bind(lib.instance), ); update.onSuccess = () => rowSelectionHandler(undefined); 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 @@ -59,7 +59,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { i18n } = useTranslationContext(); - const remove = safeFunctionHandler( + const remove = safeFunctionHandler(i18n.str`delete product`, lib.instance.deleteProduct.bind(lib.instance), !session.token || !deleting ? undefined : [session.token, deleting.id], ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx @@ -53,10 +53,13 @@ interface Props { export function UpdatePage({ product, onBack, onConfirm }: Props): VNode { const { state: session, lib } = useSessionContext(); - const [form, setForm] = useState<TalerMerchantApi.ProductPatchDetailRequest>(); + const [form, setForm] = + useState<TalerMerchantApi.ProductPatchDetailRequest>(); + const { i18n } = useTranslationContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const update = safeFunctionHandler( + i18n.str`update product`, lib.instance.updateProduct.bind(lib.instance), !session.token || !form ? undefined @@ -76,8 +79,6 @@ export function UpdatePage({ product, onBack, onConfirm }: Props): VNode { } }; - const { i18n } = useTranslationContext(); - // FIXME: if the category list is big the will bring a lot of info // we could find a lazy way to add up on searches const categoriesResult = useInstanceCategories(); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/create/CreatePage.tsx @@ -42,7 +42,7 @@ import { InputSelector } from "../../../../components/form/InputSelector.js"; import { useSessionContext } from "../../../../context/session.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { assert } from "console"; -import { NotificationCard } from "../../../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; const TALER_SCREEN_ID = 37; @@ -93,7 +93,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const data = !!errors ? undefined : (state as TalerMerchantApi.ReportAddRequest); - const create = safeFunctionHandler( + const create = safeFunctionHandler(i18n.str`create scheduled report`, lib.instance.createScheduledReport.bind(lib.instance), !session.token || !data ? undefined : [session.token, data], ); @@ -110,7 +110,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { }; if (noGenerators) { return ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`No report generator configured in the server`, description: i18n.str`Contant the system administrator to create a report generator before scheduling one.`, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx @@ -63,6 +63,7 @@ export function CardTable({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const remove = safeFunctionHandler( + i18n.str`delete scheduled report`, lib.instance.deleteScheduledReport.bind(lib.instance), ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); remove.onSuccess = () => i18n.str`Scheduled report deleted`; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/update/UpdatePage.tsx @@ -39,7 +39,7 @@ import { import { Input } from "../../../../components/form/Input.js"; import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; @@ -126,6 +126,7 @@ export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const data = state as TalerMerchantApi.ReportAddRequest; const update = safeFunctionHandler( + i18n.str`update scheduled report`, lib.instance.updateScheduledReport.bind(lib.instance), !token || hasErrors ? undefined : [token, report.id, data], ); @@ -143,7 +144,7 @@ export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { if (noGenerators) { return ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`No report generator configured in the server`, description: i18n.str`Contant the system administrator to create a report generator before scheduling one.`, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/OrdersChart.tsx b/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/OrdersChart.tsx @@ -43,7 +43,7 @@ import { import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { LoginPage } from "../../../login/index.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { LineCanvas } from "../../../../components/ChartJS.js"; const TALER_SCREEN_ID = 58; @@ -90,7 +90,7 @@ export function OrdersChart({ return <NotFoundPageOrAdminCreate />; case HttpStatusCode.BadGateway: return ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`Bad gateway`, type: "ERROR", @@ -100,7 +100,7 @@ export function OrdersChart({ case HttpStatusCode.ServiceUnavailable: return ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`Service unavailable`, type: "ERROR", diff --git a/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/RevenueChart.tsx b/packages/merchant-backoffice-ui/src/paths/instance/statistics/list/RevenueChart.tsx @@ -40,7 +40,7 @@ import { FormProvider } from "../../../../components/form/FormProvider.js"; import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { MerchantRevenueStatsSlug, useInstanceStatisticsAmount, @@ -107,7 +107,7 @@ export function RevenueChart({ return <NotFoundPageOrAdminCreate />; case HttpStatusCode.BadGateway: return ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`Bad gateway`, type: "ERROR", @@ -117,7 +117,7 @@ export function RevenueChart({ case HttpStatusCode.ServiceUnavailable: return ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`Service unavailable`, type: "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 @@ -180,6 +180,7 @@ export function CreatePage({ }; const create = safeFunctionHandler( + i18n.str`add template`, lib.instance.addTemplate.bind(lib.instance), !session.token || hasErrors ? undefined : [session.token, data], ); 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 @@ -84,6 +84,7 @@ export default function ListTemplates({ } const remove = safeFunctionHandler( + i18n.str`delete template`, lib.instance.deleteTemplate.bind(lib.instance), !session.token || !deleting ? undefined 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 @@ -50,7 +50,7 @@ import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { TextField } from "../../../../components/form/TextField.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; @@ -227,6 +227,7 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { otp_id: state.otpId!, }; const update = safeFunctionHandler( + i18n.str`update template`, lib.instance.updateTemplate.bind(lib.instance), !session.token || !!errors ? undefined : [session.token, template.id, data], ); @@ -268,7 +269,7 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { </section> <hr /> {unsupportedCurrency ? ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`The template configuration needs to be fixed.`, description: i18n.str`The currency of the template is ${template_currency} and is not in the list of supported currencies.`, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx @@ -91,6 +91,7 @@ export function UsePage({ summary: template.template_contract.summary ? undefined : state.summary, }; const useTemplate = safeFunctionHandler( + i18n.str`create order from template`, lib.instance.useTemplateCreateOrder.bind(lib.instance), !!errors ? undefined : [id, details], ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx @@ -98,6 +98,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { }; const create = safeFunctionHandler( + i18n.str`create token family`, lib.instance.createTokenFamily.bind(lib.instance), !session.token || !!errors ? undefined : [session.token, value as Entity], ); @@ -145,10 +146,10 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { name="valid_after" label={i18n.str`Start Date`} tooltip={i18n.str`The first day the coupon/subscription can be used.`} - help={i18n.str`If set to ${(format( + help={i18n.str`If set to ${format( firstDayNextMonth, dateFormatForPreferences(preferences), - ))}, it cannot be used before this date.`} + )}, it cannot be used before this date.`} withTimestampSupport /> 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 @@ -78,6 +78,7 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { } const remove = safeFunctionHandler( + i18n.str`delete token family`, lib.instance.deleteTokenFamily.bind(lib.instance), !session.token || !deleting ? undefined : [session.token, deleting.slug], ); 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 @@ -80,6 +80,7 @@ export function UpdatePage({ onUpdated, onBack, tokenFamily }: Props) { const hasErrors = errors !== undefined; const update = safeFunctionHandler( + i18n.str`update token family`, lib.instance.updateTokenFamily.bind(lib.instance), !session.token || !!errors ? undefined diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -92,6 +92,7 @@ export default function ListTransfer({}: Props): VNode { }, [shoulUseDefaultAccount]); const confirm = safeFunctionHandler( + i18n.str`inform wire transfer`, lib.instance.informWireTransfer.bind(lib.instance), !session.token || !selected ? undefined @@ -191,10 +192,7 @@ export default function ListTransfer({}: Props): VNode { <div class="columns"> <div class="column" /> <div class="column is-10"> - <FormProvider - object={form} - valueHandler={setForm} - > + <FormProvider object={form} valueHandler={setForm}> <InputSelector name="payto_uri" label={i18n.str`Bank account`} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx @@ -25,11 +25,14 @@ import { LocalNotificationBannerBulma, useChallengeHandler, useLocalNotificationBetter, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { FormErrors, FormProvider } from "../../../components/form/FormProvider.js"; +import { + FormErrors, + FormProvider, +} from "../../../components/form/FormProvider.js"; import { Input } from "../../../components/form/Input.js"; import { InputToggle } from "../../../components/form/InputToggle.js"; import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; @@ -73,6 +76,7 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode { const mfa = useChallengeHandler(); const remove = safeFunctionHandler( + i18n.str`delete current instance`, (token: AccessToken, purge: boolean, challengeIds: string[]) => lib.instance.deleteCurrentInstance(token, { purge, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -134,6 +134,7 @@ export function UpdatePage({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); const update = safeFunctionHandler( + i18n.str`update instance settings`, ( token: AccessToken, d: TalerMerchantApi.InstanceReconfigurationMessage, 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 @@ -95,6 +95,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const data = state as TalerMerchantApi.WebhookAddDetails; const create = safeFunctionHandler( + i18n.str`add webhook`, lib.instance.addWebhook.bind(lib.instance), !session.token || hasErrors ? undefined : [session.token, data], ); 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 @@ -64,6 +64,7 @@ export function CardTable({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const deleteWebhook = safeFunctionHandler( + i18n.str`delete webhook`, lib.instance.deleteWebhook.bind(lib.instance), ); deleteWebhook.onSuccess = () => i18n.str`Webhook deleted successfully`; 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 @@ -95,6 +95,7 @@ export function UpdatePage({ webhook, onConfirm, onBack }: Props): VNode { const data = state as Entity; const update = safeFunctionHandler( + i18n.str`update webhook`, lib.instance.updateWebhook.bind(lib.instance), !session.token || !!errors ? undefined : [session.token, webhook.id, data], ); diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -77,6 +77,7 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { const mfa = useChallengeHandler(); const login = safeFunctionHandler( + i18n.str`create access token`, (usr: string, pwd: string, challengeIds: string[]) => { const api = getInstanceForUsername(usr); return api.createAccessToken( diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx @@ -198,6 +198,7 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { }; const create = safeFunctionHandler( + i18n.str`self provision instance`, (req: InstanceConfigurationMessage, challengeIds: string[]) => lib.instance.createInstanceSelfProvision(req, { challengeIds, diff --git a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx @@ -22,7 +22,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { Link, route } from "preact-router"; -import { NotificationCard } from "../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { cleanAllCache, DEFAULT_ADMIN_USERNAME, @@ -51,7 +51,7 @@ export function NotFoundPageOrAdminCreate(): VNode { if (state.isAdmin && state.instance === DEFAULT_ADMIN_USERNAME) { return ( <Fragment> - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`No 'admin' instance configured yet.`, description: i18n.str`Create an 'admin' instance to begin using the merchant backoffice.`, diff --git a/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx @@ -88,6 +88,7 @@ export function ResetAccount({ const mfa = useChallengeHandler(); const reset = safeFunctionHandler( + i18n.str`reset password for self provision`, async (password: string, challengeIds: string[]) => { const forgot = await lib .subInstanceApi(instanceId) diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx @@ -26,7 +26,7 @@ import { FormProvider, } from "../../components/form/FormProvider.js"; import { InputSelector } from "../../components/form/InputSelector.js"; -import { NotificationCard } from "../../components/menu/index.js"; +import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; import { LangSelector } from "../../components/menu/LangSelector.js"; import { useSessionContext } from "../../context/session.js"; import { @@ -169,7 +169,7 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { </FormProvider> </div> {value.persona === "tester" ? ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`Testing features`, description: i18n.str`Only use beta-tester mode if you know how the application works. Features enabled in this mode requires more work.`, @@ -178,7 +178,7 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { /> ) : undefined} {value.developer ? ( - <NotificationCard + <NotificationCardBulma notification={{ message: i18n.str`Developer mode`, description: i18n.str`Only use developer mode if you know what you are doing. Tools enabled in this mode are intended to fix problems or get more information about the runtime. YOU MAY LOSE DATA.`, diff --git a/packages/merchant-backoffice-ui/src/utils/types.ts b/packages/merchant-backoffice-ui/src/utils/types.ts @@ -20,12 +20,5 @@ export interface KeyValue { [key: string]: string; } -export interface Notification { - message: string; - description?: string | VNode; - details?: string | VNode | string; - type: MessageType; -} export type ValueOrFunction<T> = T | ((p: T) => T); -export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS"; diff --git a/packages/taler-harness/src/harness/tops.ts b/packages/taler-harness/src/harness/tops.ts @@ -32,8 +32,8 @@ import { LimitOperationType, MerchantAccountKycRedirectsResponse, MerchantAccountKycStatus, - OfficerAccount, OfficerId, + OfficerSession, parsePaytoUriOrThrow, succeedOrThrow, TalerCorebankApiClient, @@ -577,7 +577,7 @@ export interface TopsTestEnv { exchangeApi: TalerExchangeHttpClient; wireGatewayApi: TalerWireGatewayHttpClient; merchantApi: TalerMerchantInstanceHttpClient; - officerAcc: OfficerAccount; + officerAcc: OfficerSession; bank: BankServiceHandle; } @@ -764,7 +764,7 @@ export async function createTopsEnvironment( httpClient: harnessHttpLib, }); - const officerAcc: OfficerAccount = { + const officerAcc: OfficerSession = { id: amlKeypair.pub as OfficerId, signingKey: decodeCrock(amlKeypair.priv), }; @@ -1011,7 +1011,7 @@ export interface MeasuresTestEnvironment { challengerPostal: TestfakeChallengerService; challengerSms: TestfakeChallengerService; accountPaytoHash: string; - officerAcc: OfficerAccount; + officerAcc: OfficerSession; exchangeClient: TalerExchangeHttpClient; } @@ -1236,7 +1236,7 @@ export function isFrozen(decision: AmlDecision): boolean { async function doTriggerReset( t: GlobalTestState, args: { - officerAcc: OfficerAccount; + officerAcc: OfficerSession; exchangeClient: TalerExchangeHttpClient; merchantPaytoHash: string; }, @@ -1298,7 +1298,7 @@ async function doTriggerReset( async function doTriggerMeasure( t: GlobalTestState, args: { - officerAcc: OfficerAccount; + officerAcc: OfficerSession; exchangeClient: TalerExchangeHttpClient; merchantPaytoHash: string; measure: string; diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-postal.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-postal.ts @@ -19,19 +19,16 @@ */ import { AccessToken, - decodeCrock, encodeCrock, hashNormalizedPaytoUri, j2s, KycStatusLongPollingReason, Logger, - OfficerAccount, - OfficerId, parsePaytoUriOrThrow, succeedOrThrow, TalerExchangeHttpClient, TalerMerchantInstanceHttpClient, - TalerProtocolTimestamp, + TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { startFakeChallenger } from "../harness/fake-challenger.js"; import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-sms.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-sms.ts @@ -25,7 +25,7 @@ import { j2s, KycStatusLongPollingReason, Logger, - OfficerAccount, + OfficerSession, OfficerId, parsePaytoUriOrThrow, succeedOrThrow, @@ -162,7 +162,7 @@ export async function runTopsAmlCustomAddrSmsTest(t: GlobalTestState) { logger.info(`kyc status after accept-tos: ${j2s(kycStatus)}`); } - const officerAcc: OfficerAccount = { + const officerAcc: OfficerSession = { id: amlKeypair.pub as OfficerId, signingKey: decodeCrock(amlKeypair.priv), }; diff --git a/packages/taler-util/src/aml/reporting.ts b/packages/taler-util/src/aml/reporting.ts @@ -1,6 +1,6 @@ import { TalerExchangeHttpClient } from "../http-client/exchange-client.js"; import { AbsoluteTime, Duration } from "../time.js"; -import { OfficerAccount } from "../types-taler-common.js"; +import { OfficerSession } from "../types-taler-common.js"; import { TOPS_AmlEventsName } from "./events.js"; /** @@ -346,7 +346,7 @@ function safeAdd( export async function fetchTopsInfoFromServer( api: TalerExchangeHttpClient, - officer: OfficerAccount, + officer: OfficerSession, ) { type EventType = typeof EventReporting_TOPS_queries; const eventList = Object.entries(EventReporting_TOPS_queries); @@ -375,7 +375,7 @@ export async function fetchTopsInfoFromServer( export async function fetchVqfInfoFromServer( api: TalerExchangeHttpClient, - officer: OfficerAccount, + officer: OfficerSession, jan_1st: AbsoluteTime, dec_31st: AbsoluteTime, ) { diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -45,7 +45,7 @@ import { EddsaPublicKeyString, EddsaSignatureString, LongPollParams, - OfficerAccount, + OfficerSession, PaginationParams, } from "../types-taler-common.js"; import { @@ -863,7 +863,7 @@ export class TalerExchangeHttpClient { * */ async getAmlMeasures( - auth: OfficerAccount, + auth: OfficerSession, ): Promise< | OperationOk<AvailableMeasureSummary> | OperationFail< @@ -898,7 +898,7 @@ export class TalerExchangeHttpClient { * */ async getAmlKycStatistics( - auth: OfficerAccount, + auth: OfficerSession, names: string[], filter: { since?: AbsoluteTime; @@ -949,7 +949,7 @@ export class TalerExchangeHttpClient { * */ async getAmlAccounts( - auth: OfficerAccount, + auth: OfficerSession, params: PaginationParams & { highRisk?: boolean; open?: boolean; @@ -999,7 +999,7 @@ export class TalerExchangeHttpClient { * */ async getAmlAccountsAsOtherFormat( - auth: OfficerAccount, + auth: OfficerSession, mime: "text/csv" | "application/vnd.ms-excel" | "application/json", params: { highRisk?: boolean; @@ -1058,7 +1058,7 @@ export class TalerExchangeHttpClient { * */ async getAmlDecisions( - auth: OfficerAccount, + auth: OfficerSession, params: PaginationParams & { account?: PaytoHash; active?: boolean; @@ -1114,7 +1114,7 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-legitimizations */ async getAmlLegitimizations( - officer: OfficerAccount, + officer: OfficerSession, params: PaginationParams & { account?: PaytoHash; active?: boolean; @@ -1155,7 +1155,7 @@ export class TalerExchangeHttpClient { * */ async getAmlAttributesForAccount( - auth: OfficerAccount, + auth: OfficerSession, account: string, params: PaginationParams = {}, ): Promise< @@ -1196,7 +1196,7 @@ export class TalerExchangeHttpClient { * */ async getAmlAttributesForAccountAsPdf( - auth: OfficerAccount, + auth: OfficerSession, account: string, params: PaginationParams = {}, ) { @@ -1218,6 +1218,7 @@ export class TalerExchangeHttpClient { } case HttpStatusCode.NoContent: case HttpStatusCode.Forbidden: + case HttpStatusCode.NotImplemented: case HttpStatusCode.NotFound: case HttpStatusCode.Conflict: return opKnownHttpFailure(resp.status, resp); @@ -1231,7 +1232,7 @@ export class TalerExchangeHttpClient { * */ async makeAmlDesicion( - auth: OfficerAccount, + auth: OfficerSession, decision: Omit<AmlDecisionRequest, "officer_sig">, ) { const body: AmlDecisionRequest = { @@ -1272,7 +1273,7 @@ export class TalerExchangeHttpClient { * */ async getTransfersCredit( - auth: OfficerAccount, + auth: OfficerSession, params: PaginationParams & { threshold?: AmountJson; account?: PaytoHash; @@ -1323,7 +1324,7 @@ export class TalerExchangeHttpClient { * */ async getTransfersDebit( - auth: OfficerAccount, + auth: OfficerSession, params: PaginationParams & { threshold?: AmountJson; account?: PaytoHash; @@ -1374,7 +1375,7 @@ export class TalerExchangeHttpClient { * */ async getTransfersKycAuth( - auth: OfficerAccount, + auth: OfficerSession, params: PaginationParams & { threshold?: AmountJson; account?: PaytoHash; diff --git a/packages/taler-util/src/http-client/officer-account.ts b/packages/taler-util/src/http-client/officer-account.ts @@ -18,7 +18,7 @@ import { EddsaPrivP, EncryptionNonceP, LockedAccount, - OfficerAccount, + OfficerSession, OfficerId, ReserveAccount, createEddsaKeyPair, @@ -43,7 +43,7 @@ import { export async function unlockOfficerAccount( account: LockedAccount, password: string, -): Promise<OfficerAccount> { +): Promise<OfficerSession> { const rawKey = decodeCrock(account); const rawPassword = stringToBytes(password); @@ -73,7 +73,7 @@ export async function unlockOfficerAccount( export async function createNewOfficerAccount( password: string, extraNonce: EncryptionNonceP, -): Promise<OfficerAccount & { safe: LockedAccount }> { +): Promise<OfficerSession & { safe: LockedAccount }> { const { eddsaPriv, eddsaPub } = createEddsaKeyPair(); const key = stringToBytes(password); diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -519,7 +519,7 @@ export type OfficerId = string & { [opaque_OfficerId]: true }; declare const opaque_OfficerSigningKey: unique symbol; -export interface OfficerAccount { +export interface OfficerSession { id: OfficerId; signingKey: EddsaPrivP; } diff --git a/packages/web-util/src/components/Attention.tsx b/packages/web-util/src/components/Attention.tsx @@ -4,7 +4,7 @@ import { ComponentChildren, Fragment, VNode, h } from "preact"; interface Props { type?: "info" | "success" | "warning" | "danger" | "low", onClose?: () => void, - title: TranslatedString, + title: TranslatedString | VNode, children?: ComponentChildren, timeout?: Duration, } diff --git a/packages/web-util/src/components/ErrorLoadingMerchant.tsx b/packages/web-util/src/components/ErrorLoadingMerchant.tsx @@ -105,7 +105,7 @@ export function ErrorLoading({ error }: { error: TalerError }): VNode { return ( <Attention type="danger" - title={i18n.str`Too many requests were made to the server, and this action was throttled.`} + title={i18n.str`Too many requests were made to the server and this action was throttled.`} > {error.message} {showDebugInfo && ( @@ -177,7 +177,7 @@ export function ErrorLoading({ error }: { error: TalerError }): VNode { const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail; return ( - <Attention type="danger" title={i18n.str`Unexpected request error`}> + <Attention type="danger" title={i18n.str`The server's response was unexpected.`}> {error.message} {showDebugInfo && ( <pre class="whitespace-break-spaces "> diff --git a/packages/web-util/src/components/NotificationBanner.tsx b/packages/web-util/src/components/NotificationBanner.tsx @@ -3,8 +3,10 @@ import { DebugInfo, Notification, useCommonPreferences, + useTranslationContext, } from "../index.browser.js"; import { Attention } from "./Attention.js"; +import { useState, useTransition } from "preact/compat"; export function LocalNotificationBanner({ notification, @@ -56,9 +58,11 @@ export function LocalNotificationBannerBulma({ }: { notification?: Notification; }): VNode { + const { i18n } = useTranslationContext(); + const [{ showDebugInfo }] = useCommonPreferences(); + const [moreInfo, setMoreInfo] = useState(showDebugInfo); if (!notification) return <Fragment />; const msg = notification.message; - const [{ showDebugInfo }] = useCommonPreferences(); switch (msg.type) { case "error": return ( @@ -85,9 +89,32 @@ export function LocalNotificationBannerBulma({ onClick={() => notification.acknowledge()} /> </div> - {msg.description && ( + {msg.description && msg.description.length && ( <div class="message-body"> - <div>{msg.description}</div> + {moreInfo ? ( + msg.description.map((d) => { + return <div>{d}</div>; + }) + ) : ( + <div>{msg.description[0]}</div> + )} + {moreInfo ? ( + <a + onClick={() => setMoreInfo(false)} + type="button" + style={{ justifySelf: "right", color: "gray" }} + > + <i18n.Translate>show less info</i18n.Translate> + </a> + ) : ( + <a + onClick={() => setMoreInfo(true)} + type="button" + style={{ justifySelf: "right", color: "gray" }} + > + <i18n.Translate>show more info</i18n.Translate> + </a> + )} {showDebugInfo && msg.debug && ( <pre> {JSON.stringify(msg.debug, undefined, 2)}</pre> )} diff --git a/packages/web-util/src/components/NotificationCardBulma.tsx b/packages/web-util/src/components/NotificationCardBulma.tsx @@ -0,0 +1,45 @@ +import { h, VNode } from "preact"; + +export type MessageType = "INFO" | "WARN" | "ERROR" | "SUCCESS"; +export interface NotificationCard { + message: string; + description?: string | VNode; + details?: string | VNode | string; + type: MessageType; +} + + +interface Props { + notification?: NotificationCard; +} + +export function NotificationCardBulma({ + notification: n, +}: Props): VNode | null { + if (!n) return null; + return ( + <div class="notification"> + <div class="columns is-vcentered"> + <div class="column is-12"> + <article + class={n.type === "ERROR" + ? "message is-danger" + : n.type === "WARN" + ? "message is-warning" + : "message is-info"} + > + <div class="message-header"> + <p>{n.message}</p> + </div> + {n.description && ( + <div class="message-body"> + <div>{n.description}</div> + {n.details && <pre>{n.details}</pre>} + </div> + )} + </article> + </div> + </div> + </div> + ); +} diff --git a/packages/web-util/src/components/index.ts b/packages/web-util/src/components/index.ts @@ -14,3 +14,4 @@ export * from "./ToastBanner.js"; export * from "./Time.js"; export * from "./RenderAmount.js"; export * from "./Pagination.js"; +export * from "./NotificationCardBulma.js"; diff --git a/packages/web-util/src/hooks/useNotifications.ts b/packages/web-util/src/hooks/useNotifications.ts @@ -16,6 +16,8 @@ import { memoryMap, useTranslationContext, } from "../index.browser.js"; +import { VNode } from "preact"; +import { format } from "date-fns"; export type NotificationMessage = ErrorNotification | InfoNotification; @@ -24,8 +26,9 @@ export interface ErrorNotification { title: TranslatedString; ack?: boolean; timeout?: boolean; - description?: TranslatedString; + description?: TranslatedString[]; debug?: any; + actions?: {}; when: AbsoluteTime; } export interface InfoNotification { @@ -73,7 +76,7 @@ export function notifyError( notify({ type: "error" as const, title, - description, + description: description ? [description] : undefined, debug, when: AbsoluteTime.now(), }); @@ -82,7 +85,7 @@ export function notifyException(title: TranslatedString, ex: Error) { notify({ type: "error" as const, title, - description: ex.message as TranslatedString, + description: [ex.message as TranslatedString], debug: ex.stack, when: AbsoluteTime.now(), }); @@ -148,38 +151,6 @@ function hash(msg: NotificationMessage): string { return hashCode(str); } -function translateTalerError( - cause: TalerError, - i18n: InternationalizationAPI, -): TranslatedString { - switch (cause.errorDetail.code) { - case TalerErrorCode.GENERIC_TIMEOUT: { - return i18n.str`Request timeout`; - } - case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: { - return i18n.str`Request cancelled`; - } - case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { - return i18n.str`Request timeout`; - } - case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { - return i18n.str`Request throttled`; - } - case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { - return i18n.str`Malformed response`; - } - case TalerErrorCode.WALLET_NETWORK_ERROR: { - return i18n.str`Network error`; - } - case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { - return i18n.str`Unexpected request error`; - } - default: { - return i18n.str`Unexpected error`; - } - } -} - /** * A function that may fail and return a message to be shown * as a notification @@ -207,6 +178,7 @@ type ReplaceReturnType<T extends (...a: any) => any, TNewReturn> = ( export function useLocalNotificationBetter(): [ Notification | undefined, <Args extends any[], R extends OperationResult<any, any>>( + opName: TranslatedString, doAction: (...args: Args) => Promise<R>, args?: Args, ) => SafeHandlerTemplate<Args, R>, @@ -228,6 +200,7 @@ export function useLocalNotificationBetter(): [ Args extends any[], R extends OperationResult<any, any>, >( + opName: TranslatedString, doAction: (...args: Args) => Promise<R>, args?: Args, ): SafeHandlerTemplate<Args, R> { @@ -254,22 +227,22 @@ export function useLocalNotificationBetter(): [ // @ts-expect-error r.withArgs = (...args: D) => { const d = converter(...args); - if (!d) return thiz + if (!d) return thiz; const e = thiz.withArgs(...d); return e; }; /** * FIXME: there is a problem with this - * + * * adding onSuccess function after creating the lambda makes the withArgs * build handlers without onSuccess. consider this - * + * * const h = safeHandler(handler).lambda((param) -> .. ) * h.onSuccess = () => i18n.str`ok` * const button = h.withArgs(p); - * + * * button.call() - * + * * the onSuccess function is undefined when button is clicked. * But not if the lambda is created after the onSuccess assignment */ @@ -292,7 +265,7 @@ export function useLocalNotificationBetter(): [ case "fail": { const error = thiz.onFail(resp as any, ...thiz.args); if (error) { - save(failWithTitle(i18n, resp, error, thiz.args)); + save(failWithTitle(i18n, opName, resp, error, thiz.args)); } return; } @@ -303,12 +276,16 @@ export function useLocalNotificationBetter(): [ } catch (error: unknown) { // This functions should not throw, this is a problem. logBugForDevelopers(error); - onUnexpected(i18n, save)(error, thiz.args); + onUnexpected( + i18n, + i18n.str`Unexpected error trying to ${opName}`, + save, + )(error, thiz.args); return; } }, onFail: (fail, ...rest) => - i18n.str`Unhandled failure, please report. Code ${fail.case}`, + i18n.str`Unhandled failure trying to ${opName}. Code ${fail.case}`, onSuccess: () => undefined, }; return thiz; @@ -326,23 +303,131 @@ export function logBugForDevelopers(error: unknown) { ); } +function describeErrorResponse( + i18n: InternationalizationAPI, + errorResponse: { code?: number; hint?: string }, +): TranslatedString | undefined { + if (!errorResponse.code) return undefined; + switch (errorResponse.code) { + case TalerErrorCode.GENERIC_JSON_INVALID: + return i18n.str`Looks like the JSON in the request was malformed.`; + default: + return errorResponse.hint as TranslatedString | undefined; + } +} + +function notUndefined<T>(t: T | undefined): t is T { + return !!t; +} + +function translateTalerError( + cause: TalerError, + i18n: InternationalizationAPI, +): TranslatedString[] { + if ( + cause.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT) || + cause.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT) + ) { + return [ + i18n.str`The request reached a timeout, check your connection.`, + i18n.str`The ${cause.errorDetail.requestMethod} request to ${ + cause.errorDetail.requestUrl + } failed after ${cause.errorDetail.timeoutMs / 1000} seconds.`, + cause.errorDetail.when + ? i18n.str`The last request time is ${AbsoluteTime.stringify( + cause.errorDetail.when, + )}` + : undefined, + ].filter(notUndefined); + } + if (cause.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) { + return [ + i18n.str`The request was cancelled.`, + i18n.str`The ${cause.errorDetail.requestMethod} request ${cause.errorDetail.requestUrl} failed with code ${cause.errorDetail.httpStatusCode}.`, + cause.errorDetail.when + ? i18n.str`The request was made at ${AbsoluteTime.stringify( + cause.errorDetail.when, + )}` + : undefined, + ].filter(notUndefined); + } + if (cause.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { + return [ + i18n.str`Too many requests were made to the server and this action was throttled.`, + i18n.str`The request "${cause.errorDetail.requestMethod} ${cause.errorDetail.requestUrl}" failed with an code ${cause.errorDetail.httpStatusCode}`, + cause.errorDetail.when + ? i18n.str`The last request time is ${AbsoluteTime.stringify( + cause.errorDetail.when, + )}` + : undefined, + ].filter(notUndefined); + } + if (cause.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) { + return [ + i18n.str`The server's response was malformed.`, + i18n.str`The response to "${cause.errorDetail.requestMethod} ${cause.errorDetail.requestUrl}" failed with an code ${cause.errorDetail.httpStatusCode}`, + cause.errorDetail.when + ? i18n.str`The request was made at ${AbsoluteTime.stringify( + cause.errorDetail.when, + )}` + : undefined, + cause.errorDetail.contentType + ? i18n.str`The content type is ${cause.errorDetail.contentType}` + : undefined, + cause.errorDetail.validationError + ? i18n.str`The validation error is "${cause.errorDetail.validationError}"` + : undefined, + cause.errorDetail.response + ? (cause.errorDetail.response as TranslatedString) + : undefined, + ].filter(notUndefined); + } + if (cause.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) { + return [ + i18n.str`Due to a network problem the request could not be finished.`, + i18n.str`The ${cause.errorDetail.requestMethod} request to ${cause.errorDetail.requestUrl} failed.`, + cause.errorDetail.when + ? i18n.str`The request was made at ${AbsoluteTime.stringify( + cause.errorDetail.when, + )}` + : undefined, + ].filter(notUndefined); + } + if (cause.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) { + const hint = + "hint" in cause.errorDetail.errorResponse + ? cause.errorDetail.errorResponse.hint + : undefined; + return [ + i18n.str`The server's response was unexpected. This mean the client and the server are not on sync about the protocol.`, + i18n.str`The ${cause.errorDetail.requestMethod} request to ${cause.errorDetail.requestUrl} failed with code ${cause.errorDetail.httpStatusCode}`, + cause.errorDetail.when + ? i18n.str`The request was made at ${AbsoluteTime.stringify( + cause.errorDetail.when, + )}` + : undefined, + describeErrorResponse(i18n, cause.errorDetail.errorResponse), + hint ? i18n.str`And the server say: "${hint}"` : undefined, + ].filter(notUndefined); + } + return [i18n.str`Unexpected error`, cause.message as TranslatedString]; +} + function onUnexpected( i18n: InternationalizationAPI, + title: TranslatedString, save: (m: NotificationMessage) => void, ): (cause: unknown, args: any[]) => void { return (error, args) => { if (error instanceof TalerError) { save({ - title: translateTalerError(error, i18n), + title, type: "error", - description: - error && error.errorDetail.hint - ? (error.errorDetail.hint as TranslatedString) - : undefined, + description: translateTalerError(error, i18n), debug: { error, stack: error instanceof Error ? error.stack : undefined, - // args: sanitizeFunctionArguments(args), + args: sanitizeFunctionArguments(args), when: AbsoluteTime.now(), }, when: AbsoluteTime.now(), @@ -353,13 +438,15 @@ function onUnexpected( ) as TranslatedString; save({ - title: i18n.str`Operation failed.`, + title, type: "error", - description: i18n.str`Unexpected error, this is likely a bug. Please report `, + description: [ + i18n.str`Unexpected error, this is likely a bug. Please report `, + ], debug: { error: String(error), stack: error instanceof Error ? error.stack : undefined, - // args: sanitizeFunctionArguments(args), + args: sanitizeFunctionArguments(args), when: AbsoluteTime.now(), }, when: AbsoluteTime.now(), @@ -372,7 +459,7 @@ function sanitizeFunctionArguments(args: any[]): string { return args .map((d) => typeof d === "string" && d.startsWith("secret-token:") - ? "<session>" + ? "secret-token:...redacted..." : typeof d === "object" ? JSON.stringify(d, undefined, 2) : d, @@ -419,14 +506,15 @@ function successWithTitle(title: TranslatedString): NotificationMessage { function failWithTitle( i18n: InternationalizationAPI, + opName: TranslatedString, fail: OperationFail<any>, description: TranslatedString, args: any[], ): NotificationMessage { return { - title: i18n.str`The operation failed.`, + title: i18n.str`Unable to ${opName}.`, type: "error", - description, + description: [description], debug: { detail: fail.detail, case: fail.case, diff --git a/packages/web-util/src/utils/http-impl.sw.ts b/packages/web-util/src/utils/http-impl.sw.ts @@ -156,8 +156,11 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary { text, bytes: async () => (await response.blob()).arrayBuffer(), }; - } catch (e) { - if (controller.signal) { + } catch (e: unknown) { + if (!(e instanceof Error)) { + throw e; + } + if (controller.signal.aborted) { throw TalerError.fromDetail( controller.signal.reason, { @@ -166,10 +169,18 @@ export class BrowserFetchHttpLib implements HttpRequestLibrary { timeoutMs: requestTimeout.d_ms === "forever" ? 0 : requestTimeout.d_ms, }, - `HTTP request failed.`, + `HTTP request aborted: ${e.message}`, + ); + } else { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_NETWORK_ERROR, + { + requestUrl, + requestMethod, + }, + `HTTP request failed: ${e.message}`, ); } - throw e; } } }