taler-typescript-core

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

commit 7d8483255847c5f492d902db45d82cb80e74ced8
parent e09939cfe18f7fe29b8da45c8f5b68ffcf8c0e25
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Mon,  1 Jun 2026 11:17:58 -0300

several fixes and improvements

better error reporting (user can copy the content)
normalize safeHandler and MFA
simplify button
only one notificaton handler
prevent printing sensitive info

Diffstat:
Mpackages/merchant-backoffice-ui/src/Application.tsx | 193++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/merchant-backoffice-ui/src/Routing.tsx | 78++++++++++++++----------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/components/SolveMFA.tsx | 126+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mpackages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx | 45+++++++++++++++++++++++++++++----------------
Mpackages/merchant-backoffice-ui/src/components/menu/index.tsx | 12++++++------
Mpackages/merchant-backoffice-ui/src/components/modal/index.tsx | 23+++++++++++------------
Apackages/merchant-backoffice-ui/src/context/challenge.ts | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/merchant-backoffice-ui/src/context/session.ts | 2+-
Mpackages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx | 58++++++++++++++++++++--------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/admin/list/View.tsx | 52+++++++++++++++++++---------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create-pos/CreatePage.tsx | 53+++++++++++++++++++----------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx | 53+++++++++++++++++++----------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx | 21++++++++++-----------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 145++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx | 2++
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx | 21++++++++++-----------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 80++++++++++++++++++++++++++++++-------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx | 24+++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx | 32++++++++++++++++----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx | 25+++++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/create/CreatePage.tsx | 24+++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx | 31+++++++++++++++----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/groups/list/UpdatePage.tsx | 25+++++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 30+++++++++++++-----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx | 46+++++++++++++++++++++++++---------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx | 19++++++++-----------
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx | 24+++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx | 1-
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx | 32++++++++++++++++----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx | 1-
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx | 26+++++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx | 1-
Mpackages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx | 11++++++-----
Mpackages/merchant-backoffice-ui/src/paths/instance/password/index.tsx | 93+++++++++++++++++++++++++++----------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/create/CreatePage.tsx | 26++++++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx | 31+++++++++++++++----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/pots/update/UpdatePage.tsx | 25+++++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx | 24+++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx | 20+++++++++++++-------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx | 19++++++++-----------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx | 24+++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/create/CreatePage.tsx | 24+++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx | 31+++++++++++++++----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/reports/update/UpdatePage.tsx | 24+++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx | 27+++++++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx | 19++++++++-----------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx | 26+++++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx | 28+++++++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx | 4+---
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx | 26+++++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx | 21++++++++++-----------
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx | 24+++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/DetailsPage.tsx | 26++++++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx | 23++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx | 53+++++++++++++++++++----------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx | 53+++++++++++++++++++----------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx | 24+++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx | 32+++++++++++++++-----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx | 26+++++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 65+++++++++++++++++++++++------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/newAccount/index.tsx | 79+++++++++++++++++--------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx | 56++++++++++++++++++--------------------------------------
Mpackages/merchant-backoffice-ui/src/scss/main.scss | 5+++++
63 files changed, 1126 insertions(+), 1285 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx @@ -21,7 +21,6 @@ import { CacheEvictor, - TalerMerchantApi, TalerMerchantInstanceCacheEviction, TalerMerchantManagementCacheEviction, assertUnreachable, @@ -29,18 +28,21 @@ import { } from "@gnu-taler/taler-util"; import { BrowserHashNavigationProvider, - ConfigResultFail, MerchantApiProvider, + NotificationCardBulma, + NotificationProvider, TalerWalletIntegrationBrowserProvider, + ToastBannerBulma, TranslationProvider, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { SWRConfig } from "swr"; import { Routing } from "./Routing.js"; import { Loading } from "./components/exception/loading.js"; -import { NotificationCardBulma } from "@gnu-taler/web-util/browser"; +import { MerchantChallengeHandlerProvider } from "./context/challenge.js"; +import { CurrenciesProvider } from "./context/currency.js"; import { SessionContextProvider } from "./context/session.js"; import { SettingsProvider } from "./context/settings.js"; import { revalidateInstanceAccessTokens } from "./hooks/access-tokens.js"; @@ -52,6 +54,7 @@ import { revalidateCategoryDetails, revalidateInstanceCategories, } from "./hooks/category.js"; +import { revalidateInstanceGroups } from "./hooks/groups.js"; import { revalidateBackendInstances, revalidateInstanceDetails, @@ -67,9 +70,14 @@ import { revalidateOtpDeviceDetails, } from "./hooks/otp.js"; import { + revalidateInstanceMoneyPots, + revalidateMoneyPotDetails, +} from "./hooks/pots.js"; +import { revalidateInstanceProducts, revalidateProductDetails, } from "./hooks/product.js"; +import { revalidateInstanceScheduledReports } from "./hooks/reports.js"; import { revalidateInstanceTemplates, revalidateTemplateDetails, @@ -79,6 +87,10 @@ import { revalidateTokenFamilyDetails, } from "./hooks/tokenfamily.js"; import { + revalidateInstanceConfirmedTransfers, + revalidateInstanceIncomingTransfers, +} from "./hooks/transfer.js"; +import { revalidateInstanceWebhooks, revalidateWebhookDetails, } from "./hooks/webhooks.js"; @@ -88,17 +100,6 @@ import { buildDefaultBackendBaseURL, fetchSettings, } from "./settings.js"; -import { - revalidateInstanceConfirmedTransfers, - revalidateInstanceIncomingTransfers, -} from "./hooks/transfer.js"; -import { - revalidateInstanceMoneyPots, - revalidateMoneyPotDetails, -} from "./hooks/pots.js"; -import { revalidateInstanceGroups } from "./hooks/groups.js"; -import { revalidateInstanceScheduledReports } from "./hooks/reports.js"; -import { CurrenciesProvider } from "./context/currency.js"; const TALER_SCREEN_ID = 2; const WITH_LOCAL_STORAGE_CACHE = false; @@ -114,53 +115,78 @@ export function Application(): VNode { return ( <SettingsProvider value={settings}> <TranslationProvider source={strings}> - <MerchantApiProvider - baseUrl={new URL("./", baseUrl)} - frameOnError={OnConfigError} - evictors={{ - management: swrCacheEvictor, - }} - > - <SessionContextProvider> - <SWRConfig - value={{ - provider: WITH_LOCAL_STORAGE_CACHE - ? localStorageProvider - : undefined, - // normally, do not revalidate - revalidateOnFocus: false, - revalidateOnReconnect: false, - revalidateIfStale: false, - revalidateOnMount: undefined, - focusThrottleInterval: undefined, + <NotificationProvider> + <SubApp baseUrl={baseUrl} /> + </NotificationProvider> + </TranslationProvider> + </SettingsProvider> + ); +} - // normally, do not refresh - refreshInterval: undefined, - dedupingInterval: 2000, - refreshWhenHidden: false, - refreshWhenOffline: false, +function SubApp({ baseUrl }: { baseUrl: string }) { + return ( + <MerchantApiProvider + baseUrl={new URL("./", baseUrl)} + frameOnError={OnConfigError} + evictors={{ + management: swrCacheEvictor, + }} + > + <SWRConfig + value={{ + provider: WITH_LOCAL_STORAGE_CACHE ? localStorageProvider : undefined, + // normally, do not revalidate + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + revalidateOnMount: undefined, + focusThrottleInterval: undefined, + + // normally, do not refresh + refreshInterval: undefined, + dedupingInterval: 2000, + refreshWhenHidden: false, + refreshWhenOffline: false, - // ignore errors - shouldRetryOnError: false, - errorRetryCount: 0, - errorRetryInterval: undefined, + // ignore errors + shouldRetryOnError: false, + errorRetryCount: 0, + errorRetryInterval: undefined, - // do not go to loading again if already has data - keepPreviousData: true, - }} - > - <TalerWalletIntegrationBrowserProvider> - <BrowserHashNavigationProvider> - <CurrenciesProvider> + // do not go to loading again if already has data + keepPreviousData: true, + }} + > + <MerchantChallengeHandlerProvider> + <SessionContextProvider> + <TalerWalletIntegrationBrowserProvider> + <BrowserHashNavigationProvider> + <CurrenciesProvider> + <ToastBannerBulma/> + {/* <ShowNotificationsDialog> */} <Routing /> - </CurrenciesProvider> - </BrowserHashNavigationProvider> - </TalerWalletIntegrationBrowserProvider> - </SWRConfig> + {/* </ShowNotificationsDialog> */} + </CurrenciesProvider> + </BrowserHashNavigationProvider> + </TalerWalletIntegrationBrowserProvider> </SessionContextProvider> - </MerchantApiProvider> - </TranslationProvider> - </SettingsProvider> + </MerchantChallengeHandlerProvider> + </SWRConfig> + </MerchantApiProvider> + ); +} + +export function ShowNotificationsDialog({ + children, +}: { + children: ComponentChildren; +}): VNode { + console.log("asdasd") + return ( + <Fragment> + <ToastBannerBulma /> + <Fragment key="childs">{children}</Fragment> + </Fragment> ); } @@ -208,44 +234,21 @@ function localStorageProvider(): Map<unknown, unknown> { return map; } -function OnConfigError({ - state, -}: { - state: ConfigResultFail<TalerMerchantApi.MerchantVersionResponse> | undefined; -}): VNode { +function OnConfigError({ children }: { children: ComponentChildren }): VNode { const { i18n } = useTranslationContext(); - if (!state) { - return ( - <i18n.Translate>checking compatibility with server...</i18n.Translate> - ); - } - switch (state.type) { - case "error": { - return ( - <NotificationCardBulma - notification={{ - message: i18n.str`Contacting the server failed`, - description: state.error.message, - details: JSON.stringify(state.error.errorDetail, undefined, 2), - type: "ERROR", - }} - /> - ); - } - case "incompatible": { - return ( - <NotificationCardBulma - notification={{ - message: i18n.str`The server version is not supported`, - description: i18n.str`Supported version "${state.supported}", server version "${state.result.version}".`, - type: "WARN", - }} - /> - ); - } - default: - assertUnreachable(state); - } + return ( + <div> + <NotificationCardBulma + notification={{ + message: i18n.str`Contacting the server failed`, + type: "ERROR", + description: <Fragment> + {children} + </Fragment> + }} + /> + </div> + ); } const swrCacheEvictor = new (class implements CacheEvictor< diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -23,18 +23,18 @@ import { AbsoluteTime, HttpStatusCode, TalerError, - TranslatedString, } from "@gnu-taler/taler-util"; import { - NotificationCard, NotificationCardBulma, + ToastBannerBulma, urlPattern, - useTranslationContext, + useRenderErrorReport, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; import { Fragment, VNode, h } from "preact"; import { Route, Router, route } from "preact-router"; -import { useEffect, useErrorBoundary, useState } from "preact/hooks"; +import { useEffect } from "preact/hooks"; import { Loading } from "./components/exception/loading.js"; import { Menu, NotConnectedAppMenu } from "./components/menu/index.js"; import { useSessionContext } from "./context/session.js"; @@ -43,6 +43,7 @@ import { useInstanceKYCDetails } from "./hooks/instance.js"; import { usePreference } from "./hooks/preference.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/index.js"; +import PosTokenCreatePage from "./paths/instance/accessTokens/create-pos/index.js"; import AccessTokenCreatePage from "./paths/instance/accessTokens/create/index.js"; import AccessTokenListPage from "./paths/instance/accessTokens/list/index.js"; import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; @@ -51,6 +52,7 @@ import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; import CreateCategory from "./paths/instance/categories/create/index.js"; import ListCategories from "./paths/instance/categories/list/index.js"; import UpdateCategory from "./paths/instance/categories/update/index.js"; +import ListExchanges from "./paths/instance/exchanges/list/index.js"; import CreateProductGroup from "./paths/instance/groups/create/index.js"; import ListProductGroups from "./paths/instance/groups/list/index.js"; import ListKYCPage from "./paths/instance/kyc/list/index.js"; @@ -97,8 +99,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 PosTokenCreatePage from "./paths/instance/accessTokens/create-pos/index.js"; -import ListExchanges from "./paths/instance/exchanges/list/index.js"; const TALER_SCREEN_ID = 3; @@ -195,13 +195,7 @@ export function Routing(_p: Props): VNode { const { state, config } = useSessionContext(); const { i18n } = useTranslationContext(); - type GlobalNotifState = - | (NotificationCard & { to: string | undefined }) - | undefined; - const [globalNotification, setGlobalNotification] = - useState<GlobalNotifState>(undefined); - - const [error] = useErrorBoundary(); + useRenderErrorReport({ version: __VERSION__, hash: __GIT_HASH__ }); const [preference] = usePreference(); const now = AbsoluteTime.now(); @@ -254,12 +248,9 @@ export function Routing(_p: Props): VNode { /> <Route default - component={() => ( - <LoginPage - showCreateAccount={config.have_self_provisioning} - focus - /> - )} + component={LoginPage} + showCreateAccount={config.have_self_provisioning} + focus /> </Router> </Fragment> @@ -296,51 +287,9 @@ export function Routing(_p: Props): VNode { <Fragment> <Menu /> <KycBanner /> - <NotificationCardBulma notification={globalNotification} /> - {error && ( - <Fragment> - <NotificationCardBulma - notification={{ - message: "Internal error.", - type: "ERROR", - - description: ( - <Fragment> - <i18n.Translate> - The application is in an unexpected state and can't recover - from here. You can report the problem to the developers at{" "} - <a - href="https://bugs.gnunet.org/" - target="_blank" - referrerpolicy="no-referrer" - > - https://bugs.gnunet.org/ - </a> - </i18n.Translate> - <pre> - { - (error instanceof Error - ? error.stack - : String(error)) as TranslatedString - } - </pre> - </Fragment> - ), - }} - /> - </Fragment> - )} - - <Router - history={history} - onChange={(e) => { - const movingOutFromNotification = - globalNotification && e.url !== globalNotification.to; - if (movingOutFromNotification) { - setGlobalNotification(undefined); - } - }} - > + <ToastBannerBulma /> + + <Router history={history}> <Route path="/" component={Redirect} to={InstancePaths.order_list} /> {/** * Admin pages @@ -1007,3 +956,4 @@ function KycBanner(): VNode { /> ); } + diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx @@ -9,16 +9,16 @@ import { TanChannel, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - SafeHandlerTemplate, + Button, + SafeHandler, undefinedIfEmpty, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; +import { useMerchantChallengeHandlerContext } from "../context/challenge.js"; import { useSessionContext } from "../context/session.js"; import { datetimeFormatForPreferences, @@ -26,16 +26,15 @@ import { } from "../hooks/preference.js"; import { FormErrors, FormProvider } from "./form/FormProvider.js"; import { InputCode } from "./form/InputCode.js"; -import { InputBoolean } from "./form/InputBoolean.js"; const TALER_SCREEN_ID = 5; export interface Props { - onCompleted: SafeHandlerTemplate<[challenges: string[]], any>; + onCompleted: SafeHandler<[challenges: string[]], any>; onCancel(): void; currentChallenge: ChallengeResponse; focus?: boolean; - initial?: { request: Challenge; response: ChallengeRequestResponse }; + initial?: { request: Challenge; response?: ChallengeRequestResponse }; showFull?: Partial<Record<TanChannel, string>>; } @@ -192,15 +191,15 @@ function SolveChallenge({ setValue(v); } const tan = !value.code || !!errors ? undefined : value.code; - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const verify = safeFunctionHandler( - i18n.str`verify code`, - lib.instance.confirmChallenge.bind(lib.instance), - !tan ? undefined : [challenge.challenge_id, { tan }], + const verify = actionHandler( + /*verify code*/ + (ct, id, b) => lib.instance.confirmChallenge(id, b), + !tan ? undefined : ([challenge.challenge_id, { tan }] as const), ); verify.onSuccess = onSolved; - verify.onFail = (fail) => { + verify.onFail = showError(i18n.str`Verifcation failed`, (fail) => { setValue({}); setTries((t) => ({ numTries: t.numTries + 1, @@ -218,12 +217,12 @@ function SolveChallenge({ default: assertUnreachable(fail); } - }; + }); - const resend = safeFunctionHandler( - i18n.str`send challenge`, - () => lib.instance.sendChallenge(challenge.challenge_id), - !showResend ? undefined : [], + const resend = actionHandler( + /*send challenge*/ + (ct, id) => lib.instance.sendChallenge(id), + !showResend ? undefined : [challenge.challenge_id], ); resend.onSuccess = (success) => { @@ -239,7 +238,7 @@ function SolveChallenge({ ); }; - resend.onFail = (fail) => { + resend.onFail = showError(i18n.str`Resend failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Failed to send the verification code.`; @@ -256,7 +255,7 @@ function SolveChallenge({ default: assertUnreachable(fail); } - }; + }); useEffect(() => { if (!tan || tries.numTries > 0) { @@ -274,8 +273,6 @@ function SolveChallenge({ return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> <FormProvider<Form> @@ -363,12 +360,9 @@ function SolveChallenge({ <p style={{ alignContent: "center", marginRight: 8 }}> <i18n.Translate>Didn't received the code?</i18n.Translate> </p> - <ButtonBetterBulma - class="button" - onClick={resend} - > + <Button class="button" onClick={resend}> <i18n.Translate>Resend</i18n.Translate> - </ButtonBetterBulma> + </Button> </div> </div> <RetransmissionCodeLimitExpiration @@ -386,13 +380,14 @@ function SolveChallenge({ <button class="button" type="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> - <ButtonBetterBulma + <Button + class="button is-success" submit disabled={!tries.numTries} onClick={verify} > <i18n.Translate>Verify</i18n.Translate> - </ButtonBetterBulma> + </Button> </footer> </FormProvider> </div> @@ -400,8 +395,23 @@ function SolveChallenge({ </Fragment> ); } +export function SolveChallengeDialog({}: {}): VNode { + const mfa = useMerchantChallengeHandlerContext(); + if (!mfa.pending) return <Fragment />; + return ( + <Fragment> + <SolveMFAChallenges + currentChallenge={mfa.pending.requirement} + initial={mfa.pending.initial} + focus + onCompleted={mfa.pending.retry} + onCancel={mfa.cancel} + /> + </Fragment> + ); +} -export function SolveMFAChallenges({ +function SolveMFAChallenges({ currentChallenge, onCompleted, onCancel, @@ -421,21 +431,22 @@ export function SolveMFAChallenges({ expiration: AbsoluteTime; retransmission: AbsoluteTime; }; - const initialActive: Active | undefined = initial - ? ({ - ch: initial.request, - expiration: !initial.response.solve_expiration - ? AbsoluteTime.never() - : AbsoluteTime.fromProtocolTimestamp( - initial.response.solve_expiration, - ), - retransmission: !initial.response.earliest_retransmission - ? AbsoluteTime.never() - : AbsoluteTime.fromProtocolTimestamp( - initial.response.earliest_retransmission, - ), - } as Active) - : undefined; + const initialActive: Active | undefined = + initial && initial.response + ? ({ + ch: initial.request, + expiration: !initial.response.solve_expiration + ? AbsoluteTime.never() + : AbsoluteTime.fromProtocolTimestamp( + initial.response.solve_expiration, + ), + retransmission: !initial.response.earliest_retransmission + ? AbsoluteTime.never() + : AbsoluteTime.fromProtocolTimestamp( + initial.response.earliest_retransmission, + ), + } as Active) + : undefined; // const [retransmission, setRetransmission] = useState(initialRetrans); @@ -447,11 +458,11 @@ export function SolveMFAChallenges({ : undefined; const [selectedChallenge, setSelectedChallenge] = useState(defaultSelected); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const sendMessage = safeFunctionHandler( - i18n.str`send challenge`, - (ch: Challenge) => lib.instance.sendChallenge(ch.challenge_id), + const sendMessage = actionHandler( + /*send challenge*/ + (ct, ch: Challenge) => lib.instance.sendChallenge(ch.challenge_id), ); sendMessage.onSuccess = (success, ch) => { @@ -466,7 +477,7 @@ export function SolveMFAChallenges({ }); }; - sendMessage.onFail = (fail) => { + sendMessage.onFail = showError(i18n.str`Send failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Failed to send the verification code.`; @@ -483,7 +494,7 @@ export function SolveMFAChallenges({ default: assertUnreachable(fail); } - }; + }); const hasSolvedEnough = currentChallenge.combi_and ? solved.length === currentChallenge.challenges.length @@ -509,6 +520,7 @@ export function SolveMFAChallenges({ const enough = currentChallenge.combi_and ? total.length === currentChallenge.challenges.length : total.length > 0; + if (enough) { setSolved(total); setActive(undefined); @@ -536,8 +548,6 @@ export function SolveMFAChallenges({ return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> <header @@ -624,16 +634,16 @@ export function SolveMFAChallenges({ <button class="button" type="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> - <ButtonBetterBulma onClick={doSend}> + <Button class="button is-success" onClick={doSend}> <i18n.Translate>Continue</i18n.Translate> - </ButtonBetterBulma> + </Button> {!enough ? undefined : ( - <ButtonBetterBulma - + <Button + class="button is-success" onClick={onCompleted.withArgs(solved)} > <i18n.Translate>Complete</i18n.Translate> - </ButtonBetterBulma> + </Button> )} </footer> </div> diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx @@ -13,11 +13,16 @@ 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, TranslatedString } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, + AccessToken, + assertUnreachable, + HttpStatusCode, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { + Button, Notification, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -56,17 +61,29 @@ export function JumpToElementById({ const [id, setId] = useState<string>(); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const checkExist = safeFunctionHandler( - i18n.str`get product details`, - (token: AccessToken, id: string) => + const { actionHandler, showError } = useNotificationContext(); + const checkExist = actionHandler( + /*get product details*/ + (ct, token: AccessToken, id: string) => lib.instance.getProductDetails(token, id), !session.token || !id ? undefined : [session.token, id], ); checkExist.onSuccess = (success, t, id) => { onSelect(id); }; - checkExist.onFail = () => i18n.str`Not found`; + checkExist.onFail = showError( + i18n.str`Getting product by ID failed.`, + (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; + default: + assertUnreachable(fail); + } + }, + ); return ( <Fragment> @@ -82,20 +99,16 @@ export function JumpToElementById({ value={id ?? ""} onChange={(e) => { setId(e.currentTarget.value); - if (notification) notification.acknowledge(); + // if (notification) notification.acknowledge(); }} placeholder={placeholder} /> - <NotificationFieldFoot notification={notification} /> + {/* <NotificationFieldFoot notification={notification} /> */} </div> <Tooltip text={description}> - <ButtonBetterBulma - class="button" - submit - onClick={checkExist} - > + <Button class="button" submit onClick={checkExist}> <i class="icon mdi mdi-arrow-right" /> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx @@ -219,9 +219,9 @@ export function NotConnectedAppMenu({ }: NotConnectedAppMenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); const { i18n } = useTranslationContext(); - useEffect(() => { - document.title = `Taler Merchant Portal: ${title}`; - }, [title]); + // useEffect(() => { + // document.title = `Taler Merchant Portal: ${title}`; + // }, [title]); const { isTestingEnvironment } = useSettingsContext(); return ( @@ -264,9 +264,9 @@ export function NotYetReadyAppMenu({ title }: NotYetReadyAppMenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); const { state } = useSessionContext(); - useEffect(() => { - document.title = `Taler Merchant Portal: ${title}`; - }, [title]); + // useEffect(() => { + // document.title = `Taler Merchant Portal: ${title}`; + // }, [title]); const isLoggedIn = state.status === "loggedIn"; diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -28,16 +28,15 @@ import { TranslatedString, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, + Button, QR_Bank, QR_SwissBank, - SafeHandlerTemplate, + SafeHandler, useCommonPreferences, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; -import { useSessionContext } from "../../context/session.js"; import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js"; import { Spinner } from "../exception/loading.js"; import { doAutoFocus } from "../form/Input.js"; @@ -48,7 +47,7 @@ interface Props { active?: boolean; description?: TranslatedString; onCancel?: () => void; - confirm?: SafeHandlerTemplate<any, any>; + confirm?: SafeHandler<any, any>; label: TranslatedString; children?: ComponentChildren; danger?: boolean; @@ -99,13 +98,13 @@ export function ConfirmModal({ </button> ) : undefined} - <ButtonBetterBulma + <Button submit class={danger ? "button is-danger " : "button is-info "} onClick={confirm} > <i18n.Translate>{label}</i18n.Translate> - </ButtonBetterBulma> + </Button> </Fragment> ) : noCancelButton ? undefined : ( <button @@ -156,14 +155,14 @@ export function ContinueModal({ <section class="modal-card-body">{children}</section> <footer class="modal-card-foot"> <div class="buttons is-right" style={{ width: "100%" }}> - <ButtonBetterBulma + <Button + class="button is-success" ref={doAutoFocus} submit - class="button is-success " onClick={confirm} > <i18n.Translate>Continue</i18n.Translate> - </ButtonBetterBulma> + </Button> </div> </footer> </div> @@ -238,14 +237,14 @@ export function ClearConfirmModal({ <button class="button " type="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> - <ButtonBetterBulma + <Button class="button is-info" submit onClick={confirm} ref={doAutoFocus} > <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </div> </footer> </div> @@ -261,7 +260,7 @@ export function ClearConfirmModal({ interface CompareAccountsModalProps { onCancel: () => void; - confirm: SafeHandlerTemplate<any, any>; + confirm: SafeHandler<any, any>; formPayto: Paytos.URI | undefined; testPayto: Paytos.URI; } diff --git a/packages/merchant-backoffice-ui/src/context/challenge.ts b/packages/merchant-backoffice-ui/src/context/challenge.ts @@ -0,0 +1,137 @@ +/* + This file is part of GNU Taler + (C) 2022-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + Challenge, + ChallengeRequestResponse, + ChallengeResponse, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { + SafeHandler, + useMerchantApiContext, +} from "@gnu-taler/web-util/browser"; +import { ComponentChildren, createContext, h, VNode } from "preact"; +import { useContext, useState } from "preact/hooks"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +export type ContextType = { + pending?: MfaState; + cancel(): void; + onNewChallenge( + operation: TranslatedString, + challenge: ChallengeResponse, + handler: SafeHandler<[string[]], any>, + ): void; +}; + +const initial: ContextType = { + onNewChallenge: () => { + throw Error("MerchantChallengeHandlerProvider not initialized"); + }, + cancel: () => { + throw Error("MerchantChallengeHandlerProvider not initialized"); + }, +}; +const Context = createContext<ContextType>(initial); + +export const useMerchantChallengeHandlerContext = (): ContextType => + useContext(Context); + +type MfaState = { + requirement: ChallengeResponse; + loadingFirstChallenge: boolean; + title: TranslatedString; + retry: SafeHandler<[string[]], any>; + initial?: { request: Challenge; response?: ChallengeRequestResponse }; +}; +// FIXME: practically the same as BankChallengeHandlerProvider but we cant +// reuse it because it ask for username +export const MerchantChallengeHandlerProvider = ({ + children, +}: { + children: ComponentChildren; +}): VNode => { + const [state, setState] = useState<MfaState>(); + const { lib } = useMerchantApiContext(); + + /** + * Check the response of the handler and + * expect a MFA response and verify: + * - if there is only one challenge + * - or multiple challenge and all needs to be completed + * then start for the first challenge right away. + * + * The first challenge should always be the cheaper based on server + * selection. + * + */ + async function onNewChallenge( + operation: TranslatedString, + requirement: ChallengeResponse, + handler: SafeHandler<[string[]], any>, + ) { + const loadingFirstChallenge = + requirement.combi_and === true || requirement.challenges.length === 1; + + handler.addListener((ev) => { + if (ev === "success") { + setState(undefined); + } + }); + + // Set the sate now, if "LFC" is true "initial" is undefined it means "loading" + setState({ + title: operation, + requirement, + retry: handler, + loadingFirstChallenge, + }); + + if (loadingFirstChallenge) { + const challenge = requirement.challenges[0]; + const result = await lib.instance.sendChallenge(challenge.challenge_id); + + setState({ + title: operation, + retry: handler, + requirement, + loadingFirstChallenge, + initial: { + request: challenge, + response: result.type === "ok" ? result.body : undefined, + }, + }); + } + } + + function cancel() { + setState(undefined); + } + + return h(Context.Provider, { + value: { + pending: state, + cancel, + onNewChallenge, + }, + children, + }); +}; diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts @@ -26,13 +26,13 @@ import { } from "@gnu-taler/taler-util"; import { buildStorageKey, + MerchantLib, useLocalStorage, useMerchantApiContext, } from "@gnu-taler/web-util/browser"; import { ComponentChildren, VNode, createContext, h } from "preact"; import { useContext, useEffect, useState } from "preact/hooks"; import { mutate } from "swr"; -import { MerchantLib } from "../../../web-util/lib/context/activity.js"; /** * Has the information to reach and diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -31,10 +31,9 @@ import { } from "@gnu-taler/taler-util"; import { dummyHttpResponse } from "@gnu-taler/taler-util/http"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, useChallengeHandler, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -45,7 +44,6 @@ import { } from "../../../components/form/FormProvider.js"; import { InputPassword } from "../../../components/form/InputPassword.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; -import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; import { Tooltip } from "../../../components/Tooltip.js"; import { useSessionContext } from "../../../context/session.js"; import { @@ -54,8 +52,8 @@ import { PHONE_JUST_NUMBERS_REGEX, } from "../../../utils/constants.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; -import { maybeTryFirstMFA } from "../../instance/accounts/create/CreatePage.js"; import { FOREVER_REFRESHABLE_TOKEN } from "../../login/index.js"; +import { useMerchantChallengeHandlerContext } from "../../../context/challenge.js"; const TALER_SCREEN_ID = 25; @@ -98,8 +96,8 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { const { i18n } = useTranslationContext(); const { state: session, lib, logIn, config } = useSessionContext(); const [value, valueHandler] = useState(with_defaults(forceId, config)); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); + const { actionHandler, showError } = useNotificationContext(); + const mfa = useMerchantChallengeHandlerContext(); let phoneRegex: RegExp | undefined = undefined; if (config.phone_regex) { @@ -177,9 +175,10 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { password: value.password!, }, }; - const create = safeFunctionHandler( - i18n.str`create instance and login`, + const create = actionHandler( + /*create instance and login*/ async ( + ct, token: AccessToken | undefined, data: TalerMerchantApi.InstanceConfigurationMessage, challengeIds: string[], @@ -200,13 +199,7 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { data.auth.password, FOREVER_REFRESHABLE_TOKEN(i18n.str`Instance created`), ); - if (tokenResp.type === "fail") { - if (tokenResp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, tokenResp.body); - } - return tokenResp; - } - return opFixedSuccess(dummyHttpResponse, tokenResp.body); + return tokenResp; } return opEmptySuccess(dummyHttpResponse); }, @@ -218,9 +211,16 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { } onConfirm(); }; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`New account`, + fail.body, + create.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -231,28 +231,10 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { default: assertUnreachable(fail); } - }; - const retry = create.lambda((ids: string[]) => [ - create.args![0], - create.args![1], - ids, - ]); - - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - onCompleted={retry} - onCancel={mfa.doCancelChallenge} - focus - /> - ); - } + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -311,9 +293,9 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma onClick={create} submit> + <Button class="button is-success" onClick={create} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx @@ -25,21 +25,19 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, - SafeHandlerTemplate, + SafeHandler, useChallengeHandler, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { CardTable as CardTableActive } from "./TableActive.js"; import { Fragment } from "preact"; -import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; import { useSessionContext } from "../../../context/session.js"; import { ConfirmModal } from "../../../components/modal/index.js"; import { Tooltip } from "../../../components/Tooltip.js"; -import { maybeTryFirstMFA } from "../../instance/accounts/create/CreatePage.js"; +import { useMerchantChallengeHandlerContext } from "../../../context/challenge.js"; const TALER_SCREEN_ID = 28; @@ -72,12 +70,14 @@ export function View({ const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const mfa = useChallengeHandler(); - const deleteAction = safeFunctionHandler( - i18n.str`delete instance`, + const mfa = useMerchantChallengeHandlerContext(); + + const deleteAction = actionHandler( + /*delete instance*/ async ( + ct, token: AccessToken, instance: TalerMerchantApi.Instance, purge: boolean, @@ -87,14 +87,6 @@ export function View({ challengeIds, purge, }); - if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA( - lib.instance, - mfa, - resp.body, - deleteAction.lambda((ids) => [token, instance, purge, ids]), - ); - } return resp; }, ); @@ -107,9 +99,16 @@ export function View({ return i18n.str`Instance "${instance.name}" (ID: ${instance.id}) has been deleted.`; }; - deleteAction.onFail = (fail, t, i, p) => { + deleteAction.onFail = showError(i18n.str`Delete failed.`, (fail, t, i, p) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`Delete instance "${i.name}"`, + fail.body, + deleteAction.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], prev[2], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -118,7 +117,7 @@ export function View({ case HttpStatusCode.Conflict: return i18n.str`Conflict.`; } - }; + }); const remove = !session.token || !deleting @@ -130,21 +129,8 @@ export function View({ ? deleteAction : deleteAction.withArgs(session.token, deleting, true, []); - if (mfa.pendingChallenge && mfa.repeatCall) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - focus - onCompleted={mfa.repeatCall} - onCancel={mfa.doCancelChallenge} - /> - ); - } - return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> {deleting && (purging ? ( <PurgeModal @@ -215,7 +201,7 @@ export function View({ interface DeleteModalProps { element: { id: string; name: string }; onCancel: () => void; - confirm: SafeHandlerTemplate<any, any>; + confirm: SafeHandler<any, any>; } export function DeleteModal({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create-pos/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create-pos/CreatePage.tsx @@ -27,10 +27,9 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, useChallengeHandler, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -41,11 +40,10 @@ import { TalerForm, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; import { Tooltip } from "../../../../components/Tooltip.js"; import { useSessionContext } from "../../../../context/session.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { maybeTryFirstMFA } from "../../accounts/create/CreatePage.js"; +import { useMerchantChallengeHandlerContext } from "../../../../context/challenge.js"; const TALER_SCREEN_ID = 110; @@ -72,8 +70,8 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const hasErrors = errors !== undefined; - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); + const { actionHandler, showError } = useNotificationContext(); + const mfa = useMerchantChallengeHandlerContext(); const data: TalerMerchantApi.LoginTokenRequest = { scope: LoginTokenScope.OrderPos_Refreshable, @@ -81,9 +79,10 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { duration: Duration.toTalerProtocolDuration(Duration.fromSpec({ days: 10 })), }; - const create = safeFunctionHandler( - i18n.str`create pos access token`, + const create = actionHandler( + /*create pos access token*/ async ( + ct, pwd: string, request: TalerMerchantApi.LoginTokenRequest, challengeIds: string[], @@ -96,16 +95,20 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { challengeIds, }, ); - if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, resp.body); - } return resp; }, !!errors || !state.password ? undefined : [state.password, data, []], ); - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`New access token`, + fail.body, + create.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Check the password.`; @@ -114,29 +117,11 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); create.onSuccess = onCreated; - const retry = create.lambda((ids: string[]) => [ - create.args![0], - create.args![1], - ids, - ]); - - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - focus - onCompleted={retry} - onCancel={mfa.doCancelChallenge} - /> - ); - } return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -170,9 +155,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={create}> + <Button class="button is-success" submit onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -27,11 +27,10 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, NotificationCardBulma, useChallengeHandler, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -45,12 +44,11 @@ import { Input } from "../../../../components/form/Input.js"; import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { getAvailableForPersona } from "../../../../components/menu/SideBar.js"; -import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; import { Tooltip } from "../../../../components/Tooltip.js"; import { useSessionContext } from "../../../../context/session.js"; import { UIElement, usePreference } from "../../../../hooks/preference.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { maybeTryFirstMFA } from "../../accounts/create/CreatePage.js"; +import { useMerchantChallengeHandlerContext } from "../../../../context/challenge.js"; const TALER_SCREEN_ID = 111; @@ -113,8 +111,8 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const hasErrors = errors !== undefined; - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); + const { actionHandler, showError } = useNotificationContext(); + const mfa = useMerchantChallengeHandlerContext(); const data: TalerMerchantApi.LoginTokenRequest = { scope: state.scope!, @@ -123,9 +121,10 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { ? undefined : Duration.toTalerProtocolDuration(state.duration), }; - const create = safeFunctionHandler( - i18n.str`create access token`, + const create = actionHandler( + /*create access token*/ async ( + ct, pwd: string, request: TalerMerchantApi.LoginTokenRequest, challengeIds: string[], @@ -138,16 +137,20 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { challengeIds, }, ); - if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, resp.body); - } return resp; }, !!errors || !state.password ? undefined : [state.password, data, []], ); - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`Create access token`, + fail.body, + create.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Check the password.`; @@ -156,29 +159,11 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); create.onSuccess = onCreated; - const retry = create.lambda((ids: string[]) => [ - create.args![0], - create.args![1], - ids, - ]); - - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - focus - onCompleted={retry} - onCancel={mfa.doCancelChallenge} - /> - ); - } return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -308,9 +293,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={create}> + <Button class="button is-success" submit onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -27,8 +27,7 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -75,17 +74,19 @@ 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], + const { actionHandler, showError } = useNotificationContext(); + const deleteToken = actionHandler( + /*delete access token*/ + (ct, t, s) => lib.instance.deleteAccessToken(t, s), + !session.token || !deleting + ? undefined + : ([session.token, deleting.serial] as const), ); deleteToken.onSuccess = () => { setDeleting(null); }; - deleteToken.onFail = (fail) => { + deleteToken.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -96,13 +97,11 @@ export default function AccessTokenListPage({ onCreate }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <Fragment> <section class="section is-main-section"> - <LocalNotificationBannerBulma notification={notification} /> - <p style={{ marginBottom: 10 }}> <a href={"#/access-token/new-pos"} class="has-icon"> <i class="icon mdi mdi-account-plus" /> 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 @@ -31,12 +31,11 @@ import { } from "@gnu-taler/taler-util"; import { dummyHttpResponse } from "@gnu-taler/taler-util/http"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, MfaState, - SafeHandlerTemplate, + SafeHandler, useChallengeHandler, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -53,13 +52,13 @@ 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 { Tooltip } from "../../../../components/Tooltip.js"; import { useSessionContext } from "../../../../context/session.js"; import { UIElement, usePreference } from "../../../../hooks/preference.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { safeConvertURL } from "../update/UpdatePage.js"; import { TestRevenueErrorType, testRevenueAPI } from "./index.js"; +import { useMerchantChallengeHandlerContext } from "../../../../context/challenge.js"; const TALER_SCREEN_ID = 33; @@ -165,17 +164,14 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { extra_wire_subject_metadata: state.extra_wire_subject_metadata, }; - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); - const add = safeFunctionHandler( - i18n.str`add bank account`, - async (token: AccessToken, request: Entity, challengeIds: string[]) => { + const { actionHandler, showError } = useNotificationContext(); + const mfa = useMerchantChallengeHandlerContext(); + const add = actionHandler( + /*add bank account*/ + async (ct, token: AccessToken, request: Entity, challengeIds: string[]) => { const resp = await lib.instance.addBankAccount(token, request, { challengeIds, }); - if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, resp.body); - } return resp; }, !session.token || !request ? undefined : [session.token, request, []], @@ -183,9 +179,16 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { add.onSuccess = (resp) => { onCreated(resp.h_wire); }; - add.onFail = (fail) => { + add.onFail = showError(i18n.str`Add failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`New bank account`, + fail.body, + add.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -196,24 +199,18 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; - - const repeat = add.lambda((ids: string[]) => [ - add.args![0], - add.args![1], - ids, - ]); + }); const revenueAPI = !state.credit_facade_url ? undefined : new URL("./", state.credit_facade_url); - const test = safeFunctionHandler( - i18n.str`test revenue api`, + const test = actionHandler( + /*test revenue api*/ testRevenueAPI, !state.credit_facade_credentials || !revenueAPI ? undefined - : [revenueAPI, state.credit_facade_credentials], + : ([revenueAPI, state.credit_facade_credentials] as const), ); test.onSuccess = (success) => { @@ -227,7 +224,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { } }; - test.onFail = (fail) => { + test.onFail = showError(i18n.str`Test failed`, (fail) => { switch (fail.case) { case TestRevenueErrorType.CANT_VALIDATE: return i18n.str`The request was made correctly, but the bank's server did not respond with the appropriate value for 'credit_account', so we cannot confirm that it is the same bank account.`; @@ -242,22 +239,10 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - focus - onCompleted={repeat} - onCancel={mfa.doCancelChallenge} - /> - ); - } return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -343,13 +328,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { threeState side={ <Tooltip text={i18n.str`Verify details with server`}> - <ButtonBetterBulma - class="button is-info" - - onClick={test} - > + <Button class="button is-info" onClick={test}> <i18n.Translate>Test</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> } /> @@ -367,9 +348,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma onClick={add} submit> + <Button class="button is-success" onClick={add} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> @@ -382,9 +363,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { onCancel={() => { setRevenuePayto(undefined); }} - confirm={safeFunctionHandler( - i18n.str`parse revenue payto`, - async () => { + confirm={actionHandler( + /*parse revenue payto*/ + async (ct) => { setState({ ...state, payto_uri: Paytos.toFullString(revenuePayto), @@ -402,29 +383,43 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { ); } -export async function maybeTryFirstMFA( - api: TalerMerchantManagementHttpClient, - mfa: MfaState, - b: TalerMerchantApi.ChallengeResponse, - repeat?: SafeHandlerTemplate<[ids: string[]], any>, -) { - const letUserDecide = b.combi_and === false && b.challenges.length > 1; - if (b.challenges.length === 0 || letUserDecide) { - mfa.onChallengeRequired(b, repeat); - return; - } - const challenge = b.challenges[0]; - const result = await api.sendChallenge(challenge.challenge_id); - if (result.type === "fail") { - mfa.onChallengeRequired(b, repeat); - } else { - mfa.onChallengeRequiredWithInitial( - b, - { - request: challenge, - response: result.body, - }, - repeat, - ); - } -} +/** + * Read the MFA response and check: + * - if there is only one challenge + * - or multiple challenge and all needs to be completed + * then start for the first challenge right away. + * The first challenge should always be the cheaper based on server + * selection. + * + * @param api + * @param mfa + * @param body + * @param repeat + * @returns + */ +// export async function maybeTryFirstMFA ( +// api: TalerMerchantManagementHttpClient, +// mfa: MfaState, +// body: TalerMerchantApi.ChallengeResponse, +// repeat: SafeHandler<[ids: string[]], any>, +// ) { +// const letUserDecide = body.combi_and === false && body.challenges.length > 1; +// if (body.challenges.length === 0 || letUserDecide) { +// mfa.onChallengeRequired(body, repeat); +// return; +// } +// const challenge = body.challenges[0]; +// const result = await api.sendChallenge(challenge.challenge_id); +// if (result.type === "fail") { +// mfa.onChallengeRequired(body, repeat); +// } else { +// mfa.onChallengeRequiredWithInitial( +// body, +// { +// request: challenge, +// response: result.body, +// }, +// repeat, +// ); +// } +// } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -22,6 +22,7 @@ import { AccessToken, BasicOrTokenAuth, + CancellationToken, FacadeCredentials, HttpStatusCode, OperationFail, @@ -59,6 +60,7 @@ export enum TestRevenueErrorType { } export async function testRevenueAPI( + ct: CancellationToken, revenueAPI: URL, creds: FacadeCredentials | undefined, ): Promise< 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 @@ -30,8 +30,7 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -55,16 +54,18 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { const { i18n } = useTranslationContext(); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const [deleting, setDeleting] = 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], + const remove = actionHandler( + /*delete bank account*/ + (ct, t, w) => lib.instance.deleteBankAccount(t, w), + !session.token || !deleting + ? undefined + : ([session.token, deleting.h_wire] as const), ); - remove.onFail = (fail) => { + remove.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -74,7 +75,7 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { assertUnreachable(fail); } } - }; + }); remove.onSuccess = () => { setDeleting(null); return i18n.str`The bank account has been deleted.`; @@ -82,8 +83,6 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - {deleting && ( <ConfirmModal label={i18n.str`Delete account`} 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 @@ -21,6 +21,7 @@ import { AccessToken, + CancellationToken, HttpStatusCode, PaytoString, Paytos, @@ -31,11 +32,9 @@ import { } from "@gnu-taler/taler-util"; import { dummyHttpResponse } from "@gnu-taler/taler-util/http"; import { - ButtonBetterBulma, - LocalNotificationBanner, - useChallengeHandler, - useLocalNotificationBetter, - useTranslationContext, + Button, + useNotificationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -50,13 +49,12 @@ 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 { Tooltip } from "../../../../components/Tooltip.js"; +import { useMerchantChallengeHandlerContext } from "../../../../context/challenge.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 { maybeTryFirstMFA } from "../create/CreatePage.js"; import { TestRevenueErrorType, testRevenueAPI } from "../create/index.js"; import { UnableToUseBankAccountWarning } from "../list/index.js"; @@ -189,9 +187,10 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { type: "none", }; const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); async function changeBankAccount( + ct: CancellationToken, token: AccessToken, newPayto: string | undefined, id: string, @@ -207,9 +206,6 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { challengeIds, }); if (created.type === "fail") { - if (created.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, created.body); - } return created; } const deleted = await lib.instance.deleteBankAccount(token, id); @@ -223,9 +219,9 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { return opEmptySuccess(dummyHttpResponse); } - const mfa = useChallengeHandler(); - const update = safeFunctionHandler( - i18n.str`change bank account`, + const mfa = useMerchantChallengeHandlerContext(); + const update = actionHandler( + /*change bank account*/ changeBankAccount, !session.token ? undefined @@ -244,9 +240,16 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { ], ); update.onSuccess = onUpdated; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`Update bank account`, + fail.body, + update.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], prev[2], prev[3], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -257,21 +260,14 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; - const repeat = update.lambda((ids: string[]) => [ - update.args![0], - update.args![1], - update.args![2], - update.args![3], - ids, - ]); + }); const revenueAPI = !state.credit_facade_url ? undefined : new URL("./", state.credit_facade_url); - const test = safeFunctionHandler( - i18n.str`test revenue api`, + const test = actionHandler( + /*test revenue api*/ testRevenueAPI, !revenueAPI || !state.credit_facade_url ? undefined @@ -287,7 +283,7 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { setRevenuePayto(success); } }; - test.onFail = (fail) => { + test.onFail = showError(i18n.str`Test failed`, (fail) => { switch (fail.case) { case HttpStatusCode.BadRequest: return i18n.str`Server replied with "bad request".`; @@ -302,24 +298,12 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - focus - onCompleted={repeat} - onCancel={mfa.doCancelChallenge} - /> - ); - } const payto = Result.unpack(Paytos.fromString(account.payto_uri)); return ( <Fragment> - <LocalNotificationBanner notification={notification} /> <section class="section "> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -426,13 +410,9 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { <Tooltip text={i18n.str`Compare info from server with account form`} > - <ButtonBetterBulma - - class="button is-info" - onClick={test} - > + <Button class="button is-info" onClick={test}> <i18n.Translate>Test</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> } /> @@ -452,9 +432,9 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma onClick={update} submit> + <Button class="button is-success" onClick={update} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </div> @@ -465,9 +445,9 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { onCancel={() => { setRevenuePayto(undefined); }} - confirm={safeFunctionHandler( - i18n.str`parse revenue payto`, - async () => { + confirm={actionHandler( + /*parse revenue payto*/ + async (ct) => { setState({ ...state, payto_uri: Paytos.toFullString(revenuePayto), 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 @@ -25,9 +25,8 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; @@ -65,18 +64,18 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const hasErrors = errors !== undefined; const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const data = !!errors ? 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], + const create = actionHandler( + /*add category*/ + (ct, t, d) => lib.instance.addCategory(t, d), + !session.token || !data ? undefined : ([session.token, data] as const), ); create.onSuccess = onCreated; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Add failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -85,11 +84,10 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -117,9 +115,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={create}> + <Button class="button is-success" submit onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -25,11 +25,10 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, PaginationControl, - SafeHandlerTemplate, - useLocalNotificationBetter, + SafeHandler, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -59,14 +58,16 @@ export function CardTable({ const { i18n } = useTranslationContext(); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const remove = safeFunctionHandler( - i18n.str`delete category`, - lib.instance.deleteCategory.bind(lib.instance), - ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); + const remove = actionHandler( + /*delete category*/ + (ct, t, i) => lib.instance.deleteCategory(t, i), + ).lambda((prev, [id]: [string]) => + !session.token ? undefined! : [session.token, id], + ); remove.onSuccess = () => i18n.str`Category deleted`; - remove.onFail = (fail) => { + remove.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -75,11 +76,10 @@ export function CardTable({ default: assertUnreachable(fail); } - }; + }); + return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class="card has-table"> <header class="card-header"> <p class="card-header-title"> @@ -124,7 +124,7 @@ export function CardTable({ interface TableProps { rowSelection: string[]; instances: Entity[]; - onDelete: SafeHandlerTemplate<[id: string], unknown>; + onDelete: SafeHandler<[id: string], unknown>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; paginator: PaginationControl; @@ -182,12 +182,12 @@ function Table({ <Tooltip text={i18n.str`Delete selected category from the database`} > - <ButtonBetterBulma + <Button class="button is-danger is-small" onClick={onDelete.withArgs(String(i.category_id))} > <i18n.Translate>Delete</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </td> 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 @@ -27,11 +27,10 @@ import { TranslatedString, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, + Button, Loading, - LocalNotificationBannerBulma, PaginationControl, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -100,15 +99,15 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { setState({ ...state, product_map }); }); }, []); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); 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], + const update = actionHandler( + /*update category*/ + (ct, t, id, d) => lib.instance.updateCategory(t, id, d), + !token ? undefined : ([token, category.id, data] as const), ); update.onSuccess = onUpdated; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -117,12 +116,10 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> - <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -157,9 +154,9 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { </button> )} <Tooltip text={i18n.str`Confirm operation`}> - <ButtonBetterBulma submit onClick={update}> + <Button class="button is-success" submit onClick={update}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> <ProductListSmall 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 @@ -25,9 +25,8 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; @@ -66,19 +65,19 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const hasErrors = errors !== undefined; const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const data = !!errors ? undefined : (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], + const create = actionHandler( + /*create product group*/ + (ct, t, d) => lib.instance.createProductGroup(t, d), + !session.token || !data ? undefined : ([session.token, data] as const), ); create.onSuccess = onCreated; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -87,11 +86,10 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -124,9 +122,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={create}> + <Button class="button is-success" submit onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -25,11 +25,10 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, PaginationControl, - SafeHandlerTemplate, - useLocalNotificationBetter, + SafeHandler, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -54,14 +53,16 @@ export function CardTable({ devices, onCreate, paginator }: Props): VNode { const { i18n } = useTranslationContext(); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const remove = safeFunctionHandler( - i18n.str`delete product group`, - lib.instance.deleteProductGroup.bind(lib.instance), - ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); + const remove = actionHandler( + /*delete product group*/ + (ct, t, id) => lib.instance.deleteProductGroup(t, id), + ).lambda((prev, [id]: [string]) => + !session.token ? undefined! : ([session.token, id] as const), + ); remove.onSuccess = () => i18n.str`Product group deleted`; - remove.onFail = (fail) => { + remove.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -70,11 +71,9 @@ export function CardTable({ devices, onCreate, paginator }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class="card has-table"> <header class="card-header"> <p class="card-header-title"> @@ -118,7 +117,7 @@ export function CardTable({ devices, onCreate, paginator }: Props): VNode { interface TableProps { rowSelection: string[]; instances: Entity[]; - onDelete: SafeHandlerTemplate<[id: string], unknown>; + onDelete: SafeHandler<[id: string], unknown>; // onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; paginator: PaginationControl; @@ -162,12 +161,12 @@ function Table({ instances, onDelete, paginator }: TableProps): VNode { <Tooltip text={i18n.str`Delete selected group from the database`} > - <ButtonBetterBulma + <Button class="button is-danger is-small" onClick={onDelete.withArgs(String(i.group_serial))} > <i18n.Translate>Delete</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </td> 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 @@ -25,9 +25,8 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -90,15 +89,15 @@ export function UpdatePage({ group, onUpdated, onBack }: Props): VNode { // }); // }, []); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); 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], + const update = actionHandler( + /*update product group*/ + (ct, t, id, d) => lib.instance.updateProductGroup(t, id, d), + !token ? undefined : ([token, group.id, data] as const), ); update.onSuccess = onUpdated; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -109,12 +108,10 @@ export function UpdatePage({ group, onUpdated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> - <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -149,9 +146,9 @@ export function UpdatePage({ group, onUpdated, onBack }: Props): VNode { </button> )} <Tooltip text={i18n.str`Confirm operation`}> - <ButtonBetterBulma submit onClick={update}> + <Button class="button is-success" submit onClick={update}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -32,10 +32,9 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, RenderAmountBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format, isFuture } from "date-fns"; @@ -322,17 +321,19 @@ export function CreatePage({ })), create_token: value.payments.createToken, }; - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const create = safeFunctionHandler( - i18n.str`create order`, - lib.instance.createOrder.bind(lib.instance), - !session.token || !request ? undefined : [session.token, request], + const create = actionHandler( + /*create order*/ + (ct, t, r) => lib.instance.createOrder(t, r), + !session.token || !request + ? undefined + : ([session.token, request] as const), ); create.onSuccess = (resp) => { onCreated(resp.order_id); }; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -347,7 +348,7 @@ export function CreatePage({ default: assertUnreachable(fail); } - }; + }); const addProductToTheInventoryList = ( product: TalerMerchantApi.ProductDetailResponse & WithId, quantity: number, @@ -461,7 +462,6 @@ export function CreatePage({ return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <MissingBankAccountsWarning /> <LimitedKycActionWarning /> <section class="section is-main-section"> @@ -854,13 +854,9 @@ export function CreatePage({ <i18n.Translate>Cancel</i18n.Translate> </button> )} - <ButtonBetterBulma - class="button is-success" - submit - onClick={create} - > + <Button class="button is-success" submit onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </div> </FormProvider> </div> 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 @@ -31,12 +31,11 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, PaginationControl, RenderAmount, RenderAmountBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; @@ -209,15 +208,16 @@ function Table({ const { i18n } = useTranslationContext(); const [preferences] = usePreference(); const { state: session, lib, config } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const copyUrl = safeFunctionHandler( - i18n.str`copy order URL`, - (token: AccessToken, id: string) => lib.instance.getOrderDetails(token, id), + const { actionHandler, showError } = useNotificationContext(); + const copyUrl = actionHandler( + /*copy order URL*/ + (ct, token: AccessToken, id: string) => + lib.instance.getOrderDetails(token, id), ); copyUrl.onSuccess = (success) => { copyToClipboard(success.order_status_url); }; - copyUrl.onFail = (fail) => { + copyUrl.onFail = showError(i18n.str`Copy failed`, (fail) => { switch (fail.case) { case TalerErrorCode.MERCHANT_GENERIC_INSTANCE_UNKNOWN: return i18n.str`The instance doesn't exist`; @@ -228,11 +228,10 @@ function Table({ default: assertUnreachable(fail); } - }; + }); + return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class=""> <PaginationRow paginator={paginator} /> <table class="table is-striped is-hoverable is-fullwidth"> @@ -313,7 +312,7 @@ function Table({ </button> )} {!i.paid && ( - <ButtonBetterBulma + <Button class="button is-small is-info jb-modal" onClick={ !session.token @@ -322,7 +321,7 @@ function Table({ } > <i18n.Translate>copy url</i18n.Translate> - </ButtonBetterBulma> + </Button> )} </div> </td> @@ -339,6 +338,7 @@ function Table({ function EmptyTable(): VNode { const { i18n } = useTranslationContext(); + throw Error("this is a break error") return ( <div class="content has-text-grey has-text-centered"> <p> @@ -373,7 +373,7 @@ export function RefundModal({ const [preferences] = usePreference(); const { i18n } = useTranslationContext(); // const [errors, setErrors] = useState<FormErrors<State>>({}); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { state: session, lib, config } = useSessionContext(); const orderam = getOrderAmountAndMaxDepositFee(order); @@ -427,14 +427,18 @@ export function RefundModal({ : `${form.mainReason}: ${form.description}`, }; - const refund = safeFunctionHandler( - i18n.str`authorize refund`, - (token: AccessToken, id: string, request: TalerMerchantApi.RefundRequest) => - lib.instance.addRefund(token, id, request), + const refund = actionHandler( + /*authorize refund*/ + ( + ct, + token: AccessToken, + id: string, + request: TalerMerchantApi.RefundRequest, + ) => lib.instance.addRefund(token, id, request), !session.token || !req ? undefined : [session.token, id, req], ); refund.onSuccess = onConfirmed; - refund.onFail = (fail) => { + refund.onFail = showError(i18n.str`Refund failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -451,7 +455,8 @@ export function RefundModal({ default: assertUnreachable(fail); } - }; + }); + //FIXME: parameters in the translation return ( <ConfirmModal @@ -462,7 +467,6 @@ export function RefundModal({ onCancel={onCancel} confirm={refund} > - <LocalNotificationBannerBulma notification={notification} /> {refunds.length > 0 && ( <div class="columns"> <div class="column is-12"> 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 @@ -28,9 +28,8 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, NotificationCardBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; @@ -113,7 +112,7 @@ export default function OrderList({ const setNewDate = (date?: AbsoluteTime): void => setFilter({ date }); const result = useInstanceOrders({ ...sectionToFilter(section), ...filter }); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { state: session, lib } = useSessionContext(); const { i18n } = useTranslationContext(); @@ -137,17 +136,17 @@ export default function OrderList({ } const data = {} as TalerMerchantApi.RefundRequest; - const refund = safeFunctionHandler( - i18n.str`authorize refund`, - lib.instance.addRefund.bind(lib.instance), + const refund = actionHandler( + /*authorize refund*/ + (ct, t, id, d) => lib.instance.addRefund(t, id, d), !session.token || !orderToBeRefunded ? undefined - : [session.token, orderToBeRefunded.order_id, data], + : ([session.token, orderToBeRefunded.order_id, data] as const), ); refund.onSuccess = () => { setOrderToBeRefunded(undefined); }; - refund.onFail = (fail) => { + refund.onFail = showError(i18n.str`Refund failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -164,14 +163,12 @@ export default function OrderList({ default: assertUnreachable(fail); } - }; + }); const [pickDate, setPickDate] = useState(false); const [preferences] = usePreference(); return ( <section class="section is-main-section"> - <LocalNotificationBannerBulma notification={notification} /> - <div style={{ display: "flex", 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 @@ -27,9 +27,8 @@ import { randomRfc3548Base32Key, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -63,7 +62,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>({ otp_algorithm: 0, }); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { state: session, lib } = useSessionContext(); const [showKey, setShowKey] = useState(false); @@ -94,15 +93,15 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const data = hasErrors ? undefined : (state as TalerMerchantApi.OtpDeviceAddDetails); - const create = safeFunctionHandler( - i18n.str`add otp device`, - lib.instance.addOtpDevice.bind(lib.instance), - !session.token || !data ? undefined : [session.token, data], + const create = actionHandler( + /*add otp device*/ + (ct, t, d) => lib.instance.addOtpDevice(t, d), + !session.token || !data ? undefined : ([session.token, data] as const), ); create.onSuccess = (success, token, req) => { onCreated(req); }; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Adding failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -111,11 +110,10 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -193,9 +191,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma onClick={create} submit> + <Button class="button is-success" onClick={create} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx @@ -21,7 +21,6 @@ import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; 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 @@ -25,11 +25,10 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, PaginationControl, - SafeHandlerTemplate, - useLocalNotificationBetter, + SafeHandler, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -56,17 +55,19 @@ export function CardTable({ paginator, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { state: session, lib } = useSessionContext(); const { i18n } = useTranslationContext(); - const remove = safeFunctionHandler( - i18n.str`delete otp device`, - lib.instance.deleteOtpDevice.bind(lib.instance), - ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); + const remove = actionHandler( + /*delete otp device*/ + (ct, t, id) => lib.instance.deleteOtpDevice(t, id), + ).lambda((prev, [id]: [string]) => + !session.token ? undefined! : [session.token, id], + ); - remove.onFail = (fail) => { + remove.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -75,11 +76,10 @@ export function CardTable({ default: assertUnreachable(fail); } - }; + }); + return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class="card has-table"> <header class="card-header"> <p class="card-header-title"> @@ -124,7 +124,7 @@ export function CardTable({ interface TableProps { rowSelection: string[]; instances: Entity[]; - onDelete: SafeHandlerTemplate<[id: string], unknown>; + onDelete: SafeHandler<[id: string], unknown>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; paginator: PaginationControl; @@ -173,12 +173,12 @@ function Table({ <Tooltip text={i18n.str`Delete selected devices from the database`} > - <ButtonBetterBulma + <Button class="button is-danger is-small" onClick={onDelete.withArgs(i.otp_device_id)} > <i18n.Translate>Delete</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </td> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx @@ -26,7 +26,6 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; 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 @@ -26,9 +26,8 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -57,18 +56,20 @@ export function UpdatePage({ device, onUpdated, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>(device); const [showKey, setShowKey] = useState(false); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); 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], + const update = actionHandler( + /*update otp device*/ + (ct, t, i, s) => lib.instance.updateOtpDevice(t, i, s), + !session.token + ? undefined + : ([session.token, device.id, state as Entity] as const), ); update.onSuccess = (suc, t, id, req) => { onUpdated({ ...req, id: device.id }); }; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -79,11 +80,10 @@ export function UpdatePage({ device, onUpdated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -192,9 +192,9 @@ export function UpdatePage({ device, onUpdated, onBack }: Props): VNode { </button> )} <Tooltip text={i18n.str`Confirm operation`}> - <ButtonBetterBulma onClick={update} submit> + <Button class="button is-success" onClick={update} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx @@ -26,7 +26,6 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx @@ -20,8 +20,8 @@ */ import { - ButtonBetterBulma, - SafeHandlerTemplate, + Button, + SafeHandler, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -41,7 +41,7 @@ interface Props { instanceId: string; onBack?: () => void; withoutCurrentPassword?: boolean; - changePassword: SafeHandlerTemplate<[current: string, new: string], any>; + changePassword: SafeHandler<[current: string, new: string], any>; } export function DetailPage({ @@ -138,7 +138,8 @@ export function DetailPage({ : i18n.str`Confirm operation` } > - <ButtonBetterBulma + <Button + class="button is-success" submit onClick={ hasErrors @@ -150,7 +151,7 @@ export function DetailPage({ } > <i18n.Translate>Confirm change</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx @@ -23,15 +23,13 @@ import { } from "@gnu-taler/taler-util"; import { dummyHttpResponse } from "@gnu-taler/taler-util/http"; import { - LocalNotificationBannerBulma, useChallengeHandler, - useLocalNotificationBetter, + useNotificationContext, 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 { SolveMFAChallenges } from "../../../components/SolveMFA.js"; import { useSessionContext } from "../../../context/session.js"; import { useInstanceDetails, @@ -39,8 +37,8 @@ import { } from "../../../hooks/instance.js"; import { LoginPage, TEMP_TEST_TOKEN } from "../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; -import { maybeTryFirstMFA } from "../accounts/create/CreatePage.js"; import { DetailPage } from "./DetailPage.js"; +import { useMerchantChallengeHandlerContext } from "../../../context/challenge.js"; const TALER_SCREEN_ID = 54; @@ -50,7 +48,7 @@ export interface Props { } export default function PasswordPage({ onCancel, onChange }: Props): VNode { - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { state: session, lib } = useSessionContext(); const result = useInstanceDetails(); const instanceId = session.instance; @@ -73,11 +71,12 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { } } const { i18n } = useTranslationContext(); - const mfa = useChallengeHandler(); + const mfa = useMerchantChallengeHandlerContext(); - const changePassword = safeFunctionHandler( - i18n.str`change password`, + const changePassword = actionHandler( + /*change password*/ async ( + ct, token: AccessToken, current: string, next: string, @@ -106,9 +105,6 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { }, { challengeIds }, ); - if (updated.type === "fail" && updated.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, updated.body); - } return updated; }, !session.token ? undefined : [session.token, "", "", []], @@ -117,9 +113,16 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { onChange(); return i18n.str`Password changed`; }; - changePassword.onFail = (fail) => { + changePassword.onFail = showError(i18n.str`Change failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`Change password`, + fail.body, + changePassword.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], prev[2], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -130,34 +133,15 @@ export default function PasswordPage({ onCancel, onChange }: Props): VNode { default: assertUnreachable(fail); } - }; - - const retry = changePassword.lambda((ids: string[]) => [ - changePassword.args![0], - changePassword.args![1], - changePassword.args![2], - ids, - ]); - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - focus - onCompleted={retry} - onCancel={mfa.doCancelChallenge} - /> - ); - } + }); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <DetailPage onBack={onCancel} instanceId={result.body.name} changePassword={changePassword.lambda( - (current: string, next: string) => [ + (prev, [current, next]: [string, string]) => [ changePassword.args![0], current, next, @@ -174,7 +158,7 @@ export function AdminPassword({ onCancel, onChange, }: Props & { instanceId: string }): VNode { - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { state: session, lib } = useSessionContext(); const subInstanceLib = lib.subInstanceApi(instanceId).instance; @@ -199,10 +183,11 @@ export function AdminPassword({ } const { i18n } = useTranslationContext(); - const mfa = useChallengeHandler(); - const changePassword = safeFunctionHandler( - i18n.str`change instance password`, + const mfa = useMerchantChallengeHandlerContext(); + const changePassword = actionHandler( + /*change instance password*/ async ( + ct, token: AccessToken, id: string, pwd: string, @@ -232,9 +217,6 @@ export function AdminPassword({ }, { challengeIds }, ); - if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, resp.body); - } return resp; }, !session.token ? undefined : [session.token, instanceId, "", []], @@ -243,9 +225,16 @@ export function AdminPassword({ onChange(); return i18n.str`Password changed`; }; - changePassword.onFail = (fail) => { + changePassword.onFail = showError(i18n.str`Change failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`Change password`, + fail.body, + changePassword.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], prev[2], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`No enough rights to change the password.`; @@ -254,33 +243,15 @@ export function AdminPassword({ default: assertUnreachable(fail); } - }; - const retry = changePassword.lambda((ids: string[]) => [ - changePassword.args![0], - changePassword.args![1], - changePassword.args![2], - ids, - ]); - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - focus - onCompleted={retry} - onCancel={mfa.doCancelChallenge} - /> - ); - } + }); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <DetailPage onBack={onCancel} instanceId={result.body.name} changePassword={changePassword.lambda( - (current: string, next: string) => [ + (prev, [current, next]: [string, string]) => [ changePassword.args![0], changePassword.args![1], next, 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 @@ -25,10 +25,9 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, - useTranslationContext, + Button, + useNotificationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -62,16 +61,16 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const hasErrors = errors !== undefined; const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const data = !!errors ? undefined : (state as TalerMerchantApi.PotAddRequest); - const create = safeFunctionHandler( - i18n.str`create money pot`, - lib.instance.createMoneyPot.bind(lib.instance), - !session.token || !data ? undefined : [session.token, data], + const create = actionHandler( + /*create money pot*/ + (ct, t, d) => lib.instance.createMoneyPot(t, d), + !session.token || !data ? undefined : ([session.token, data] as const), ); create.onSuccess = onCreated; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -82,11 +81,10 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -119,9 +117,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={create}> + <Button class="button is-success" submit onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -25,11 +25,10 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, PaginationControl, - SafeHandlerTemplate, - useLocalNotificationBetter, + SafeHandler, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -59,14 +58,16 @@ export function CardTable({ const { i18n } = useTranslationContext(); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const remove = safeFunctionHandler( - i18n.str`delete money pot`, - lib.instance.deleteMoneyPot.bind(lib.instance), - ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); + const remove = actionHandler( + /*delete money pot*/ + (ct, t, id) => lib.instance.deleteMoneyPot(t, id), + ).lambda((prev, [id]: [string]) => + !session.token ? undefined! : ([session.token, id] as const), + ); remove.onSuccess = () => i18n.str`Money pot deleted`; - remove.onFail = (fail) => { + remove.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -75,11 +76,9 @@ export function CardTable({ default: assertUnreachable(fail); } - }; + }); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class="card has-table"> <header class="card-header"> <p class="card-header-title"> @@ -124,7 +123,7 @@ export function CardTable({ interface TableProps { rowSelection: string[]; instances: Entity[]; - onDelete: SafeHandlerTemplate<[id: string], unknown>; + onDelete: SafeHandler<[id: string], unknown>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; paginator: PaginationControl; @@ -173,12 +172,12 @@ function Table({ <Tooltip text={i18n.str`Delete selected pots from the database`} > - <ButtonBetterBulma + <Button class="button is-danger is-small" onClick={onDelete.withArgs(String(i.pot_serial))} > <i18n.Translate>Delete</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </td> 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 @@ -31,9 +31,8 @@ import { TranslatedString, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -70,7 +69,7 @@ export function UpdatePage({ moneyPot, onUpdated, onBack }: Props): VNode { new_pot_totals: moneyPot.pot_totals, pot_name: moneyPot.pot_name, }); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const data = state as TalerMerchantApi.PotModifyRequest; function isAmountValid( @@ -115,13 +114,13 @@ export function UpdatePage({ moneyPot, onUpdated, onBack }: Props): VNode { : undefined, }); - const update = safeFunctionHandler( - i18n.str`update money pot`, - lib.instance.updateMoneyPot.bind(lib.instance), - !token || errors ? undefined : [token, moneyPot.id, data], + const update = actionHandler( + /*update money pot*/ + (ct, t, id, d) => lib.instance.updateMoneyPot(t, id, d), + !token || errors ? undefined : ([token, moneyPot.id, data] as const), ); update.onSuccess = onUpdated; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -132,12 +131,10 @@ export function UpdatePage({ moneyPot, onUpdated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> - <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -195,9 +192,9 @@ export function UpdatePage({ moneyPot, onUpdated, onBack }: Props): VNode { </button> )} <Tooltip text={i18n.str`Confirm operation`}> - <ButtonBetterBulma onClick={update}> + <Button class="button is-success" onClick={update}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </div> 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 @@ -27,9 +27,8 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; @@ -59,14 +58,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { 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], + const { actionHandler, showError } = useNotificationContext(); + const create = actionHandler( + /*add product*/ + (ct, t, f) => lib.instance.addProduct(t, f), + !session.token || !form ? undefined : ([session.token, form] as const), ); create.onSuccess = onCreate; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Add failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -83,7 +82,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); const potsResult = useInstanceMoneyPots(); const groupsResults = useInstanceProductGroups(); @@ -112,7 +111,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : potsResult.body.pots; return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <MissingBankAccountsWarning /> <LimitedKycActionWarning /> <section class="section is-main-section"> @@ -138,9 +136,9 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma onClick={create} submit> + <Button class="button is-success" onClick={create} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </ProductForm> 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 @@ -20,6 +20,7 @@ */ import { + AccessToken, AmountJson, AmountString, Amounts, @@ -31,7 +32,7 @@ import { import { PaginationControl, RenderAmountBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; @@ -143,13 +144,18 @@ function Table({ const { i18n } = useTranslationContext(); const [preference] = usePreference(); const { state: session, lib, config } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const update = safeFunctionHandler( - i18n.str`update product`, - lib.instance.updateProduct.bind(lib.instance), + const { actionHandler, showError } = useNotificationContext(); + const update = actionHandler( + /*update product*/ + ( + ct, + t: AccessToken, + id: string, + b: TalerMerchantApi.ProductPatchDetailRequest, + ) => lib.instance.updateProduct(t, id, b), ); update.onSuccess = () => rowSelectionHandler(undefined); - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -160,7 +166,7 @@ function Table({ default: assertUnreachable(fail); } - }; + }); return ( <div class=""> <PaginationRow paginator={paginator} /> 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 @@ -26,8 +26,7 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; @@ -56,23 +55,23 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { (TalerMerchantApi.ProductDetailResponse & WithId) | null >(null); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { i18n } = useTranslationContext(); const [forceDeletion, setForceDeletion] = useState(false); - const remove = safeFunctionHandler( - i18n.str`delete product`, - lib.instance.deleteProduct.bind(lib.instance), + const remove = actionHandler( + /*delete product*/ + (ct, t, id, opts) => lib.instance.deleteProduct(t, id, opts), !session.token || !deleting ? undefined - : [session.token, deleting.id, { force: forceDeletion }], + : ([session.token, deleting.id, { force: forceDeletion }] as const), ); remove.onSuccess = (suc, t, id) => { setDeleting(null); setForceDeletion(false); return i18n.str`Product (ID: ${id}) has been deleted`; }; - remove.onFail = (fail) => { + remove.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -84,7 +83,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { default: assertUnreachable(fail); } - }; + }); if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />; @@ -105,8 +104,6 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { return ( <section class="section is-main-section"> - <LocalNotificationBannerBulma notification={notification} /> - <div style={{ marginBottom: 10 }}> <JumpToElementById onSelect={onSelect} 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 @@ -26,9 +26,8 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; @@ -58,16 +57,16 @@ export function UpdatePage({ product, onBack, onConfirm }: Props): VNode { useState<TalerMerchantApi.ProductPatchDetailRequest>(); const { i18n } = useTranslationContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const update = safeFunctionHandler( - i18n.str`update product`, - lib.instance.updateProduct.bind(lib.instance), + const { actionHandler, showError } = useNotificationContext(); + const update = actionHandler( + /*update product*/ + (ct, t, id, b) => lib.instance.updateProduct(t, id, b), !session.token || !form ? undefined - : [session.token, product.product_id, form], + : ([session.token, product.product_id, form] as const), ); update.onSuccess = onConfirm; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -78,7 +77,7 @@ export function UpdatePage({ product, onBack, onConfirm }: Props): VNode { default: assertUnreachable(fail); } - }; + }); // 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 @@ -107,7 +106,6 @@ export function UpdatePage({ product, onBack, onConfirm }: Props): VNode { return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -149,9 +147,9 @@ export function UpdatePage({ product, onBack, onConfirm }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma onClick={update} submit> + <Button class="button is-success" onClick={update} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </ProductForm> 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 @@ -25,10 +25,9 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, NotificationCardBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; @@ -94,18 +93,18 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { }); const hasErrors = errors !== undefined; - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const data = !!errors ? undefined : (state as TalerMerchantApi.ReportAddRequest); - const create = safeFunctionHandler( - i18n.str`create scheduled report`, - lib.instance.createScheduledReport.bind(lib.instance), - !session.token || !data ? undefined : [session.token, data], + const create = actionHandler( + /*create scheduled report*/ + (ct, t, b) => lib.instance.createScheduledReport(t, b), + !session.token || !data ? undefined : ([session.token, data] as const), ); create.onSuccess = onCreated; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -114,7 +113,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); if (noGenerators) { return ( <NotificationCardBulma @@ -128,7 +127,6 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { } return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -205,9 +203,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={create}> + <Button class="button is-success" submit onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -26,11 +26,10 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, PaginationControl, - SafeHandlerTemplate, - useLocalNotificationBetter, + SafeHandler, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -61,14 +60,16 @@ export function CardTable({ const { i18n } = useTranslationContext(); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const remove = safeFunctionHandler( - i18n.str`delete scheduled report`, - lib.instance.deleteScheduledReport.bind(lib.instance), - ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); + const remove = actionHandler( + /*delete scheduled report*/ + (ct, t, id) => lib.instance.deleteScheduledReport(t, id), + ).lambda((prev, [id]: [string]) => + !session.token ? undefined! : [session.token, id], + ); remove.onSuccess = () => i18n.str`Scheduled report deleted`; - remove.onFail = (fail) => { + remove.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -77,11 +78,9 @@ export function CardTable({ default: assertUnreachable(fail); } - }; + }); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class="card has-table"> <header class="card-header"> <p class="card-header-title"> @@ -126,7 +125,7 @@ export function CardTable({ interface TableProps { rowSelection: string[]; instances: Entity[]; - onDelete: SafeHandlerTemplate<[id: string], unknown>; + onDelete: SafeHandler<[id: string], unknown>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; paginator: PaginationControl; @@ -178,12 +177,12 @@ function Table({ <Tooltip text={i18n.str`Delete selected scheduled report from the database`} > - <ButtonBetterBulma + <Button class="button is-danger is-small" onClick={onDelete.withArgs(String(i.report_serial))} > <i18n.Translate>Delete</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </td> 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 @@ -25,10 +25,9 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, NotificationCardBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -128,15 +127,15 @@ export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { const hasErrors = errors !== undefined; - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); 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], + const update = actionHandler( + /*update scheduled report*/ + (ct, t, id, d) => lib.instance.updateScheduledReport(t, id, d), + !token || hasErrors ? undefined : ([token, report.id, data] as const), ); update.onSuccess = onUpdated; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -145,7 +144,7 @@ export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); if (noGenerators) { return ( @@ -160,7 +159,6 @@ export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { } return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -248,9 +246,9 @@ export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { </button> )} <Tooltip text={i18n.str`Confirm operation`}> - <ButtonBetterBulma submit onClick={update}> + <Button class="button is-success" submit onClick={update}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -31,9 +31,8 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -94,7 +93,7 @@ export function CreatePage({ const { i18n } = useTranslationContext(); const { config, state: session, lib } = useSessionContext(); const devices = useInstanceOtpDevices(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const [state, setState] = useState<Partial<Entity>>({ minimum_age: 0, @@ -193,14 +192,16 @@ export function CreatePage({ otp_id: state.otpId!, }; - const create = safeFunctionHandler( - i18n.str`add template`, - lib.instance.addTemplate.bind(lib.instance), - !session.token || !data || hasErrors ? undefined : [session.token, data], + const create = actionHandler( + /*add template*/ + (ct, t, d) => lib.instance.addTemplate(t, d), + !session.token || !data || hasErrors + ? undefined + : ([session.token, data] as const), ); create.onSuccess = onCreated; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`You don't have enough permissions.`; @@ -211,7 +212,7 @@ export function CreatePage({ default: assertUnreachable(fail); } - }; + }); const deviceList = !devices || devices instanceof TalerError || devices.type === "fail" @@ -227,8 +228,6 @@ export function CreatePage({ return ( <div> - <LocalNotificationBannerBulma notification={notification} /> - <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -356,9 +355,9 @@ export function CreatePage({ : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={create}> + <Button class="button is-success" submit onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -26,8 +26,7 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; @@ -60,7 +59,7 @@ export default function ListTemplates({ const { i18n } = useTranslationContext(); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const result = useInstanceTemplates(); const [deleting, setDeleting] = useState<TalerMerchantApi.TemplateEntry | null>(null); @@ -83,17 +82,17 @@ export default function ListTemplates({ } } - const remove = safeFunctionHandler( - i18n.str`delete template`, - lib.instance.deleteTemplate.bind(lib.instance), + const remove = actionHandler( + /*delete template*/ + (ct, t, id) => lib.instance.deleteTemplate(t, id), !session.token || !deleting ? undefined - : [session.token, deleting.template_id], + : ([session.token, deleting.template_id] as const), ); remove.onSuccess = () => { setDeleting(null); }; - remove.onFail = (fail) => { + remove.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -102,12 +101,10 @@ export default function ListTemplates({ default: assertUnreachable(fail); } - }; + }); return ( <section class="section is-main-section"> - <LocalNotificationBannerBulma notification={notification} /> - <div style={{ marginBottom: 10 }}> <JumpToElementById onSelect={onSelect} 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 @@ -32,11 +32,10 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, NotificationCardBulma, undefinedIfEmpty, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -100,7 +99,7 @@ function changeToCurrency( function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { config, state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); if (template.template_contract.template_type !== TemplateType.FIXED_ORDER) { return <Fragment />; @@ -232,13 +231,15 @@ function UpdateFixedOrderPage({ 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], + const update = actionHandler( + /*update template*/ + (ct, t, id, d) => lib.instance.updateTemplate(t, id, d), + !session.token || !!errors + ? undefined + : ([session.token, template.id, data] as const), ); update.onSuccess = onUpdated; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -249,11 +250,10 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -405,9 +405,9 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={update}> + <Button class="button is-success" submit onClick={update}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -29,10 +29,9 @@ import { UsingTemplateDetailsRequest, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, undefinedIfEmpty, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -94,7 +93,7 @@ function UseFixedOrderPage({ }: Props): VNode { const { i18n } = useTranslationContext(); const { lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); if (template.template_contract.template_type !== TemplateType.FIXED_ORDER) { return <Fragment />; @@ -126,15 +125,15 @@ function UseFixedOrderPage({ amount: template.template_contract.amount ? undefined : state.amount, 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], + const useTemplate = actionHandler( + /*create order from template*/ + (ct, id, d) => lib.instance.useTemplateCreateOrder(id, d), + !!errors ? undefined : ([id, details] as const), ); useTemplate.onSuccess = (success) => { onOrderCreated(success.order_id); }; - useTemplate.onFail = (fail) => { + useTemplate.onFail = showError(i18n.str`Use failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -149,11 +148,10 @@ function UseFixedOrderPage({ default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -213,9 +211,13 @@ function UseFixedOrderPage({ : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={useTemplate}> + <Button + class="button is-success" + submit + onClick={useTemplate} + > <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx @@ -26,9 +26,7 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, - useLocalNotificationBetter, - useTranslationContext, + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; 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 @@ -29,10 +29,9 @@ import { TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, undefinedIfEmpty, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { addDays, addMonths, endOfMonth, format, startOfMonth } from "date-fns"; @@ -80,7 +79,7 @@ const YEAR = TalerProtocolDuration.fromSpec({ days: 365 }); export function CreatePage({ onCreated, onBack }: Props): VNode { const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { i18n } = useTranslationContext(); const [value, valueHandler] = useState<Partial<Entity>>({ @@ -125,13 +124,15 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { kind: !value.kind ? i18n.str`Required` : undefined, }); - const create = safeFunctionHandler( - i18n.str`create token family`, - lib.instance.createTokenFamily.bind(lib.instance), - !session.token || !!errors ? undefined : [session.token, value as Entity], + const create = actionHandler( + /*create token family*/ + (ct, t, v) => lib.instance.createTokenFamily(t, v), + !session.token || !!errors + ? undefined + : ([session.token, value as Entity] as const), ); create.onSuccess = onCreated; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -142,12 +143,11 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); const [preferences] = usePreference(); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -273,9 +273,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma onClick={create} submit> + <Button class="button is-success" onClick={create} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -26,8 +26,7 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { - LocalNotificationBannerBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; @@ -57,7 +56,7 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { const { i18n } = useTranslationContext(); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); if (!result) return <Loading />; if (result instanceof TalerError) { @@ -77,16 +76,18 @@ 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], + const remove = actionHandler( + /*delete token family*/ + (ct, t, s) => lib.instance.deleteTokenFamily(t, s), + !session.token || !deleting + ? undefined + : ([session.token, deleting.slug] as const), ); remove.onSuccess = () => { setDeleting(null); return i18n.str`Token family has been deleted.`; }; - remove.onFail = (fail) => { + remove.onFail = showError(i18n.str`Remove failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -95,12 +96,10 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <section class="section is-main-section"> - <LocalNotificationBannerBulma notification={notification} /> - <CardTable instances={result.body.token_families} onCreate={onCreate} 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 @@ -27,9 +27,8 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h } from "preact"; @@ -101,19 +100,19 @@ export function UpdatePage({ onUpdated, onBack, tokenFamily }: Props) { // kind: !value.kind ? i18n.str`Required` : undefined, }); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const hasErrors = errors !== undefined; - const update = safeFunctionHandler( - i18n.str`update token family`, - lib.instance.updateTokenFamily.bind(lib.instance), + const update = actionHandler( + /*update token family*/ + (ct, t, s, v) => lib.instance.updateTokenFamily(t, s, v), !session.token || !!errors ? undefined - : [session.token, tokenFamily.slug, value as Entity], + : ([session.token, tokenFamily.slug, value as Entity] as const), ); update.onSuccess = onUpdated; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -122,13 +121,12 @@ export function UpdatePage({ onUpdated, onBack, tokenFamily }: Props) { default: assertUnreachable(fail); } - }; + }); const [preferences] = usePreference(); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -220,9 +218,9 @@ export function UpdatePage({ onUpdated, onBack, tokenFamily }: Props) { : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={update}> + <Button class="button is-success" submit onClick={update}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/DetailsPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/DetailsPage.tsx @@ -8,10 +8,9 @@ import { TalerError, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, RenderAmountBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; @@ -107,13 +106,13 @@ function DetailsPageInternal({ const { state: session, lib, config } = useSessionContext(); const [preferences] = usePreference(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const confirm = safeFunctionHandler( - i18n.str`inform wire transfer`, - lib.instance.informWireTransfer.bind(lib.instance), + const { actionHandler, showError } = useNotificationContext(); + const confirm = actionHandler( + /*inform wire transfer*/ + (ct, t, b) => lib.instance.informWireTransfer(t, b), !session.token || !wt || wt.confirmed ? undefined - : [ + : ([ session.token, { credit_amount: wt.expected_credit_amount!, @@ -121,12 +120,12 @@ function DetailsPageInternal({ payto_uri: wt.payto_uri, wtid: wt.wtid, }, - ], + ] as const), ); confirm.onSuccess = () => { onBack(); }; - confirm.onFail = (fail) => { + confirm.onFail = showError(i18n.str`Confirm failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -137,7 +136,7 @@ function DetailsPageInternal({ default: assertUnreachable(fail); } - }; + }); const zero = Amounts.zeroOfAmount( Amounts.parseOrThrow(wt.expected_credit_amount!), @@ -168,7 +167,6 @@ function DetailsPageInternal({ const orders = Object.keys(merged); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> {!wt.confirmed ? ( <p> <i18n.Translate> @@ -231,9 +229,9 @@ function DetailsPageInternal({ <i18n.Translate>Cancel</i18n.Translate> </button> - <ButtonBetterBulma submit onClick={confirm}> + <Button class="button is-success" submit onClick={confirm}> <i18n.Translate>I have received the wire transfer</i18n.Translate> - </ButtonBetterBulma> + </Button> </div> {!orders.length ? undefined : ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -27,11 +27,10 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, PaginationControl, RenderAmountBulma, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; @@ -64,11 +63,11 @@ export function CardTableIncoming({ const { config } = useSessionContext(); const { state: session, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const confirm = safeFunctionHandler( - i18n.str`inform wire transfer`, - (at: AccessToken, wt: TalerMerchantApi.ExpectedTransferEntry) => + const confirm = actionHandler( + /*inform wire transfer*/ + (ct, at: AccessToken, wt: TalerMerchantApi.ExpectedTransferEntry) => lib.instance.informWireTransfer(at, { credit_amount: wt.expected_credit_amount!, exchange_url: wt.exchange_url, @@ -79,7 +78,7 @@ export function CardTableIncoming({ confirm.onSuccess = () => { revalidateInstanceIncomingTransfers(); }; - confirm.onFail = (fail) => { + confirm.onFail = showError(i18n.str`Confirm failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -90,11 +89,9 @@ export function CardTableIncoming({ default: assertUnreachable(fail); } - }; + }); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class="card has-table"> <header class="card-header"> <p class="card-header-title"> @@ -178,7 +175,7 @@ export function CardTableIncoming({ <Tooltip text={i18n.str`You confirm that the incoming wire transfer has arrived into your bank account.`} > - <ButtonBetterBulma + <Button class="button is-success is-small" onClick={ !session.token @@ -187,7 +184,7 @@ export function CardTableIncoming({ } > Confirm - </ButtonBetterBulma> + </Button> </Tooltip> </div> )} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx @@ -21,10 +21,9 @@ import { AccessToken, HttpStatusCode } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, useChallengeHandler, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -35,11 +34,10 @@ import { } 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"; import { Tooltip } from "../../../components/Tooltip.js"; import { useSessionContext } from "../../../context/session.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; -import { maybeTryFirstMFA } from "../accounts/create/CreatePage.js"; +import { useMerchantChallengeHandlerContext } from "../../../context/challenge.js"; const TALER_SCREEN_ID = 73; @@ -74,33 +72,32 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode { const text = i18n.str`You are deleting the instance with ID "${instanceId}"`; - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); + const { actionHandler, showError } = useNotificationContext(); + const mfa = useMerchantChallengeHandlerContext(); - const remove = safeFunctionHandler( - i18n.str`delete current instance`, - async (token: AccessToken, purge: boolean, challengeIds: string[]) => { + const remove = actionHandler( + /*delete current instance*/ + async (ct, token: AccessToken, purge: boolean, challengeIds: string[]) => { const resp = await lib.instance.deleteCurrentInstance(token, { purge, challengeIds, }); - if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA( - lib.instance, - mfa, - resp.body, - remove.lambda((ids: string[]) => [token, purge, ids]), - ); - } return resp; }, !session.token ? undefined : [session.token, form.purge!!, []], ); remove.onSuccess = onDeleted; - remove.onFail = (fail, t, p) => { + remove.onFail = showError(i18n.str`Remove failed`, (fail, t, p) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`Delete current instance`, + fail.body, + remove.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -109,22 +106,10 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode { case HttpStatusCode.Conflict: return i18n.str`Conflict.`; } - }; - if (mfa.pendingChallenge && mfa.repeatCall) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - focus - onCompleted={mfa.repeatCall} - onCancel={mfa.doCancelChallenge} - /> - ); - } + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -168,13 +153,13 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma + <Button class="button is-small is-danger" submit onClick={remove} > <i18n.Translate>DELETE</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -28,10 +28,9 @@ import { TanChannel, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, useChallengeHandler, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; @@ -42,16 +41,15 @@ import { TalerForm, } from "../../../components/form/FormProvider.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; -import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; import { useSessionContext } from "../../../context/session.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; import { CopyButton } from "../../../components/modal/index.js"; import { Tooltip } from "../../../components/Tooltip.js"; -import { maybeTryFirstMFA } from "../accounts/create/CreatePage.js"; import { EMAIL_REGEX, PHONE_JUST_NUMBERS_REGEX, } from "../../../utils/constants.js"; +import { useMerchantChallengeHandlerContext } from "../../../context/challenge.js"; const TALER_SCREEN_ID = 75; @@ -175,21 +173,19 @@ export function UpdatePage({ : default_wire_transfer_delay, ...rest, }; - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); + const { actionHandler, showError } = useNotificationContext(); + const mfa = useMerchantChallengeHandlerContext(); const cleanForm = deepEqual(result, initial); - const update = safeFunctionHandler( - i18n.str`update instance settings`, + const update = actionHandler( + /*update instance settings*/ async ( + ct, token: AccessToken, d: TalerMerchantApi.InstanceReconfigurationMessage, challengeIds: string[], ) => { const resp = await doUpdate(token, d, { challengeIds }); - if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, resp.body); - } return resp; }, hasErrors || cleanForm || !state.token @@ -197,9 +193,16 @@ export function UpdatePage({ : [state.token, result, []], ); update.onSuccess = onConfirm; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`Update settings`, + fail.body, + update.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -208,28 +211,10 @@ export function UpdatePage({ default: assertUnreachable(fail); } - }; - const retry = update.lambda((challengesIds: string[]) => [ - update.args![0], - update.args![1], - challengesIds, - ]); - - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - focus - onCompleted={retry} - onCancel={mfa.doCancelChallenge} - /> - ); - } + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -306,9 +291,9 @@ export function UpdatePage({ : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={update}> + <Button class="button is-success" submit onClick={update}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -25,9 +25,8 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -68,7 +67,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>({}); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const { state: session, lib } = useSessionContext(); const errors = undefinedIfEmpty<FormErrors<Entity>>({ @@ -95,13 +94,13 @@ 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], + const create = actionHandler( + /*add webhook*/ + (ct, t, d) => lib.instance.addWebhook(t, d), + !session.token || hasErrors ? undefined : ([session.token, data] as const), ); create.onSuccess = onCreate; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -110,11 +109,10 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -302,9 +300,9 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma submit onClick={create}> + <Button class="button is-success" submit onClick={create}> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> 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 @@ -26,17 +26,16 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, PaginationControl, - SafeHandlerTemplate, - useLocalNotificationBetter, - useTranslationContext, + SafeHandler, + useNotificationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; -import { useSessionContext } from "../../../../context/session.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { useSessionContext } from "../../../../context/session.js"; import { PaginationRow } from "../../orders/list/Table.js"; const TALER_SCREEN_ID = 77; @@ -62,14 +61,14 @@ export function CardTable({ const { state, lib } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); - const deleteWebhook = safeFunctionHandler( - i18n.str`delete webhook`, - lib.instance.deleteWebhook.bind(lib.instance), + const deleteWebhook = actionHandler( + /*delete webhook*/ + (ct, t: AccessToken, id: string) => lib.instance.deleteWebhook(t, id), ); deleteWebhook.onSuccess = () => i18n.str`Webhook deleted successfully`; - deleteWebhook.onFail = (fail) => { + deleteWebhook.onFail = showError(i18n.str`Delete failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -78,13 +77,12 @@ export function CardTable({ default: assertUnreachable(fail); } - }; - const deleteById = deleteWebhook.lambda((id: string) => + }); + const deleteById = deleteWebhook.lambda((prev, [id]: [string]) => !state.token ? undefined : [state.token, id], ); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <div class="card has-table"> <header class="card-header"> <p class="card-header-title"> @@ -130,7 +128,7 @@ export function CardTable({ interface TableProps { rowSelection: string[]; instances: Entity[]; - deleteWebhook: SafeHandlerTemplate<[id: string], any>; + deleteWebhook: SafeHandler<[id: string], any>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; paginator: PaginationControl; @@ -179,12 +177,12 @@ function Table({ <Tooltip text={i18n.str`Delete selected webhook from the database`} > - <ButtonBetterBulma + <Button class="button is-danger is-small" onClick={deleteWebhook.withArgs(i.webhook_id)} > <i18n.Translate>Delete</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </td> 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 @@ -25,9 +25,8 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; @@ -69,7 +68,7 @@ export function UpdatePage({ webhook, onConfirm, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { state: session, lib, logIn } = useSessionContext(); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { actionHandler, showError } = useNotificationContext(); const [state, setState] = useState<Partial<Entity>>(webhook); @@ -95,16 +94,18 @@ export function UpdatePage({ webhook, onConfirm, onBack }: Props): VNode { const hasErrors = errors !== undefined; 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], + const update = actionHandler( + /*update webhook*/ + (ct, t, w, d) => lib.instance.updateWebhook(t, w, d), + !session.token || !!errors + ? undefined + : ([session.token, webhook.id, data] as const), ); update.onSuccess = (success) => { onConfirm(); return i18n.str`Webhook updated`; }; - update.onFail = (fail) => { + update.onFail = showError(i18n.str`Update failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.`; @@ -115,11 +116,10 @@ export function UpdatePage({ webhook, onConfirm, onBack }: Props): VNode { default: assertUnreachable(fail); } - }; + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -225,9 +225,9 @@ export function UpdatePage({ webhook, onConfirm, onBack }: Props): VNode { : i18n.str`Confirm operation` } > - <ButtonBetterBulma onClick={update} submit> + <Button class="button is-success" onClick={update} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </Tooltip> </div> </FormProvider> diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -28,20 +28,17 @@ import { TranslatedString, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useChallengeHandler, - useLocalNotificationBetter, + Button, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { SolveMFAChallenges } from "../../components/SolveMFA.js"; -import { useSessionContext } from "../../context/session.js"; import { FormProvider } from "../../components/form/FormProvider.js"; import { doAutoFocus } from "../../components/form/Input.js"; import { Tooltip } from "../../components/Tooltip.js"; -import { maybeTryFirstMFA } from "../instance/accounts/create/CreatePage.js"; +import { useMerchantChallengeHandlerContext } from "../../context/challenge.js"; +import { useSessionContext } from "../../context/session.js"; const TALER_SCREEN_ID = 79; @@ -72,16 +69,15 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { const [username, setUsername] = useState( showCreateAccount ? "" : state.instance, ); - const { i18n } = useTranslationContext(); const [hidePassword, setHidePassword] = useState(true); - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); + const { actionHandler, showError } = useNotificationContext(); + const mfa = useMerchantChallengeHandlerContext(); - const login = safeFunctionHandler( - i18n.str`login`, - async (usr: string, pwd: string, challengeIds: string[]) => { + const login = actionHandler( + /*login*/ + async (ct, usr: string, pwd: string, challengeIds?: string[]) => { const api = getInstanceForUsername(usr); const resp = await api.createAccessToken( usr, @@ -89,19 +85,23 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { FOREVER_REFRESHABLE_TOKEN(i18n.str`Logged in`), { challengeIds }, ); - if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, resp.body); - } return resp; }, - !username || !password ? undefined : [username, password, []], + !username || !password ? undefined : [username, password], ); login.onSuccess = (success, usr) => { logIn(usr, success.access_token); }; - login.onFail = (fail) => { + login.onFail = showError(i18n.str`Login failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`New session`, + fail.body, + login.lambda((prev, [ids]) => + !prev ? undefined : [prev[0], prev[1], ids], + ), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Wrong password.`; @@ -110,29 +110,10 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { default: assertUnreachable(fail); } - }; - const retry = login.lambda((ids: string[]) => [ - login.args![0], - login.args![1], - ids, - ]); - - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - initial={mfa.initial} - onCompleted={retry} - onCancel={mfa.doCancelChallenge} - focus - /> - ); - } + }); return ( <div> - <LocalNotificationBannerBulma notification={notification} /> - <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> <header @@ -143,7 +124,7 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { <i18n.Translate>Login required</i18n.Translate> </p> </header> - <FormProvider> + <form> <section class="modal-card-body" style={{ @@ -248,11 +229,11 @@ export function LoginPage({ showCreateAccount, focus }: Props): VNode { <i18n.Translate>Forgot password</i18n.Translate> </a> )} - <ButtonBetterBulma onClick={login} submit> + <Button class="button is-success" onClick={login} submit> <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> + </Button> </footer> - </FormProvider> + </form> </div> </div> {!showCreateAccount ? undefined : ( diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx @@ -29,12 +29,10 @@ import { } from "@gnu-taler/taler-util"; import { buildStorageKey, - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, undefinedIfEmpty, - useChallengeHandler, - useLocalNotificationBetter, useLocalStorage, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -47,14 +45,13 @@ import { Input } from "../../components/form/Input.js"; import { InputPassword } from "../../components/form/InputPassword.js"; import { InputToggle } from "../../components/form/InputToggle.js"; import { InputWithAddon } from "../../components/form/InputWithAddon.js"; -import { SolveMFAChallenges } from "../../components/SolveMFA.js"; +import { useMerchantChallengeHandlerContext } from "../../context/challenge.js"; import { useSessionContext } from "../../context/session.js"; import { EMAIL_REGEX, INSTANCE_ID_REGEX, PHONE_JUST_NUMBERS_REGEX, } from "../../utils/constants.js"; -import { maybeTryFirstMFA } from "../instance/accounts/create/CreatePage.js"; const TALER_SCREEN_ID = 80; @@ -179,8 +176,8 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { saveForm(saved); } - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); + const { actionHandler, showError } = useNotificationContext(); + const mfa = useMerchantChallengeHandlerContext(); const request: InstanceConfigurationMessage = { address: {}, @@ -203,16 +200,13 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { phone_number: value.phone, }; - const create = safeFunctionHandler( - i18n.str`self provision instance`, - async (req: InstanceConfigurationMessage, challengeIds: string[]) => { + const create = actionHandler( + /*self provision instance*/ + async (ct, req: InstanceConfigurationMessage, challengeIds: string[]) => { const resp = await lib.instance.createInstanceSelfProvision(req, { challengeIds, tokenValidity: Duration.fromSpec({ months: 6 }), }); - if (resp.type === "fail" && resp.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, resp.body); - } return resp; }, !!errors ? undefined : [request, []], @@ -224,9 +218,14 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { saveForm({}); onCreated(); }; - create.onFail = (fail) => { + create.onFail = showError(i18n.str`Create failed`, (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`Self provision`, + fail.body, + create.lambda((prev, [ids]) => (!prev ? undefined : [prev[0], ids])), + ); return undefined; case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized`; @@ -235,54 +234,10 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { default: assertUnreachable(fail); } - }; - const retry = create.lambda((ids: string[]) => [create.args![0], ids]); - - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - onCompleted={retry} - initial={mfa.initial} - // currentChallenge={{ - // challenges: [ - // { - // challenge_id: "1", - // tan_channel: TanChannel.EMAIL, - // tan_info: "zxc", - // }, - // { - // challenge_id: "2", - // tan_channel: TanChannel.EMAIL, - // tan_info: "aasd", - // }, - // ], - // combi_and: false, - // }} - // initial={{ - // request:{ - // challenge_id: "1", - // tan_channel: TanChannel.EMAIL, - // tan_info: "asd" - // }, - // response: { - // earliest_retransmission: TalerProtocolTimestamp.never(), - // solve_expiration: TalerProtocolTimestamp.never(), - // } - // }} - focus - showFull={{ - [TanChannel.EMAIL]: value.email, - [TanChannel.SMS]: value.phone, - }} - onCancel={mfa.doCancelChallenge} - /> - ); - } + }); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> <FormProvider<Account> @@ -386,9 +341,9 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { > <i18n.Translate>Cancel</i18n.Translate> </button> - <ButtonBetterBulma onClick={create} submit> + <Button class="button is-success" onClick={create} submit> <i18n.Translate>Create</i18n.Translate> - </ButtonBetterBulma> + </Button> </footer> </FormProvider> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx @@ -18,14 +18,12 @@ import { assertUnreachable, HttpStatusCode, MerchantAuthMethod, - opFixedSuccess, TalerErrorCode, } from "@gnu-taler/taler-util"; import { - ButtonBetterBulma, - LocalNotificationBannerBulma, + Button, useChallengeHandler, - useLocalNotificationBetter, + useNotificationContext, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; @@ -34,13 +32,10 @@ import { FormErrors, FormProvider, } from "../../components/form/FormProvider.js"; -import { Input } from "../../components/form/Input.js"; -import { SolveMFAChallenges } from "../../components/SolveMFA.js"; +import { InputPassword } from "../../components/form/InputPassword.js"; import { useSessionContext } from "../../context/session.js"; -import { FOREVER_REFRESHABLE_TOKEN } from "../login/index.js"; import { undefinedIfEmpty } from "../../utils/table.js"; -import { InputPassword } from "../../components/form/InputPassword.js"; -import { maybeTryFirstMFA } from "../instance/accounts/create/CreatePage.js"; +import { useMerchantChallengeHandlerContext } from "../../context/challenge.js"; const TALER_SCREEN_ID = 82; @@ -85,21 +80,18 @@ export function ResetAccount({ }; setValue(v); } - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const mfa = useChallengeHandler(); + const { actionHandler, showError } = useNotificationContext(); + const mfa = useMerchantChallengeHandlerContext(); - const reset = safeFunctionHandler( - i18n.str`reset password for self provision`, - async (password: string, challengeIds: string[]) => { + const reset = actionHandler( + /*reset password for self provision*/ + async (ct, password: string, challengeIds: string[]) => { const forgot = await lib .subInstanceApi(instanceId) .instance.forgotPasswordSelfProvision( { method: MerchantAuthMethod.TOKEN, password }, { challengeIds }, ); - if (forgot.type === "fail" && forgot.case === HttpStatusCode.Accepted) { - await maybeTryFirstMFA(lib.instance, mfa, forgot.body); - } return forgot; }, hasErrors ? undefined : [value.password!, []], @@ -108,14 +100,17 @@ export function ResetAccount({ // logIn(instanceId, suc.access_token); onReseted(); }; - reset.onFail = (fail) => { + reset.onFail = showError(i18n.str`Reset failed`, (fail) => { switch (fail.case) { case TalerErrorCode.MERCHANT_GENERIC_MFA_MISSING: return i18n.str`The instance is not properly configured to allow MFA.`; case HttpStatusCode.Accepted: + mfa.onNewChallenge( + i18n.str`Forgot password`, + fail.body, + reset.lambda((prev, [ids]) => (!prev ? undefined : [prev[0], ids])), + ); return undefined; - // case HttpStatusCode.Unauthorized: - // return i18n.str`Unauthorized.`; case HttpStatusCode.Forbidden: return i18n.str`Forbidden.`; case HttpStatusCode.NotFound: @@ -123,25 +118,10 @@ export function ResetAccount({ default: assertUnreachable(fail); } - }; - - const retry = reset.lambda((ids: string[]) => [reset.args![0], ids]); - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - onCompleted={retry} - initial={mfa.initial} - focus - onCancel={mfa.doCancelChallenge} - /> - ); - } + }); return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> - <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> <header @@ -188,9 +168,9 @@ export function ResetAccount({ <button type="button" class="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> - <ButtonBetterBulma submit onClick={reset}> + <Button class="button is-success" submit onClick={reset}> <i18n.Translate>Reset</i18n.Translate> - </ButtonBetterBulma> + </Button> </footer> </FormProvider> </div> diff --git a/packages/merchant-backoffice-ui/src/scss/main.scss b/packages/merchant-backoffice-ui/src/scss/main.scss @@ -233,3 +233,7 @@ div.buttons.is-right { div.menu.is-menu-main a { display: flex; } +button.button.is-success[data-failed="true"]{ + background-color: #f14668; + border-color: #f14668; +} +\ No newline at end of file