taler-typescript-core

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

commit ffb7f050fd93d65727f0ff76bc8e088438113aef
parent 66b5ad1bf71ca0f06574d697146a54569155573d
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 27 Oct 2025 10:53:38 -0300

merchant update: local notification

Diffstat:
Mpackages/merchant-backoffice-ui/src/Routing.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/components/SolveMFA.tsx | 42++++++++++++++++++++----------------------
Dpackages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx | 61-------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx | 9+++------
Mpackages/merchant-backoffice-ui/src/paths/admin/create/index.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/paths/admin/list/index.tsx | 8++++++--
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/CreatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx | 8++++++--
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 8+++-----
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx | 26++++++++++++++++----------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx | 8++++++--
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx | 7+++----
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx | 9++-------
Mpackages/merchant-backoffice-ui/src/paths/instance/password/index.tsx | 10++++++++--
Mpackages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx | 15++++++---------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx | 6++----
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx | 7+++----
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx | 5++---
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx | 5+++--
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx | 6+++---
Mpackages/merchant-backoffice-ui/src/paths/instance/update/DeletePage.tsx | 10+++++-----
Mpackages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx | 118++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/update/index.tsx | 64+++++++++-------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx | 54++++++++++++++++++++++++++++++++++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx | 45+++++++--------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx | 6------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx | 125+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx | 31+------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx | 53++++++++++++++++++++++++++++++++++++++++-------------
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx | 38++------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 114+++++++++++++++++++++++++++++++------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/newAccount/index.tsx | 112+++++++++++++++++++++++++++++++++++--------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx | 105++++++++++++++++++++++++++++++++++++++-----------------------------------------
62 files changed, 588 insertions(+), 703 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -25,7 +25,7 @@ import { TalerError, TranslatedString, } from "@gnu-taler/taler-util"; -import { urlPattern, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, urlPattern, useTranslationContext } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; import { Fragment, VNode, h } from "preact"; import { Route, Router, route } from "preact-router"; diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx @@ -2,34 +2,32 @@ import { AbsoluteTime, assertUnreachable, Challenge, - ChallengeRequestResponse, ChallengeResponse, - Duration, HttpStatusCode, TalerErrorCode, - TanChannel, + TanChannel } from "@gnu-taler/taler-util"; import { - Time, + ButtonBetterBulma, + LocalNotificationBannerBulma, + SafeHandlerTemplate, undefinedIfEmpty, - useTranslationContext, + 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 { useSessionContext } from "../context/session.js"; -import { Notification } from "../utils/types.js"; -import { AsyncButton } from "./exception/AsyncButton.js"; -import { FormErrors, FormProvider } from "./form/FormProvider.js"; -import { Input } from "./form/Input.js"; -import { NotificationCard } from "./menu/index.js"; import { datetimeFormatForSettings, usePreference, } from "../hooks/preference.js"; -import { format } from "date-fns"; +import { Notification } from "../utils/types.js"; +import { FormErrors, FormProvider } from "./form/FormProvider.js"; +import { Input } from "./form/Input.js"; export interface Props { - onCompleted(challenges: string[]): void; + onCompleted: SafeHandlerTemplate<[challenges: string[]], any>; onCancel(): void; currentChallenge: ChallengeResponse; } @@ -51,7 +49,7 @@ function SolveChallenge({ }): VNode { const { i18n } = useTranslationContext(); const { state: session, lib, logIn } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const [value, setValue] = useState<Partial<Form>>({}); const [showExpired, setExpired] = useState( @@ -142,7 +140,7 @@ function SolveChallenge({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> @@ -220,13 +218,13 @@ function SolveChallenge({ <button class="button" onClick={onCancel}> <i18n.Translate>Back</i18n.Translate> </button> - <AsyncButton + <ButtonBetterBulma type="is-info" disabled={errors !== undefined} onClick={doVerificationImpl} > <i18n.Translate>Verify</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </footer> </div> </div> @@ -242,7 +240,7 @@ export function SolveMFAChallenges({ }: Props): VNode { const { i18n } = useTranslationContext(); const { state: session, lib, logIn } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const [solved, setSolved] = useState<string[]>([]); // FIXME: we should save here also the expiration of the // tan channel to be used when the user press "i have the code" @@ -370,7 +368,7 @@ export function SolveMFAChallenges({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> @@ -455,7 +453,7 @@ export function SolveMFAChallenges({ > <i18n.Translate>I have a code</i18n.Translate> </button> - <AsyncButton + <ButtonBetterBulma disabled={ hasSolvedEnough || solved.indexOf(d.challenge_id) !== -1 || @@ -464,7 +462,7 @@ export function SolveMFAChallenges({ onClick={() => doSendCodeImpl(d)} > <i18n.Translate>Send me a message</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </section> ); @@ -480,13 +478,13 @@ export function SolveMFAChallenges({ <button class="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> - <AsyncButton + <ButtonBetterBulma type="is-info" disabled={!hasSolvedEnough} onClick={async () => onCompleted(solved)} > <i18n.Translate>Complete</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </footer> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx b/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx @@ -1,61 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2024 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ComponentChildren, h } from "preact"; -import { LoadingModal } from "../modal/index.js"; -import { useAsync } from "../../hooks/async.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; - -type Props = { - children: ComponentChildren; - disabled?: boolean; - onClick?: () => Promise<void>; - [rest: string]: any; -}; - -export function AsyncButton({ onClick, disabled, children, ...rest }: Props) { - const { isSlow, isLoading, request, cancel, error } = useAsync(onClick); - const { i18n } = useTranslationContext(); - if (isSlow) { - return <LoadingModal onCancel={cancel} />; - } - if (isLoading) { - return ( - <button class="button"> - <i18n.Translate>Loading...</i18n.Translate> - </button> - ); - } - - return ( - <span {...rest}> - <button class="button is-success" onClick={request} disabled={disabled}> - {children} - </button> - - {!error ? undefined : ( - <div class="message is-danger" style={{padding:2}}> - Unexpected error: {error} - </div> - )} - </span> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -22,19 +22,16 @@ import { Duration, MerchantAuthMethod, - TalerMerchantApi, - createRFC8959AccessTokenPlain, + TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, } from "../../../components/form/FormProvider.js"; import { Input } from "../../../components/form/Input.js"; -import { InputToggle } from "../../../components/form/InputToggle.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; import { usePreference } from "../../../hooks/preference.js"; import { EMAIL_REGEX, INSTANCE_ID_REGEX, PHONE_JUST_NUMBERS_REGEX } from "../../../utils/constants.js"; @@ -223,7 +220,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma onClick={submit} disabled={hasErrors || !hasTokenErrors} data-tooltip={ @@ -233,7 +230,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { } > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx @@ -23,6 +23,7 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, useChallengeHandler, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -43,7 +44,7 @@ interface Props { export type Entity = TalerMerchantApi.InstanceConfigurationMessage; export default function Create({ onBack, onConfirm, forceId }: Props): VNode { - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); const { lib, state, logIn } = useSessionContext(); @@ -105,7 +106,7 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} forceId={forceId} onCreate={onFirstCall} /> </Fragment> diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx @@ -27,7 +27,9 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -58,8 +60,10 @@ export default function Instances({ const result = useBackendInstances(); const [deleting, setDeleting] = useState<TalerMerchantApi.Instance>(); const [purging, setPurging] = useState<boolean>(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); + const { i18n } = useTranslationContext(); const { state, lib } = useSessionContext(); @@ -132,7 +136,7 @@ export default function Instances({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <View instances={result.body.instances} onDelete={(d) => { 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 @@ -28,7 +28,6 @@ import { import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, @@ -212,7 +211,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -222,7 +221,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx @@ -26,8 +26,10 @@ import { TalerMerchantApi, } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, Time, useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -50,12 +52,14 @@ export default function AccessTokenCreatePage({ onBack, }: Props): VNode { const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); const [ok, setOk] = useState<{ token: string; expiration: AbsoluteTime }>(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); + const [doCreate, repeatCreate] = mfa.withMfaHandler( ({ challengeIds, onChallengeRequired }) => async function doCreateImpl(pwd:string, request: Entity) { @@ -106,7 +110,7 @@ export default function AccessTokenCreatePage({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> {!ok ? undefined : ( <ConfirmModal // label={`Confirm`} 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 @@ -38,7 +38,7 @@ import { NotificationCard } from "../../../../components/menu/index.js"; import { ConfirmModal } from "../../../../components/modal/index.js"; import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; interface Props { onCreate: () => void; @@ -49,7 +49,7 @@ export default function AccessTokenListPage({ onCreate }: Props): VNode { const { state, lib } = useSessionContext(); const [deleting, setDeleting] = useState<TokenInfo | null>(null); const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + if (!result) return <Loading />; if (result instanceof TalerError) { @@ -75,7 +75,7 @@ export default function AccessTokenListPage({ onCreate }: Props): VNode { return ( <Fragment> <section class="section is-main-section"> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CardTable tokens={result.body.map((o) => ({ 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,7 +31,6 @@ import { import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, @@ -42,11 +41,10 @@ import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; import { CompareAccountsModal } from "../../../../components/modal/index.js"; +import { usePreference } from "../../../../hooks/preference.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { safeConvertURL } from "../update/UpdatePage.js"; import { TestRevenueErrorType, testRevenueAPI } from "./index.js"; -import { usePreference } from "../../../../hooks/preference.js"; -import { useSettingsContext } from "../../../../context/settings.js"; type Entity = TalerMerchantApi.AccountAddDetails & { verified?: boolean; @@ -323,7 +321,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -333,7 +331,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> 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 @@ -21,7 +21,7 @@ import { AccessToken, - ChallengeResponse, + BasicOrTokenAuth, FacadeCredentials, HttpStatusCode, OperationFail, @@ -30,25 +30,25 @@ import { TalerError, TalerMerchantApi, TalerRevenueHttpClient, - opFixedSuccess, + opFixedSuccess } from "@gnu-taler/taler-util"; +import type { + HttpRequestLibrary +} from "@gnu-taler/taler-util/http"; import { BrowserFetchHttpLib, + LocalNotificationBannerBulma, useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; +import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -import { BasicOrTokenAuth } from "@gnu-taler/taler-util"; -import type { - HttpRequestLibrary, - HeadersImpl, -} from "@gnu-taler/taler-util/http"; -import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; export type Entity = TalerMerchantApi.AccountAddDetails; interface Props { @@ -58,11 +58,17 @@ interface Props { export default function CreateValidator({ onConfirm, onBack }: Props): VNode { const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); + const create = safeFunctionHandler( + (token: AccessToken, request: Entity, challengeIds: string[]) => + lib.instance.addBankAccount(token, request, { challengeIds }), + !state.token ? undefined : [state.token,] + ); const [doCreate, repeatCreate] = mfa.withMfaHandler( ({ challengeIds, onChallengeRequired }) => async function doCreateImpl(request: Entity) { @@ -105,7 +111,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { return ( <> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} onCreate={doCreate} /> </> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -45,7 +45,7 @@ interface Props { export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, lib } = useSessionContext(); const result = useInstanceBankAccounts(); @@ -69,7 +69,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> {result.body.accounts.length < 1 && ( <NotificationCard notification={{ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -34,7 +34,6 @@ import { import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, @@ -389,7 +388,7 @@ export function UpdatePage({ <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -399,7 +398,7 @@ export function UpdatePage({ onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx @@ -29,7 +29,9 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -60,9 +62,11 @@ export default function UpdateValidator({ }: Props): VNode { const { state, lib } = useSessionContext(); const result = useBankAccountDetails(bid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); + const { i18n } = useTranslationContext(); if (!result) return <Loading />; @@ -173,7 +177,7 @@ export default function UpdateValidator({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <UpdatePage account={{ ...result.body, id: bid }} onBack={onBack} 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 @@ -23,7 +23,6 @@ import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, @@ -82,7 +81,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -92,7 +91,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/index.tsx @@ -20,7 +20,7 @@ */ import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; @@ -36,12 +36,12 @@ interface Props { export default function CreateCategory({ onConfirm, onBack }: Props): VNode { const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); return ( <> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} onCreate={async (request: Entity) => { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/index.tsx @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -46,7 +46,7 @@ interface Props { export default function ListCategories({ onCreate, onSelect }: Props): VNode { // const [position, setPosition] = useState<string | undefined>(undefined); const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, lib } = useSessionContext(); const result = useInstanceCategories(); @@ -70,7 +70,7 @@ export default function ListCategories({ onCreate, onSelect }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <CardTable 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 @@ -28,8 +28,8 @@ import { import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; +import emptyImage from "../../../../assets/empty.png"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { useSessionContext } from "../../../../context/session.js"; @@ -39,7 +39,6 @@ import { useInstanceProducts, useInstanceProductsFromIds, } from "../../../../hooks/product.js"; -import emptyImage from "../../../../assets/empty.png"; type Entity = TalerMerchantApi.CategoryProductList & WithId; @@ -139,13 +138,13 @@ export function UpdatePage({ category, onUpdate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={false} data-tooltip={i18n.str`Confirm operation`} onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> <ProductListSmall onSelect={() => {}} list={category.products} /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/index.tsx @@ -24,7 +24,7 @@ import { TalerError, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -48,7 +48,7 @@ export default function UpdateCategory({ onBack, }: Props): VNode { const result = useCategoryDetails(cid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, lib } = useSessionContext(); const { i18n } = useTranslationContext(); @@ -73,7 +73,7 @@ export default function UpdateCategory({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <UpdatePage category={{ ...result.body, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -49,7 +49,7 @@ interface Props { } export default function OrderCreate({ onConfirm, onBack }: Props): VNode { const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const detailsResult = useInstanceDetails(); // FIXME: if the product list is big the will bring a lot of info const inventoryResult = useInstanceProducts(); @@ -92,7 +92,7 @@ export default function OrderCreate({ onConfirm, onBack }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -19,7 +19,7 @@ import { TalerErrorCode, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -39,7 +39,7 @@ export interface Props { export default function Update({ oid, onBack }: Props): VNode { const result = useOrderDetails(oid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, lib } = useSessionContext(); const { i18n } = useTranslationContext(); @@ -67,7 +67,7 @@ export default function Update({ oid, onBack }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <DetailPage onBack={onBack} 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,7 +28,7 @@ import { assertUnreachable, stringifyPayUri, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -66,7 +66,7 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { ); const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); @@ -104,7 +104,7 @@ export default function OrderList({ onCreate, onSelect }: Props): VNode { return ( <section class="section is-main-section"> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <JumpToElementById testIfExist={async (order) => { 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,7 +27,6 @@ import { import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, @@ -159,7 +158,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -169,7 +168,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> 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 @@ -20,7 +20,7 @@ */ import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; @@ -37,7 +37,7 @@ interface Props { export default function CreateValidator({ onConfirm, onBack }: Props): VNode { const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); const [created, setCreated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null); @@ -48,7 +48,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { return ( <> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} onCreate={async (request: Entity) => { 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 @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -46,7 +46,7 @@ interface Props { export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { // const [position, setPosition] = useState<string | undefined>(undefined); const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, lib } = useSessionContext(); const result = useInstanceOtpDevices(); @@ -70,7 +70,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <CardTable 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,7 +26,6 @@ import { import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; @@ -155,13 +154,13 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={false} data-tooltip={i18n.str`Confirm operation`} onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </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,6 +26,7 @@ import { assertUnreachable } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -55,7 +56,7 @@ export default function UpdateValidator({ onBack, }: Props): VNode { const result = useOtpDeviceDetails(vid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const [keyUpdated, setKeyUpdated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null); const { state, lib } = useSessionContext(); @@ -86,7 +87,7 @@ export default function UpdateValidator({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <UpdatePage device={{ id: vid, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/DetailPage.tsx @@ -19,14 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { - AccessToken, - createRFC8959AccessTokenPlain, -} from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { FormProvider } from "../../../components/form/FormProvider.js"; import { Input } from "../../../components/form/Input.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; @@ -137,7 +132,7 @@ export function DetailPage({ <i18n.Translate>Cancel</i18n.Translate> </a> )} - <AsyncButton + <ButtonBetterBulma type="submit" disabled={hasErrors} data-tooltip={ @@ -148,7 +143,7 @@ export function DetailPage({ onClick={submitForm} > <i18n.Translate>Confirm change</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </FormProvider> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/password/index.tsx @@ -22,7 +22,9 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -56,7 +58,9 @@ export default function PasswordPage(props: Props): VNode { const result = useInstanceDetails(); const instanceId = state.instance; + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); + const [doChangePassword, repeatChangePassword] = mfa.withMfaHandler( ({ challengeIds, onChallengeRequired }) => async function changePassword( @@ -132,7 +136,9 @@ export function AdminPassword(props: Props & { instanceId: string }): VNode { const instanceId = props.instanceId; + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); + const [doChangePassword, repeatChangePassword] = mfa.withMfaHandler( ({ challengeIds, onChallengeRequired }) => async function changePassword( @@ -216,7 +222,7 @@ function CommonPassword( ): VNode { const { i18n } = useTranslationContext(); const { state } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + if (!result) return <Loading />; if (result instanceof TalerError) { @@ -245,7 +251,7 @@ function CommonPassword( const id = result.body.name; return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <DetailPage onBack={onCancel} instanceId={result.body.name} 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 @@ -22,7 +22,6 @@ import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { ProductForm } from "../../../../components/product/ProductForm.js"; import { useListener } from "../../../../hooks/listener.js"; @@ -59,7 +58,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma onClick={submitForm} data-tooltip={ !submitForm @@ -69,7 +68,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { disabled={!submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx @@ -20,7 +20,7 @@ */ import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; @@ -35,12 +35,12 @@ interface Props { } export default function CreateProduct({ onConfirm, onBack }: Props): VNode { const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} onCreate={(request: TalerMerchantApi.ProductAddDetail) => { 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 @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -51,7 +51,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { const [deleting, setDeleting] = useState< (TalerMerchantApi.ProductDetail & WithId) | null >(null); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); @@ -75,7 +75,7 @@ export default function ProductList({ onCreate, onSelect }: Props): VNode { return ( <section class="section is-main-section"> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <JumpToElementById testIfExist={async (id) => { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx @@ -22,7 +22,6 @@ import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { ProductForm } from "../../../../components/product/ProductForm.js"; import { useListener } from "../../../../hooks/listener.js"; @@ -78,7 +77,7 @@ export function UpdatePage({ product, onUpdate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma onClick={submitForm} data-tooltip={ !submitForm @@ -88,7 +87,7 @@ export function UpdatePage({ product, onUpdate, onBack }: Props): VNode { disabled={!submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx @@ -21,6 +21,7 @@ import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -47,7 +48,7 @@ export default function UpdateProduct({ onBack, }: Props): VNode { const result = useProductDetails(pid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, lib } = useSessionContext(); const { i18n } = useTranslationContext(); @@ -73,7 +74,7 @@ export default function UpdateProduct({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <UpdatePage product={{ ...result.body, product_id: pid }} onBack={onBack} 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 @@ -30,7 +30,6 @@ import { import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, @@ -284,7 +283,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -294,7 +293,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx @@ -20,7 +20,7 @@ */ import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; @@ -35,12 +35,12 @@ interface Props { export default function CreateTemplate({ onConfirm, onBack }: Props): VNode { const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); return ( <> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} onCreate={async (request: TalerMerchantApi.TemplateAddDetails) => { 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 @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -54,7 +54,7 @@ export default function ListTemplates({ onNewOrder, }: Props): VNode { const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, lib } = useSessionContext(); const result = useInstanceTemplates(); const [deleting, setDeleting] = @@ -80,7 +80,7 @@ export default function ListTemplates({ return ( <section class="section is-main-section"> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <JumpToElementById testIfExist={async (id) => { 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 @@ -20,34 +20,31 @@ */ import { - AmountLike, AmountString, Amounts, Duration, TalerError, TalerMerchantApi, - TranslatedString, + TranslatedString } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; +import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { TextField } from "../../../../components/form/TextField.js"; +import { NotificationCard } from "../../../../components/menu/index.js"; import { useSessionContext } from "../../../../context/session.js"; -import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; import { WithId } from "../../../../declaration.js"; -import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; +import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; type Entity = { description?: string; @@ -362,7 +359,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -372,7 +369,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -53,7 +53,7 @@ export default function UpdateTemplate({ }: Props): VNode { const { state, lib } = useSessionContext(); const result = useTemplateDetails(tid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); @@ -77,7 +77,7 @@ export default function UpdateTemplate({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <UpdatePage template={{ ...result.body, id: tid }} onBack={onBack} 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 @@ -23,13 +23,11 @@ import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; type Entity = TalerMerchantApi.TemplateContractDetails; @@ -133,7 +131,7 @@ export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -143,7 +141,7 @@ export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> 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 @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -52,7 +52,7 @@ export default function TemplateUsePage({ }: Props): VNode { const { lib } = useSessionContext(); const result = useTemplateDetails(tid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); if (!result) return <Loading />; @@ -75,7 +75,7 @@ export default function TemplateUsePage({ return ( <> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <UsePage template={result.body} id={tid} 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 @@ -22,7 +22,6 @@ import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { TokenFamilyForm } from "../../../../components/tokenfamily/TokenFamilyForm.js"; import { useListener } from "../../../../hooks/listener.js"; @@ -59,7 +58,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma onClick={submitForm} data-tooltip={ !submitForm @@ -69,7 +68,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { disabled={!submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/index.tsx @@ -19,7 +19,7 @@ * @author Christian Blättler */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; @@ -34,13 +34,13 @@ interface Props { onConfirm: () => void; } export default function CreateTokenFamily({ onConfirm, onBack }: Props): VNode { - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); const { state, lib } = useSessionContext(); return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} onCreate={(request) => { 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 @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -47,7 +47,7 @@ interface Props { } export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { const result = useInstanceTokenFamilies(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { lib, state } = useSessionContext(); const [deleting, setDeleting] = useState<TalerMerchantApi.TokenFamilySummary | null>(null); @@ -74,7 +74,7 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { return ( <section class="section is-main-section"> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CardTable instances={result.body.token_families} 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 @@ -19,11 +19,10 @@ * @author Christian Blättler */ -import { Duration, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputDate } from "../../../../components/form/InputDate.js"; @@ -124,7 +123,7 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -134,7 +133,7 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/index.tsx @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -51,7 +51,7 @@ export default function UpdateTokenFamily({ onBack, }: Props): VNode { const result = useTokenFamilyDetails(slug); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { lib, state } = useSessionContext(); const { i18n } = useTranslationContext(); @@ -85,7 +85,7 @@ export default function UpdateTokenFamily({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <UpdatePage tokenFamily={family} onBack={onBack} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx @@ -23,7 +23,6 @@ import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, @@ -122,7 +121,7 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton + <ButtonBetterBulma disabled={hasErrors} data-tooltip={ hasErrors @@ -132,7 +131,7 @@ export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { onClick={submitForm} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx @@ -21,6 +21,7 @@ import { TalerError, TalerMerchantApi } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; @@ -39,7 +40,7 @@ interface Props { export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); const instance = useInstanceBankAccounts(); const accounts = @@ -49,7 +50,7 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { return ( <> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} accounts={accounts} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -36,7 +36,7 @@ import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { useSessionContext } from "../../../../context/session.js"; import { NotificationCard } from "../../../../components/menu/index.js"; import { Notification } from "../../../../utils/types.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; interface Props { onCreate: () => void; @@ -51,7 +51,7 @@ export default function ListTransfer({ onCreate }: Props): VNode { const { i18n } = useTranslationContext(); const { state, lib } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const [position, setPosition] = useState<string | undefined>(undefined); @@ -102,7 +102,7 @@ export default function ListTransfer({ onCreate }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <ListPage accounts={accounts} 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,23 +21,21 @@ import { assertUnreachable, - ChallengeResponse, - HttpStatusCode, + HttpStatusCode } from "@gnu-taler/taler-util"; import { useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { FormProvider } from "../../../components/form/FormProvider.js"; import { Input } from "../../../components/form/Input.js"; import { InputToggle } from "../../../components/form/InputToggle.js"; import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; import { useSessionContext } from "../../../context/session.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; -import { Notification } from "../../../utils/types.js"; interface Props { instanceId: string; @@ -57,7 +55,7 @@ export function DeletePage({ instanceId, onBack, onDeleted }: Props): VNode { }); const { i18n } = useTranslationContext(); const { state: session, lib, logOut } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const errors = undefinedIfEmpty({ name: !form.name @@ -71,7 +69,9 @@ 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 [doDelete, repeatDelete] = mfa.withMfaHandler( ({ challengeIds, onChallengeRequired }) => async function doDeleteImpl() { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -19,11 +19,22 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Duration, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + AccessToken, + Duration, + HttpStatusCode, + TalerMerchantApi, + TalerMerchantInstanceHttpClient, +} from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + useChallengeHandler, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, @@ -32,37 +43,38 @@ import { import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; import { useSessionContext } from "../../../context/session.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; - -export type Entity = Omit<Omit<TalerMerchantApi.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & { - default_pay_delay: Duration, - default_wire_transfer_delay: Duration, +import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; + +export type Entity = Omit< + Omit<TalerMerchantApi.InstanceReconfigurationMessage, "default_pay_delay">, + "default_wire_transfer_delay" +> & { + default_pay_delay: Duration; + default_wire_transfer_delay: Duration; } & TalerForm; export interface Props { - onUpdate: (d: TalerMerchantApi.InstanceReconfigurationMessage) => void; + doUpdate: typeof TalerMerchantInstanceHttpClient.prototype.updateCurrentInstance; + onConfirm: () => void; selected: TalerMerchantApi.QueryInstancesResponse; isLoading: boolean; onBack: () => void; } -function convert( - from: TalerMerchantApi.QueryInstancesResponse, -): Entity { +function convert(from: TalerMerchantApi.QueryInstancesResponse): Entity { const { default_pay_delay, default_wire_transfer_delay, ...rest } = from; const defaults = { use_stefan: false, default_pay_delay: Duration.fromTalerProtocolDuration(default_pay_delay), - default_wire_transfer_delay: Duration.fromTalerProtocolDuration(default_wire_transfer_delay), + default_wire_transfer_delay: Duration.fromTalerProtocolDuration( + default_wire_transfer_delay, + ), }; return { ...defaults, ...rest } as Entity; } -export function UpdatePage({ - onUpdate, - selected, - onBack, -}: Props): VNode { +export function UpdatePage({ doUpdate, onConfirm, selected, onBack }: Props): VNode { const { state } = useSessionContext(); const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); @@ -74,10 +86,11 @@ export function UpdatePage({ default_pay_delay: !value.default_pay_delay ? i18n.str`Required` : !!value.default_wire_transfer_delay && - value.default_wire_transfer_delay.d_ms !== "forever" && - value.default_pay_delay.d_ms !== "forever" && - value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ? - i18n.str`Pay delay can't be greater than wire transfer delay` : undefined, + value.default_wire_transfer_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms + ? i18n.str`Pay delay can't be greater than wire transfer delay` + : undefined, default_wire_transfer_delay: !value.default_wire_transfer_delay ? i18n.str`Required` : undefined, @@ -97,19 +110,56 @@ export function UpdatePage({ const hasErrors = errors !== undefined; - const submit = async (): Promise<void> => { - const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>; - const result: TalerMerchantApi.InstanceReconfigurationMessage = { - default_pay_delay: Duration.toTalerProtocolDuration(default_pay_delay), - default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay), - ...rest, + const { default_pay_delay, default_wire_transfer_delay, ...rest } = + value as Required<Entity>; + const result: TalerMerchantApi.InstanceReconfigurationMessage = { + default_pay_delay: Duration.toTalerProtocolDuration(default_pay_delay), + default_wire_transfer_delay: Duration.toTalerProtocolDuration( + default_wire_transfer_delay, + ), + ...rest, + }; + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const mfa = useChallengeHandler(); + const update = safeFunctionHandler( + ( + token: AccessToken, + d: TalerMerchantApi.InstanceReconfigurationMessage, + challengeIds: string[], + ) => doUpdate(token, d, { challengeIds }), + hasErrors || !state.token? undefined : [state.token, result, []] + ); + update.onSuccess = onConfirm; + update.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Accepted: + mfa.onChallengeRequired(fail.body); + return i18n.str`Second factor authentication required.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized.`; + case HttpStatusCode.NotFound: + return i18n.str`Instace not found.`; } - onUpdate(result); }; - // const [active, setActive] = useState(false); + const retry = update.lambda((challengesIds: string[]) => [ + update.args![0], + update.args![1], + challengesIds, + ]); + + if (mfa.pendingChallenge) { + return ( + <SolveMFAChallenges + currentChallenge={mfa.pendingChallenge} + onCompleted={retry} + onCancel={mfa.doCancelChallenge} + /> + ); + } return ( <div> + <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -117,7 +167,8 @@ export function UpdatePage({ <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - <i18n.Translate>Instance id</i18n.Translate>: <b>{state.instance}</b> + <i18n.Translate>Instance id</i18n.Translate>:{" "} + <b>{state.instance}</b> </span> </div> </div> @@ -147,17 +198,16 @@ export function UpdatePage({ <i18n.Translate>Cancel</i18n.Translate> </button> - <AsyncButton - onClick={submit} + <ButtonBetterBulma + onClick={update} data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } - disabled={hasErrors} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - ChallengeResponse, + AccessToken, HttpStatusCode, TalerError, TalerMerchantApi, @@ -23,14 +23,16 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../components/exception/loading.js"; -import { NotificationCard } from "../../../components/menu/index.js"; +import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; import { useSessionContext } from "../../../context/session.js"; import { useInstanceDetails, @@ -39,9 +41,8 @@ import { import { Notification } from "../../../utils/types.js"; import { LoginPage } from "../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; -import { UpdatePage } from "./UpdatePage.js"; -import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; import { DeletePage } from "./DeletePage.js"; +import { Entity, UpdatePage } from "./UpdatePage.js"; export interface Props { onBack: () => void; @@ -71,9 +72,10 @@ function CommonUpdate( | undefined, updateInstance: typeof TalerMerchantInstanceHttpClient.prototype.updateCurrentInstance, ): VNode { - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { i18n } = useTranslationContext(); const { state } = useSessionContext(); + const [deleting, setDeleting] = useState<boolean>(); if (!result) return <Loading />; if (result instanceof TalerError) { @@ -93,64 +95,16 @@ function CommonUpdate( } } - const mfa = useChallengeHandler(); - const [doUpdate, repeatUpdate] = mfa.withMfaHandler( - ({ challengeIds, onChallengeRequired }) => - async function doUpdateImpl( - d: TalerMerchantApi.InstanceReconfigurationMessage, - ) { - if (state.status !== "loggedIn") { - return; - } - try { - const resp = await updateInstance(state.token, d, { challengeIds }); - if (resp.type === "ok") { - return onConfirm(); - } - switch (resp.case) { - case HttpStatusCode.Accepted: { - onChallengeRequired(resp.body); - return; - } - case HttpStatusCode.Unauthorized: - case HttpStatusCode.NotFound: { - setNotif({ - message: i18n.str`Failed to update instance`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - } - } catch (error) { - return setNotif({ - message: i18n.str`Failed to update instance`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - } - }, - ); - const [deleting, setDeleting] = useState<boolean>(); - - if (mfa.pendingChallenge) { - return ( - <SolveMFAChallenges - currentChallenge={mfa.pendingChallenge} - onCompleted={repeatUpdate} - onCancel={mfa.doCancelChallenge} - /> - ); - } return ( <Fragment> - <NotificationCard notification={notif} /> <UpdatePage onBack={onBack} isLoading={false} selected={result.body} - onUpdate={doUpdate} + doUpdate={updateInstance} + onConfirm={onConfirm} /> <div class="columns"> <div class="column" /> 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 @@ -19,34 +19,51 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { assertUnreachable, HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { assertUnreachable, TalerMerchantApi } from "@gnu-taler/taler-util"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { useSessionContext } from "../../../../context/session.js"; type Entity = TalerMerchantApi.WebhookAddDetails; interface Props { - onCreate: (d: Entity) => Promise<void>; + onCreate: () => void; onBack?: () => void; } -const validType = ["pay", "refund", "order_settled", "category_added", "category_updated", "category_deleted", "inventory_added", "inventory_updated", "inventory_deleted"] as const; +const validType = [ + "pay", + "refund", + "order_settled", + "category_added", + "category_updated", + "category_deleted", + "inventory_added", + "inventory_updated", + "inventory_deleted", +] as const; const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"]; export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>({}); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { state: session, lib } = useSessionContext(); const errors = undefinedIfEmpty<FormErrors<Entity>>({ webhook_id: !state.webhook_id ? i18n.str`Required` : undefined, event_type: !state.event_type @@ -69,13 +86,23 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const hasErrors = errors !== undefined; - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onCreate(state as TalerMerchantApi.WebhookAddDetails); - }; + const data = state as TalerMerchantApi.WebhookAddDetails; + + const create = safeFunctionHandler( + lib.instance.addWebhook, + !session.token || hasErrors ? undefined : [session.token, data], + ); + create.onSuccess = onCreate + create.onFail = (fail) =>{ + switch(fail.case) { + case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized.` + case HttpStatusCode.NotFound: return i18n.str`Not found.` + } + } return ( <div> + <LocalNotificationBannerBulma notification={notification} /> <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -104,7 +131,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { case "refund": return i18n.str`Refund`; case "order_settled": - return i18n.str`Order settled` + return i18n.str`Order settled`; case "category_added": return i18n.str`Category added`; case "category_updated": @@ -243,17 +270,16 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton - disabled={hasErrors} + <ButtonBetterBulma data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } - onClick={submitForm} + onClick={create} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> <div class="column" /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx @@ -19,13 +19,14 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { AccessToken, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; import { useSessionContext } from "../../../../context/session.js"; -import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; export type Entity = TalerMerchantApi.WebhookAddDetails; @@ -35,41 +36,9 @@ interface Props { } export default function CreateWebhook({ onConfirm, onBack }: Props): VNode { - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - const { state, lib } = useSessionContext(); - return ( <> - <NotificationCard notification={notif} /> - <CreatePage - onBack={onBack} - onCreate={async (request: TalerMerchantApi.WebhookAddDetails) => { - return lib.instance.addWebhook(state.token, request) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Webhook create successfully`, - type: "SUCCESS", - }); - onConfirm() - } else { - setNotif({ - message: i18n.str`Could not create the webhook`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not create webhook`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - }); - }} - /> + <CreatePage onBack={onBack} onCreate={onConfirm} /> </> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx @@ -29,21 +29,16 @@ export interface Props { onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; onCreate: () => void; - onDelete: (e: TalerMerchantApi.WebhookEntry) => void; onSelect: (e: TalerMerchantApi.WebhookEntry) => void; } export function ListPage({ webhooks, onCreate, - onDelete, onSelect, onLoadMoreBefore, onLoadMoreAfter, }: Props): VNode { - const form = { payto_uri: "" }; - - const { i18n } = useTranslationContext(); return ( <section class="section is-main-section"> <CardTable @@ -52,7 +47,6 @@ export function ListPage({ id: String(o.webhook_id), }))} onCreate={onCreate} - onDelete={onDelete} onSelect={onSelect} onLoadMoreBefore={onLoadMoreBefore} onLoadMoreAfter={onLoadMoreAfter} 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 @@ -19,16 +19,26 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { + AccessToken, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + SafeHandlerTemplate, + useLocalNotificationBetter, + 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"; type Entity = TalerMerchantApi.WebhookEntry; interface Props { webhooks: Entity[]; - onDelete: (e: Entity) => void; onSelect: (e: Entity) => void; onCreate: () => void; onLoadMoreBefore?: () => void; @@ -38,7 +48,6 @@ interface Props { export function CardTable({ webhooks, onCreate, - onDelete, onSelect, onLoadMoreAfter, onLoadMoreBefore, @@ -47,54 +56,74 @@ export function CardTable({ const { i18n } = useTranslationContext(); + const { state, lib } = useSessionContext(); + + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const deleteWebhook = safeFunctionHandler(lib.instance.deleteWebhook); + deleteWebhook.onSuccess = () => i18n.str`Webhook deleted successfully`; + deleteWebhook.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized.`; + case HttpStatusCode.NotFound: + return i18n.str`Not found.`; + } + }; + const deleteById = deleteWebhook.lambda((id: string) => + !deleteWebhook.args ? undefined! : [deleteWebhook.args[0], id], + ); return ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-webhook" /> - </span> - <i18n.Translate>Webhooks</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`Add new webhooks`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {webhooks.length > 0 ? ( - <Table - instances={webhooks} - onDelete={onDelete} - onSelect={onSelect} - rowSelection={rowSelection} - rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} - /> - ) : ( - <EmptyTable /> - )} + <Fragment> + <LocalNotificationBannerBulma notification={notification} /> + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-webhook" /> + </span> + <i18n.Translate>Webhooks</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`Add new webhooks`} + > + <button class="button is-info" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {webhooks.length > 0 ? ( + <Table + instances={webhooks} + deleteWebhook={deleteById} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> </div> </div> </div> - </div> + </Fragment> ); } interface TableProps { rowSelection: string[]; instances: Entity[]; - onDelete: (e: Entity) => void; + deleteWebhook: SafeHandlerTemplate<[id: string], any>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; @@ -104,7 +133,7 @@ interface TableProps { function Table({ instances, onLoadMoreAfter, - onDelete, + deleteWebhook, onSelect, onLoadMoreBefore, }: TableProps): VNode { @@ -150,13 +179,13 @@ function Table({ </td> <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> - <button + <ButtonBetterBulma class="button is-danger is-small has-tooltip-left" data-tooltip={i18n.str`Delete selected webhook from the database`} - onClick={() => onDelete(i)} + onClick={deleteWebhook.withArgs(i.webhook_id)} > <i18n.Translate>Delete</i18n.Translate> - </button> + </ButtonBetterBulma> </div> </td> </tr> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx @@ -25,7 +25,7 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; @@ -44,9 +44,6 @@ interface Props { } export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { - const { i18n } = useTranslationContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { state, lib } = useSessionContext(); const result = useInstanceWebhooks(); if (!result) return <Loading />; @@ -69,7 +66,6 @@ export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> <ListPage webhooks={result.body.webhooks} @@ -79,31 +75,6 @@ export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { onSelect={(e) => { onSelect(e.webhook_id); }} - onDelete={(e: TalerMerchantApi.WebhookEntry) => { - return lib.instance - .deleteWebhook(state.token, e.webhook_id) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Webhook deleted successfully`, - type: "SUCCESS", - }); - } else { - setNotif({ - message: i18n.str`Could not delete the webhook`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - }) - .catch((error) => - setNotif({ - message: i18n.str`Could not delete the webhook`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }), - ); - }} /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx @@ -19,24 +19,33 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { assertUnreachable, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + assertUnreachable, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; import { WithId } from "../../../../declaration.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { useSessionContext } from "../../../../context/session.js"; type Entity = TalerMerchantApi.WebhookPatchDetails & WithId; interface Props { - onUpdate: (d: Entity) => Promise<void>; + onConfirm: () => void; onBack?: () => void; webhook: Entity; } @@ -53,9 +62,12 @@ const validType = [ ] as const; const validMethod = ["GET", "POST", "PUT", "PATCH", "HEAD"]; -export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode { +export function UpdatePage({ webhook, onConfirm, onBack }: Props): VNode { const { i18n } = useTranslationContext(); + const { state: session, lib, logIn } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const [state, setState] = useState<Partial<Entity>>(webhook); const errors = undefinedIfEmpty<FormErrors<Entity>>({ @@ -79,13 +91,29 @@ export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode { const hasErrors = errors !== undefined; - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onUpdate(state as Entity); + const data = state as Entity; + const update = safeFunctionHandler( + lib.instance.updateWebhook, + !session.token || !!errors ? undefined : [session.token, webhook.id, data], + ); + update.onSuccess = (success) => { + onConfirm(); + return i18n.str`Webhook updated`; + }; + update.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized.`; + case HttpStatusCode.NotFound: + return i18n.str`Not found.`; + case HttpStatusCode.Conflict: + return i18n.str`Conflict.`; + } }; return ( <div> + <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -186,17 +214,16 @@ export function UpdatePage({ webhook, onUpdate, onBack }: Props): VNode { <i18n.Translate>Cancel</i18n.Translate> </button> )} - <AsyncButton - disabled={hasErrors} + <ButtonBetterBulma data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } - onClick={submitForm} + onClick={update} > <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </div> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx @@ -27,17 +27,14 @@ import { } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; import { useWebhookDetails } from "../../../../hooks/webhooks.js"; -import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; export type Entity = TalerMerchantApi.WebhookPatchDetails & WithId; @@ -51,11 +48,7 @@ export default function UpdateWebhook({ onConfirm, onBack, }: Props): VNode { - const { state, lib } = useSessionContext(); const result = useWebhookDetails(tid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - const { i18n } = useTranslationContext(); if (!result) return <Loading />; if (result instanceof TalerError) { @@ -77,37 +70,10 @@ export default function UpdateWebhook({ return ( <Fragment> - <NotificationCard notification={notif} /> <UpdatePage webhook={{ ...result.body, id: tid }} onBack={onBack} - onUpdate={async (data) => { - return lib.instance - .updateWebhook(state.token, tid, data) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Webhook updated`, - type: "SUCCESS", - }); - onConfirm(); - } else { - setNotif({ - message: i18n.str`Could not update webhook`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not update webhook`, - type: "ERROR", - description: - error instanceof Error ? error.message : String(error), - }); - }); - }} + onConfirm={onConfirm} /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -20,25 +20,23 @@ */ import { - assertUnreachable, Duration, HttpStatusCode, LoginTokenRequest, LoginTokenScope, - TalerError, - TranslatedString, + TranslatedString } from "@gnu-taler/taler-util"; import { + ButtonBetterBulma, + LocalNotificationBannerBulma, useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../components/exception/AsyncButton.js"; -import { NotificationCard } from "../../components/menu/index.js"; import { SolveMFAChallenges } from "../../components/SolveMFA.js"; import { useSessionContext } from "../../context/session.js"; -import { Notification } from "../../utils/types.js"; interface Props { showCreateAccount?: boolean; @@ -61,76 +59,54 @@ export const FOREVER_REFRESHABLE_TOKEN = (description: TranslatedString) => const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; export function LoginPage({ showCreateAccount }: Props): VNode { const [password, setPassword] = useState(""); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, logIn, getInstanceForUsername, config } = useSessionContext(); - const [username, setUsername] = useState(showCreateAccount? "" : state.instance); + const [username, setUsername] = useState( + showCreateAccount ? "" : state.instance, + ); const { i18n } = useTranslationContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); - const [doLogin, repeatLogin] = mfa.withMfaHandler( - ({ challengeIds, onChallengeRequired }) => - async () => { - const api = getInstanceForUsername(username); - try { - const result = await api.createAccessToken( - username, - password, - FOREVER_REFRESHABLE_TOKEN(i18n.str`Logged in`), - { - challengeIds, - }, - ); - if (result.type === "ok") { - const { access_token: token } = result.body; - logIn(username, token); - return; - } else { - switch (result.case) { - case HttpStatusCode.Unauthorized: { - setNotif({ - message: i18n.str`Your password is incorrect`, - type: "ERROR", - }); - return; - } - case HttpStatusCode.NotFound: { - setNotif({ - message: i18n.str`Your instance cannot be found`, - type: "ERROR", - }); - return; - } - case HttpStatusCode.Accepted: { - onChallengeRequired(result.body); - return; - } - default: { - assertUnreachable(result); - } - } - } - } catch (error) { - const details = - error instanceof TalerError - ? JSON.stringify(error.errorDetail) - : undefined; - setNotif({ - message: i18n.str`Failed to login.`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - details, - }); - } - }, + const login = safeFunctionHandler( + (usr: string, pwd: string, challengeIds: string[]) => { + const api = getInstanceForUsername(username); + return api.createAccessToken( + usr, + pwd, + FOREVER_REFRESHABLE_TOKEN(i18n.str`Logged in`), + { challengeIds }, + ); + }, + !username || !password ? undefined : [username, password, []], ); + login.onSuccess = (success, usr) => { + logIn(usr, success.access_token); + }; + login.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Accepted: + mfa.onChallengeRequired(fail.body); + return i18n.str`Second factor authorization required.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not foudn`; + } + }; + const retry = login.lambda((ids: string[]) => [ + login.args![0], + login.args![1], + ids, + ]); if (mfa.pendingChallenge) { return ( <SolveMFAChallenges currentChallenge={mfa.pendingChallenge} - onCompleted={repeatLogin} + onCompleted={retry} onCancel={mfa.doCancelChallenge} /> ); @@ -138,7 +114,7 @@ export function LoginPage({ showCreateAccount }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> <div class="modal-card" style={{ width: "100%", margin: 0 }}> @@ -173,7 +149,7 @@ export function LoginPage({ showCreateAccount }: Props): VNode { placeholder={"instance name"} name="username" onKeyPress={(e) => - e.keyCode === 13 ? doLogin() : null + e.keyCode === 13 ? login.call() : null } value={username} onInput={(e): void => @@ -199,7 +175,7 @@ export function LoginPage({ showCreateAccount }: Props): VNode { placeholder={"current password"} name="token" onKeyPress={(e) => - e.keyCode === 13 ? doLogin() : null + e.keyCode === 13 ? login.call() : null } value={password} onInput={(e): void => @@ -234,9 +210,9 @@ export function LoginPage({ showCreateAccount }: Props): VNode { <i18n.Translate>Forgot password</i18n.Translate> </a> )} - <AsyncButton disabled={!username || !password} onClick={doLogin}> + <ButtonBetterBulma onClick={login}> <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </footer> </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 @@ -17,24 +17,26 @@ import { Duration, HttpStatusCode, + InstanceConfigurationMessage, MerchantAuthMethod, TanChannel, } from "@gnu-taler/taler-util"; import { + ButtonBetterBulma, + LocalNotificationBannerBulma, undefinedIfEmpty, useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, } from "../../components/form/FormProvider.js"; import { Input } from "../../components/form/Input.js"; import { InputWithAddon } from "../../components/form/InputWithAddon.js"; -import { NotificationCard } from "../../components/menu/index.js"; import { SolveMFAChallenges } from "../../components/SolveMFA.js"; import { useSessionContext } from "../../context/session.js"; import { @@ -63,7 +65,6 @@ interface Props { export function NewAccount({ onCancel, onCreated }: Props): VNode { const { i18n } = useTranslationContext(); const { state: session, lib, logIn, config } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); const [value, setValue] = useState<Partial<Account>>({}); @@ -116,66 +117,57 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { setValue(v); } + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); - const [doCreate, repeatCreate] = mfa.withMfaHandler( - ({ challengeIds, onChallengeRequired }) => - async function doCreateImpl() { - try { - const id = value.id!; - const resp = await lib.instance.createInstanceSelfProvision( - { - address: {}, - auth: { - method: MerchantAuthMethod.TOKEN, - password: value.password!, - }, - default_pay_delay: Duration.toTalerProtocolDuration(twoHours), - default_wire_transfer_delay: - Duration.toTalerProtocolDuration(twoDays), - id, - jurisdiction: {}, - name: value.name!, - use_stefan: true, - email: value.email, - phone_number: value.phone, - }, - { - tokenValidity: Duration.fromSpec({ months: 6 }), - challengeIds, - }, - ); - if (resp.type === "fail") { - if (resp.case === HttpStatusCode.Accepted) { - onChallengeRequired(resp.body); - } else { - setNotif({ - message: i18n.str`Failed to create account`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - return; - } - if (resp.body) { - logIn(id, resp.body.access_token); - } - onCreated(); - } catch (error) { - setNotif({ - message: i18n.str`Failed to create account`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - } - }, + const request: InstanceConfigurationMessage = { + address: {}, + auth: { + method: MerchantAuthMethod.TOKEN, + password: value.password!, + }, + default_pay_delay: Duration.toTalerProtocolDuration(twoHours), + default_wire_transfer_delay: Duration.toTalerProtocolDuration(twoDays), + id: value.id!, + jurisdiction: {}, + name: value.name!, + use_stefan: true, + email: value.email, + phone_number: value.phone, + }; + + const create = safeFunctionHandler( + (req: InstanceConfigurationMessage, challengeIds: string[]) => + lib.instance.createInstanceSelfProvision(req, { + challengeIds, + tokenValidity: Duration.fromSpec({ months: 6 }), + }), + !!errors ? undefined : [request, []], ); + create.onSuccess = (success, req) => { + if (success) { + logIn(req.id, success.access_token); + } + onCreated(); + }; + create.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Accepted: + mfa.onChallengeRequired(fail.body); + return i18n.str`Second factor authentication required.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.Conflict: + return i18n.str`Conflict`; + } + }; + const retry = create.lambda((ids: string[]) => [create.args![0], ids]); if (mfa.pendingChallenge) { return ( <SolveMFAChallenges currentChallenge={mfa.pendingChallenge} - onCompleted={repeatCreate} + onCompleted={retry} onCancel={mfa.doCancelChallenge} /> ); @@ -183,7 +175,7 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> @@ -259,13 +251,9 @@ export function NewAccount({ onCancel, onCreated }: Props): VNode { <button class="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> - <AsyncButton - type="is-info" - disabled={!!errors} - onClick={doCreate} - > + <ButtonBetterBulma type="is-info" onClick={create}> <i18n.Translate>Create</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </footer> </div> </div> diff --git a/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/resetAccount/index.tsx @@ -15,23 +15,25 @@ */ import { - ChallengeResponse, + assertUnreachable, HttpStatusCode, MerchantAuthMethod, + opFixedSuccess, } from "@gnu-taler/taler-util"; import { + ButtonBetterBulma, + LocalNotificationBannerBulma, useChallengeHandler, + useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { AsyncButton } from "../../components/exception/AsyncButton.js"; import { FormErrors, FormProvider, } from "../../components/form/FormProvider.js"; import { Input } from "../../components/form/Input.js"; -import { NotificationCard } from "../../components/menu/index.js"; import { SolveMFAChallenges } from "../../components/SolveMFA.js"; import { useSessionContext } from "../../context/session.js"; import { Notification } from "../../utils/types.js"; @@ -54,7 +56,7 @@ export function ResetAccount({ }: Props): VNode { const { i18n } = useTranslationContext(); const { state: session, lib, logIn } = useSessionContext(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); + const [value, setValue] = useState<Partial<Form>>({ // password: "asd", // repeat: "asd", @@ -77,61 +79,56 @@ export function ResetAccount({ }; setValue(v); } + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const mfa = useChallengeHandler(); - const [doReset, repeatReset] = mfa.withMfaHandler( - ({ challengeIds, onChallengeRequired }) => - async function doResetImpl() { - try { - const resp = await lib - .subInstanceApi(instanceId) - .instance.forgotPasswordSelfProvision( - { - method: MerchantAuthMethod.TOKEN, - password: value.password!, - }, - { - challengeIds, - }, - ); - if (resp.type === "fail") { - if (resp.case === HttpStatusCode.Accepted) { - onChallengeRequired(resp.body); - } else { - setNotif({ - message: i18n.str`Failed to create account`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - return; - } - //if auth has been updated, request a new access token - const result = await lib.instance.createAccessToken( - instanceId, - value.password!, - FOREVER_REFRESHABLE_TOKEN(i18n.str`Password reset`), - ); - if (result.type === "ok") { - const { access_token: token } = result.body; - logIn(instanceId, token); - } - onReseted(); - } catch (error) { - setNotif({ - message: i18n.str`Failed to create account`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - } - }, + const reset = safeFunctionHandler( + async (password: string, challengeIds: string[]) => { + const forgot = await lib + .subInstanceApi(instanceId) + .instance.forgotPasswordSelfProvision( + { method: MerchantAuthMethod.TOKEN, password }, + { challengeIds }, + ); + + if (forgot.type === "fail") { + return forgot; + } + const login = await lib.instance.createAccessToken( + instanceId, + value.password!, + FOREVER_REFRESHABLE_TOKEN(i18n.str`Password reset`), + ); + if (login.type === "fail") return login; + return opFixedSuccess(login.body); + }, ); + reset.onSuccess = (suc) => { + logIn(instanceId, suc.access_token); + onReseted(); + }; + reset.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Accepted: + mfa.onChallengeRequired(fail.body); + return i18n.str`Second factor authentication required.`; + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized.`; + case HttpStatusCode.Forbidden: + return i18n.str`Forbidden.`; + case HttpStatusCode.NotFound: + return i18n.str`Not found.`; + default: + assertUnreachable(fail); + } + }; + const retry = reset.lambda((ids: string[]) => [reset.args![0], ids]); if (mfa.pendingChallenge) { return ( <SolveMFAChallenges currentChallenge={mfa.pendingChallenge} - onCompleted={repeatReset} + onCompleted={retry} onCancel={mfa.doCancelChallenge} /> ); @@ -139,7 +136,7 @@ export function ResetAccount({ return ( <Fragment> - <NotificationCard notification={notif} /> + <LocalNotificationBannerBulma notification={notification} /> <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> @@ -187,9 +184,9 @@ export function ResetAccount({ <button class="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> - <AsyncButton disabled={!errors} onClick={doReset}> + <ButtonBetterBulma onClick={reset}> <i18n.Translate>Reset</i18n.Translate> - </AsyncButton> + </ButtonBetterBulma> </footer> </div> </div>