commit d7b75f95d05e6f0c7d64003989429d36c829e7f1 parent 7c7f32c5ce67f9774cd948389e087b9d853d577a Author: Sebastian <sebasjm@gmail.com> Date: Fri, 31 Oct 2025 08:44:04 -0300 template tkfam and otp Diffstat:
19 files changed, 361 insertions(+), 679 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx b/packages/merchant-backoffice-ui/src/components/tokenfamily/TokenFamilyForm.tsx @@ -1,147 +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 Christian Blättler - */ - -import { - AbsoluteTime, - Duration, - TalerMerchantApi -} from "@gnu-taler/taler-util"; -import { InputSelectOne, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h } from "preact"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import { useSessionContext } from "../../context/session.js"; -import { FormErrors, FormProvider } from "../form/FormProvider.js"; -import { Input } from "../form/Input.js"; -import { InputDate } from "../form/InputDate.js"; -import { InputDuration } from "../form/InputDuration.js"; -import { InputSelector } from "../form/InputSelector.js"; -import { InputWithAddon } from "../form/InputWithAddon.js"; - -type Entity = TalerMerchantApi.TokenFamilyCreateRequest; - -interface Props { - onSubscribe: (c?: () => Entity | undefined) => void; - initial?: Partial<Entity>; - alreadyExist?: boolean; -} - -export function TokenFamilyForm({ onSubscribe }: Props) { - const [value, valueHandler] = useState<Partial<Entity>>({ - slug: undefined, - name: undefined, - description: undefined, - description_i18n: {}, - kind: TalerMerchantApi.TokenFamilyKind.Discount, - duration: Duration.toTalerProtocolDuration(Duration.getForever()), - valid_after: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), - valid_before: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), - validity_granularity: Duration.toTalerProtocolDuration(Duration.getForever()), - }); - - const { i18n } = useTranslationContext(); - - const errors: FormErrors<Entity> = { - slug: !value.slug ? i18n.str`Required` : undefined, - name: !value.name ? i18n.str`Required` : undefined, - description: !value.description ? i18n.str`Required` : undefined, - valid_after: !value.valid_after ? undefined : undefined, - valid_before: !value.valid_before ? i18n.str`Required` : undefined, - validity_granularity: !value.validity_granularity ? i18n.str`Required` : undefined, - duration: !value.duration ? i18n.str`Required` : undefined, - kind: !value.kind ? i18n.str`Required` : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const submit = useCallback((): Entity | undefined => { - // HACK: Think about how this can be done better - return value as Entity; - }, [value]); - - useEffect(() => { - onSubscribe(hasErrors ? undefined : submit); - }, [submit, hasErrors]); - - const { state } = useSessionContext(); - - return ( - <div> - <FormProvider<Entity> - name="token_family" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <InputWithAddon<Entity> - name="slug" - addonBefore={new URL("tokenfamily/", state.backendUrl.href).href} - label={i18n.str`Slug`} - tooltip={i18n.str`Token family slug to use in URLs (for internal use only)`} - /> - <InputSelector<Entity> - name="kind" - label={i18n.str`Kind`} - tooltip={i18n.str`Token family kind`} - values={["discount", "subscription"]} - /> - <Input<Entity> - name="name" - inputType="text" - label={i18n.str`Name`} - tooltip={i18n.str`User-readable token family name`} - /> - <Input<Entity> - name="description" - inputType="multiline" - label={i18n.str`Description`} - tooltip={i18n.str`Token family description for customers`} - /> - <InputDate<Entity> - name="valid_after" - label={i18n.str`Valid After`} - tooltip={i18n.str`Token family can issue tokens after this date`} - withTimestampSupport - /> - <InputDate<Entity> - name="valid_before" - label={i18n.str`Valid Before`} - tooltip={i18n.str`Token family can issue tokens until this date`} - withTimestampSupport - /> - <InputDuration<Entity> - name="validity_granularity" - label={i18n.str`Validity Granularity`} - tooltip={i18n.str`Rounding granularity for the start validity of keys, must be 1 minute, 1 hour, 1 day, 1 week, 30 days or 90 days`} - useProtocolDuration - /> - <InputDuration<Entity> - name="duration" - label={i18n.str`Duration`} - tooltip={i18n.str`Validity duration of a issued token`} - withForever - useProtocolDuration - /> - </FormProvider> - </div> - ); -} 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 @@ -20,11 +20,12 @@ */ import { + HttpStatusCode, TalerMerchantApi, isRfc3548Base32Charset, randomRfc3548Base32Key, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { @@ -35,21 +36,24 @@ import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { useSessionContext } from "../../../../context/session.js"; type Entity = TalerMerchantApi.OtpDeviceAddDetails; interface Props { - onCreate: (d: Entity) => Promise<void>; + onCreated: (d: TalerMerchantApi.OtpDeviceAddDetails) => void; onBack?: () => void; } const algorithms = [0, 1, 2]; const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; -export function CreatePage({ onCreate, onBack }: Props): VNode { +export function CreatePage({ onCreated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>({}); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { state: session, lib } = useSessionContext(); const [showKey, setShowKey] = useState(false); @@ -76,13 +80,21 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const hasErrors = errors !== undefined; - const submitForm = () => { - if (hasErrors) return Promise.reject(); - return onCreate(state as TalerMerchantApi.OtpDeviceAddDetails); - }; + const data = hasErrors ? undefined : state as TalerMerchantApi.OtpDeviceAddDetails + const create = safeFunctionHandler(lib.instance.addOtpDevice, !session.token || !data ? undefined : [session.token, data]) + create.onSuccess = (success, token, req) => { + onCreated(req) + } + 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" /> @@ -159,13 +171,12 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - disabled={hasErrors} data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } - onClick={submitForm} + onClick={create} > <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> 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 @@ -23,9 +23,7 @@ import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; import { useSessionContext } from "../../../../context/session.js"; -import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; @@ -36,9 +34,6 @@ interface Props { } export default function CreateValidator({ onConfirm, onBack }: Props): VNode { - const { state, lib } = useSessionContext(); - - const { i18n } = useTranslationContext(); const [created, setCreated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null); @@ -48,35 +43,9 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { return ( <> - <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} - onCreate={async (request: Entity) => { - return lib.instance - .addOtpDevice(state.token, request) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Device added successfully`, - type: "SUCCESS", - }); - setCreated(request); - } else { - setNotif({ - message: i18n.str`Could not add device`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not add device`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - }); - }} + onCreated={setCreated} /> </> ); 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 @@ -27,13 +27,10 @@ import { } from "@gnu-taler/taler-util"; import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; import { 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 { useInstanceOtpDevices } from "../../../../hooks/otp.js"; -import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CardTable } from "./Table.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx @@ -20,10 +20,11 @@ */ import { + HttpStatusCode, randomRfc3548Base32Key, TalerMerchantApi, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { FormProvider } from "../../../../components/form/FormProvider.js"; @@ -31,28 +32,42 @@ import { Input } from "../../../../components/form/Input.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { WithId } from "../../../../declaration.js"; +import { useSessionContext } from "../../../../context/session.js"; type Entity = TalerMerchantApi.OtpDevicePatchDetails & WithId; interface Props { - onUpdate: (d: Entity) => Promise<void>; + onUpdated: (d: Entity) => void; onBack?: () => void; device: Entity; } const algorithms = [0, 1, 2]; const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; -export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { +export function UpdatePage({ device, onUpdated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>(device); const [showKey, setShowKey] = useState(false); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const { state: session, lib } = useSessionContext(); - const submitForm = () => { - return onUpdate(state as Entity); - }; + + const update = safeFunctionHandler(lib.instance + .updateOtpDevice, !session.token ? undefined : [session.token, device.id, state as Entity]) + update.onSuccess = (suc, t, id, req) => { + onUpdated({ ...req, id: device.id }) + } + update.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized` + case HttpStatusCode.NotFound: return i18n.str`Template id is unknown` + case HttpStatusCode.Conflict: return i18n.str`The provided information is inconsistent with the current state of the template` + } + } return ( <div> + <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -155,9 +170,8 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - disabled={false} data-tooltip={i18n.str`Confirm operation`} - onClick={submitForm} + onClick={update} > <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> 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 @@ -33,15 +33,13 @@ 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 { useOtpDeviceDetails } from "../../../../hooks/otp.js"; -import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CreatedSuccessfully } from "../create/CreatedSuccessfully.js"; import { UpdatePage } from "./UpdatePage.js"; -import { WithId } from "../../../../declaration.js"; export type Entity = TalerMerchantApi.OtpDevicePatchDetails & WithId; @@ -59,9 +57,6 @@ export default function UpdateValidator({ const [keyUpdated, setKeyUpdated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null); - const { state, lib } = useSessionContext(); - - const { i18n } = useTranslationContext(); if (!result) return <Loading />; if (result instanceof TalerError) { @@ -87,7 +82,6 @@ export default function UpdateValidator({ return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <UpdatePage device={{ id: vid, @@ -97,50 +91,14 @@ export default function UpdateValidator({ otp_ctr: result.body.otp_ctr, }} onBack={onBack} - onUpdate={async (newInfo) => { - return lib.instance - .updateOtpDevice(state.token, vid, newInfo) - .then((d) => { - if (d.type === "ok") { - if (newInfo.otp_key) { - setKeyUpdated({ - otp_algorithm: newInfo.otp_algorithm, - otp_device_description: newInfo.otp_device_description, - otp_device_id: newInfo.id, - otp_key: newInfo.otp_key, - otp_ctr: newInfo.otp_ctr, - }); - } else { - onConfirm(); - } - } else { - switch(d.case) { - case HttpStatusCode.NotFound: { - setNotif({ - message: i18n.str`Could not update template`, - type: "ERROR", - description: i18n.str`Template id is unknown`, - }); - break; - } - case HttpStatusCode.Conflict: { - setNotif({ - message: i18n.str`Could not update template`, - type: "ERROR", - description: i18n.str`The provided information is inconsistent with the current state of the template`, - }); - break; - } - } - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not update template`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - }); + onUpdated={(d) => { + setKeyUpdated({ + otp_algorithm: d.otp_algorithm, + otp_device_description: d.otp_device_description, + otp_device_id: d.id, + otp_key: d.otp_key, + otp_ctr: d.otp_ctr + }) }} /> </Fragment> 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 @@ -23,11 +23,12 @@ import { AmountString, Amounts, Duration, + HttpStatusCode, TalerError, TalerMerchantApi, TranslatedString, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { @@ -60,14 +61,15 @@ type Entity = { }; interface Props { - onCreate: (d: TalerMerchantApi.TemplateAddDetails) => Promise<void>; + onCreated: () => void; onBack?: () => void; } -export function CreatePage({ onCreate, onBack }: Props): VNode { +export function CreatePage({ onCreated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { config, state: session } = useSessionContext(); + const { config, state: session, lib } = useSessionContext(); const devices = useInstanceOtpDevices(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const [state, setState] = useState<Partial<Entity>>({ minimum_age: 0, @@ -127,37 +129,44 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const zero = Amounts.stringify(Amounts.zeroOfCurrency(config.currency)); - const submitForm = () => { - if (hasErrors) return Promise.reject(); - const contract_amount = state.amount_editable - ? undefined - : (state.amount as AmountString); - const contract_summary = state.summary_editable ? undefined : state.summary; - const template_contract: TalerMerchantApi.TemplateContractDetails = { - minimum_age: state.minimum_age!, - pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), - amount: contract_amount, - summary: contract_summary, + const contract_amount = state.amount_editable + ? undefined + : (state.amount as AmountString); + const contract_summary = state.summary_editable ? undefined : state.summary; + const template_contract: TalerMerchantApi.TemplateContractDetails = { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: contract_amount, + summary: contract_summary, + currency: + cList.length > 1 && state.currency_editable + ? undefined + : config.currency, + }; + const data: TalerMerchantApi.TemplateAddDetails = { + template_id: state.id!, + template_description: state.description!, + template_contract, + editable_defaults: { + amount: !state.amount_editable ? undefined : state.amount ?? zero, + summary: !state.summary_editable ? undefined : state.summary ?? "", currency: - cList.length > 1 && state.currency_editable + cList.length === 1 || !state.currency_editable ? undefined : config.currency, - }; - return onCreate({ - template_id: state.id!, - template_description: state.description!, - template_contract, - editable_defaults: { - amount: !state.amount_editable ? undefined : state.amount ?? zero, - summary: !state.summary_editable ? undefined : state.summary ?? "", - currency: - cList.length === 1 || !state.currency_editable - ? undefined - : config.currency, - }, - otp_id: state.otpId!, - }); - }; + }, + otp_id: state.otpId!, + } + + const create = safeFunctionHandler(lib.instance.addTemplate, !session.token ? undefined : [session.token, data]) + create.onSuccess = onCreated + create.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized` + case HttpStatusCode.NotFound: return i18n.str`Not found` + } + } + const deviceList = !devices || devices instanceof TalerError || devices.type === "fail" ? [] @@ -171,6 +180,8 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { ); return ( <div> + <LocalNotificationBannerBulma notification={notification} /> + <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -284,13 +295,12 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - disabled={hasErrors} data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } - onClick={submitForm} + onClick={create} > <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> 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 @@ -19,13 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { useSessionContext } from "../../../../context/session.js"; -import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; interface Props { @@ -34,41 +28,12 @@ interface Props { } export default function CreateTemplate({ onConfirm, onBack }: Props): VNode { - const { state, lib } = useSessionContext(); - - const { i18n } = useTranslationContext(); return ( <> - <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} - onCreate={async (request: TalerMerchantApi.TemplateAddDetails) => { - return lib.instance - .addTemplate(state.token, request) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Template has been created`, - type: "SUCCESS", - }); - onConfirm(); - } else { - setNotif({ - message: i18n.str`Could not create template`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not create template`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - }); - }} + onCreated={onConfirm} /> </> ); 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,17 +25,15 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; import { ConfirmModal } from "../../../../components/modal/index.js"; import { useSessionContext } from "../../../../context/session.js"; import { useInstanceTemplates } from "../../../../hooks/templates.js"; -import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CardTable } from "./Table.js"; @@ -55,7 +53,8 @@ export default function ListTemplates({ }: Props): VNode { const { i18n } = useTranslationContext(); - const { state, lib } = useSessionContext(); + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const result = useInstanceTemplates(); const [deleting, setDeleting] = useState<TalerMerchantApi.TemplateEntry | null>(null); @@ -78,6 +77,17 @@ export default function ListTemplates({ } } + const remove = safeFunctionHandler(lib.instance.deleteTemplate, !session.token || !deleting ? undefined : [session.token, deleting.template_id]) + remove.onSuccess = () => { + setDeleting(null) + } + remove.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized` + case HttpStatusCode.NotFound: return i18n.str`Not found` + } + } + return ( <section class="section is-main-section"> <LocalNotificationBannerBulma notification={notification} /> @@ -117,33 +127,7 @@ export default function ListTemplates({ danger active onCancel={() => setDeleting(null)} - onConfirm={async (): Promise<void> => { - try { - const resp = await lib.instance.deleteTemplate( - state.token, - deleting.template_id, - ); - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`, - type: "SUCCESS", - }); - } else { - setNotif({ - message: i18n.str`Failed to delete template`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - } catch (error) { - setNotif({ - message: i18n.str`Failed to delete template`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - setDeleting(null); - }} + confirm={remove} > <p> <i18n.Translate> 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 @@ -23,11 +23,12 @@ import { AmountString, Amounts, Duration, + HttpStatusCode, TalerError, TalerMerchantApi, TranslatedString } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { @@ -60,7 +61,7 @@ type Entity = { }; interface Props { - onUpdate: (d: TalerMerchantApi.TemplatePatchDetails) => Promise<void>; + onUpdated: () => void; onBack?: () => void; template: TalerMerchantApi.TemplateDetails & WithId; } @@ -76,9 +77,10 @@ function changeToCurrency(am: string | undefined, currency: string | undefined): return Amounts.stringify(newAmount); } -export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { +export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { config, state: session } = useSessionContext(); + const { config, state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const cList = Object.values(config.currencies).map((d) => d.name); const supportedCurrencies = Object.keys(config.currencies); @@ -97,8 +99,8 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { supportedCurrencies.indexOf(template_currency) === -1; const startingAmount = unsupportedCurrency - ? changeToCurrency(currentAmount, default_currency) - : currentAmount + ? changeToCurrency(currentAmount, default_currency) + : currentAmount const [state, setState] = useState<Partial<Entity>>({ description: template.template_description, @@ -106,8 +108,8 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { otpId: template.otp_id, pay_duration: template.template_contract.pay_duration ? Duration.fromTalerProtocolDuration( - template.template_contract.pay_duration, - ) + template.template_contract.pay_duration, + ) : undefined, summary: template.editable_defaults?.summary ?? template.template_contract.summary, @@ -178,39 +180,46 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { Amounts.zeroOfCurrency(state.currency ?? default_currency), ); - const submitForm = () => { - if (hasErrors) return Promise.reject(); - const contract_amount = state.amount_editable - ? undefined - : (state.amount as AmountString); - const contract_summary = state.summary_editable ? undefined : state.summary; - const template_contract: TalerMerchantApi.TemplateContractDetails = { - minimum_age: state.minimum_age!, - pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), - amount: contract_amount, - summary: contract_summary, + const contract_amount = state.amount_editable + ? undefined + : (state.amount as AmountString); + const contract_summary = state.summary_editable ? undefined : state.summary; + const template_contract: TalerMerchantApi.TemplateContractDetails = { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: contract_amount, + summary: contract_summary, + currency: + cList.length > 1 && state.currency_editable + ? undefined + : state.currency, + }; + const data: TalerMerchantApi.TemplatePatchDetails = { + template_description: state.description!, + template_contract, + editable_defaults: { + amount: !state.amount_editable ? undefined : state.amount ?? zero, + summary: !state.summary_editable ? undefined : state.summary ?? "", currency: - cList.length > 1 && state.currency_editable + cList.length === 1 || !state.currency_editable ? undefined : state.currency, - }; - return onUpdate({ - template_description: state.description!, - template_contract, - editable_defaults: { - amount: !state.amount_editable ? undefined : state.amount ?? zero, - summary: !state.summary_editable ? undefined : state.summary ?? "", - currency: - cList.length === 1 || !state.currency_editable - ? undefined - : state.currency, - }, - otp_id: state.otpId!, - }); - }; + }, + otp_id: state.otpId!, + } + const update = safeFunctionHandler(lib.instance.updateTemplate, !session.token || !!errors ? undefined : [session.token, template.id, data]) + update.onSuccess = onUpdated + 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"> @@ -271,13 +280,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { name="currency" label={i18n.str`Supported currencies`} values={supportedCurrencies} - // toStr={(str) => { - // if (str === "none") return i18n.str`Without authentication`; - // if (str === "basic") - // return i18n.str`With username and password`; - // if (str === "bearer") return i18n.str`With token`; - // return i18n.str`Do not change`; - // }} /> ) : undefined} @@ -308,9 +310,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { /> </Fragment> )} - {/* <TextField name="sc" label={i18n.str`Supported currencies`}> - <i18n.Translate>{cList.join(", ")}</i18n.Translate> - </TextField> */} <InputNumber<Entity> name="minimum_age" label={i18n.str`Minimum age`} @@ -360,13 +359,12 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - disabled={hasErrors} data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } - onClick={submitForm} + onClick={update} > <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> 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,19 +25,15 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { useSessionContext } from "../../../../context/session.js"; +import { WithId } from "../../../../declaration.js"; import { useTemplateDetails } from "../../../../hooks/templates.js"; -import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; -import { WithId } from "../../../../declaration.js"; export type Entity = TalerMerchantApi.TemplatePatchDetails & WithId; @@ -51,12 +47,8 @@ export default function UpdateTemplate({ onConfirm, onBack, }: Props): VNode { - const { state, lib } = useSessionContext(); const result = useTemplateDetails(tid); - - const { i18n } = useTranslationContext(); - if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />; @@ -77,36 +69,10 @@ export default function UpdateTemplate({ return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <UpdatePage template={{ ...result.body, id: tid }} onBack={onBack} - onUpdate={(data) => { - return lib.instance - .updateTemplate(state.token, tid, data) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Template (ID: ${tid}) has been updated`, - type: "SUCCESS", - }); - onConfirm(); - } else { - setNotif({ - message: i18n.str`Could not update template`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not update template`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - }); - }} + onUpdated={onConfirm} /> </Fragment> ); 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 @@ -19,8 +19,8 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { AmountString, HttpStatusCode, TalerMerchantApi, UsingTemplateDetails } from "@gnu-taler/taler-util"; +import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { @@ -29,18 +29,21 @@ import { } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { useSessionContext } from "../../../../context/session.js"; type Entity = TalerMerchantApi.TemplateContractDetails; interface Props { id: string; template: TalerMerchantApi.TemplateDetails; - onCreateOrder: (d: Entity) => Promise<void>; + onOrderCreated: (orderId: string) => void; onBack?: () => void; } -export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { +export function UsePage({ id, template, onBack, onOrderCreated }: Props): VNode { const { i18n } = useTranslationContext(); + const { lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const [state, setState] = useState<Partial<Entity>>({ currency: @@ -64,19 +67,33 @@ export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { (k) => (errors as any)[k] !== undefined, ); - const submitForm = () => { - if (hasErrors) return Promise.reject(); - if (template.template_contract.amount) { + + /* + * if (template.template_contract.amount) { delete state.amount; } if (template.template_contract.summary) { delete state.summary; } - return onCreateOrder(state as any); - }; + + */ + const useTemplate = safeFunctionHandler(lib.instance.useTemplateCreateOrder, !!errors ? undefined : [id, state as UsingTemplateDetails]) + useTemplate.onSuccess = (success) => { + onOrderCreated(success.order_id) + } + useTemplate.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.` + case HttpStatusCode.Gone: return i18n.str`No more stock for product with ID "${fail.body.product_id}".` + case HttpStatusCode.UnavailableForLegalReasons: return i18n.str`No exchange would accept a payment because of KYC requirements.` + } + } return ( <div> + <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -132,13 +149,12 @@ export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - disabled={hasErrors} data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : "confirm operation" } - onClick={submitForm} + onClick={useTemplate} > <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> 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,18 +25,15 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, 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 { useSessionContext } from "../../../../context/session.js"; import { useTemplateDetails } from "../../../../hooks/templates.js"; -import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UsePage } from "./UsePage.js"; -import { useSessionContext } from "../../../../context/session.js"; export type Entity = TalerMerchantApi.TransferInformation; interface Props { @@ -50,11 +47,8 @@ export default function TemplateUsePage({ onOrderCreated, onBack, }: Props): VNode { - const { lib } = useSessionContext(); const result = useTemplateDetails(tid); - const { i18n } = useTranslationContext(); - if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />; @@ -75,60 +69,11 @@ export default function TemplateUsePage({ return ( <> - <LocalNotificationBannerBulma notification={notification} /> <UsePage template={result.body} id={tid} onBack={onBack} - onCreateOrder={(request: TalerMerchantApi.UsingTemplateDetails) => { - return lib.instance - .useTemplateCreateOrder(tid, request) - .then((resp) => { - if (resp.type === "ok") { - onOrderCreated(resp.body.order_id); - } else { - switch (resp.case) { - case HttpStatusCode.UnavailableForLegalReasons: { - setNotif({ - message: i18n.str`Could not create order`, - type: "ERROR", - description: i18n.str`No exchange would accept a payment because of KYC requirements.`, - }); - return; - } - case HttpStatusCode.Unauthorized: - case HttpStatusCode.NotFound: - case HttpStatusCode.Conflict: { - setNotif({ - message: i18n.str`Could not create order`, - type: "ERROR", - description: resp.detail?.hint, - }); - return; - } - case HttpStatusCode.Gone: { - setNotif({ - message: i18n.str`Could not create order`, - type: "ERROR", - description: i18n.str`No more stock for product with ID "${resp.body.product_id}".`, - }); - return; - } - default: { - assertUnreachable(resp); - } - } - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not create order from template`, - type: "ERROR", - description: - error instanceof Error ? error.message : String(error), - }); - }); - }} + onOrderCreated={onOrderCreated} /> </> ); 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 @@ -19,37 +19,126 @@ * @author Christian Blättler */ -import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { AbsoluteTime, Duration, HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { TokenFamilyForm } from "../../../../components/tokenfamily/TokenFamilyForm.js"; -import { useListener } from "../../../../hooks/listener.js"; +import { useState } from "preact/hooks"; +import { FormErrors, FormProvider } from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { InputDate } from "../../../../components/form/InputDate.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; +import { useSessionContext } from "../../../../context/session.js"; type Entity = TalerMerchantApi.TokenFamilyCreateRequest; export interface Props { - onCreate: (d: Entity) => Promise<void>; + onCreated: () => void; onBack?: () => void; } -export function CreatePage({ onCreate, onBack }: Props): VNode { - const [submitForm, addFormSubmitter] = useListener<Entity | undefined>( - (result) => { - if (result) return onCreate(result); - return Promise.reject(); - }, - ); - +export function CreatePage({ onCreated, onBack }: Props): VNode { + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const { i18n } = useTranslationContext(); + const [value, valueHandler] = useState<Partial<Entity>>({ + slug: undefined, + name: undefined, + description: undefined, + description_i18n: {}, + kind: TalerMerchantApi.TokenFamilyKind.Discount, + duration: Duration.toTalerProtocolDuration(Duration.getForever()), + valid_after: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + valid_before: AbsoluteTime.toProtocolTimestamp(AbsoluteTime.never()), + validity_granularity: Duration.toTalerProtocolDuration(Duration.getForever()), + }); + + const errors: FormErrors<Entity> = { + slug: !value.slug ? i18n.str`Required` : undefined, + name: !value.name ? i18n.str`Required` : undefined, + description: !value.description ? i18n.str`Required` : undefined, + valid_after: !value.valid_after ? undefined : undefined, + valid_before: !value.valid_before ? i18n.str`Required` : undefined, + validity_granularity: !value.validity_granularity ? i18n.str`Required` : undefined, + duration: !value.duration ? i18n.str`Required` : undefined, + kind: !value.kind ? i18n.str`Required` : undefined, + }; + + + const create = safeFunctionHandler(lib.instance.createTokenFamily, !session.token || !!errors ? undefined : [session.token, value as Entity]); + create.onSuccess = onCreated + create.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: return i18n.str`Unaouthorized` + 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" /> <div class="column is-four-fifths"> - <TokenFamilyForm onSubscribe={addFormSubmitter} /> - + <FormProvider<Entity> + name="token_family" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <InputWithAddon<Entity> + name="slug" + addonBefore={new URL("tokenfamily/", session.backendUrl.href).href} + label={i18n.str`Slug`} + tooltip={i18n.str`Token family slug to use in URLs (for internal use only)`} + /> + <InputSelector<Entity> + name="kind" + label={i18n.str`Kind`} + tooltip={i18n.str`Token family kind`} + values={["discount", "subscription"]} + /> + <Input<Entity> + name="name" + inputType="text" + label={i18n.str`Name`} + tooltip={i18n.str`User-readable token family name`} + /> + <Input<Entity> + name="description" + inputType="multiline" + label={i18n.str`Description`} + tooltip={i18n.str`Token family description for customers`} + /> + <InputDate<Entity> + name="valid_after" + label={i18n.str`Valid After`} + tooltip={i18n.str`Token family can issue tokens after this date`} + withTimestampSupport + /> + <InputDate<Entity> + name="valid_before" + label={i18n.str`Valid Before`} + tooltip={i18n.str`Token family can issue tokens until this date`} + withTimestampSupport + /> + <InputDuration<Entity> + name="validity_granularity" + label={i18n.str`Validity Granularity`} + tooltip={i18n.str`Rounding granularity for the start validity of keys, must be 1 minute, 1 hour, 1 day, 1 week, 30 days or 90 days`} + useProtocolDuration + /> + <InputDuration<Entity> + name="duration" + label={i18n.str`Duration`} + tooltip={i18n.str`Validity duration of a issued token`} + withForever + useProtocolDuration + /> + </FormProvider> {/* <Test /> */} <div class="buttons is-right mt-5"> @@ -59,13 +148,12 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { </button> )} <ButtonBetterBulma - onClick={submitForm} + onClick={create} data-tooltip={ - !submitForm + !create.args ? i18n.str`Please complete the marked fields` : "confirm operation" } - disabled={!submitForm} > <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> 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,14 +19,9 @@ * @author Christian Blättler */ -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { Notification } from "../../../../utils/types.js"; -import { useSessionContext } from "../../../../context/session.js"; import { CreatePage } from "./CreatePage.js"; -import { TalerMerchantApi } from "@gnu-taler/taler-util"; export type Entity = TalerMerchantApi.TokenFamilyCreateRequest; interface Props { @@ -35,39 +30,11 @@ interface Props { } export default function CreateTokenFamily({ onConfirm, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - const { state, lib } = useSessionContext(); - return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <CreatePage onBack={onBack} - onCreate={(request) => { - return lib.instance.createTokenFamily(state.token, request) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Token family created successfully.`, - type: "SUCCESS", - }); - onConfirm(); - } else { - setNotif({ - message: i18n.str`Could not create token family.`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not create token family.`, - type: "ERROR", - description: error instanceof Error ? error.message : String(error), - }); - }); - }} + onCreated={onConfirm} /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/Table.tsx @@ -31,10 +31,6 @@ interface Props { instances: Entity[]; onDelete: (tokenFamily: Entity) => void; onSelect: (tokenFamily: Entity) => void; - onUpdate: ( - slug: string, - data: TalerMerchantApi.TokenFamilyUpdateRequest, - ) => Promise<void>; onCreate: () => void; selected?: boolean; } @@ -43,7 +39,6 @@ export function CardTable({ instances, onCreate, onSelect, - onUpdate, onDelete, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string | undefined>( @@ -80,7 +75,6 @@ export function CardTable({ instances={instances} onSelect={onSelect} onDelete={onDelete} - onUpdate={onUpdate} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> @@ -97,10 +91,6 @@ interface TableProps { rowSelection: string | undefined; instances: Entity[]; onSelect: (tokenFamily: Entity) => void; - onUpdate: ( - slug: string, - data: TalerMerchantApi.TokenFamilyUpdateRequest, - ) => Promise<void>; onDelete: (tokenFamily: Entity) => void; rowSelectionHandler: StateUpdater<string | undefined>; } 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,16 +25,14 @@ import { TalerMerchantApi, assertUnreachable, } from "@gnu-taler/taler-util"; -import { LocalNotificationBannerBulma, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { 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 { ConfirmModal } from "../../../../components/modal/index.js"; import { useSessionContext } from "../../../../context/session.js"; import { useInstanceTokenFamilies } from "../../../../hooks/tokenfamily.js"; -import { Notification } from "../../../../utils/types.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CardTable } from "./Table.js"; @@ -48,11 +46,12 @@ interface Props { export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { const result = useInstanceTokenFamilies(); - const { lib, state } = useSessionContext(); const [deleting, setDeleting] = useState<TalerMerchantApi.TokenFamilySummary | null>(null); const { i18n } = useTranslationContext(); + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); if (!result) return <Loading />; if (result instanceof TalerError) { @@ -72,6 +71,19 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { } } + + const remove = safeFunctionHandler(lib.instance.deleteTokenFamily, !session.token || !deleting ? undefined : [session.token, deleting.slug]); + remove.onSuccess = () => { + setDeleting(null); + return i18n.str`Token family has been deleted.` + } + remove.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: return i18n.str`Unaouthorized` + case HttpStatusCode.NotFound: return i18n.str`Not found` + } + } + return ( <section class="section is-main-section"> <LocalNotificationBannerBulma notification={notification} /> @@ -79,34 +91,6 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { <CardTable instances={result.body.token_families} onCreate={onCreate} - onUpdate={async (slug, fam) => { - try { - const resp = await lib.instance.updateTokenFamily( - state.token, - slug, - fam, - ); - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Token family updated successfully`, - type: "SUCCESS", - }); - } else { - setNotif({ - message: i18n.str`Could not update the token family`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - } catch (error) { - setNotif({ - message: i18n.str`Could not update the token family`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - return; - }} onSelect={(tokenFamily) => onSelect(tokenFamily.slug)} onDelete={(fam) => setDeleting(fam)} /> @@ -118,33 +102,7 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { danger active onCancel={() => setDeleting(null)} - onConfirm={async (): Promise<void> => { - try { - const resp = await lib.instance.deleteTokenFamily( - state.token, - deleting.slug, - ); - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Token family "${deleting.name}" (SLUG: ${deleting.slug}) has been deleted`, - type: "SUCCESS", - }); - } else { - setNotif({ - message: i18n.str`Failed to delete token family`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - } catch (error) { - setNotif({ - message: i18n.str`Failed to delete token family`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - setDeleting(null); - }} + confirm={remove} > <p> <i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx @@ -19,8 +19,8 @@ * @author Christian Blättler */ -import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; import { h } from "preact"; import { useState } from "preact/hooks"; import { FormErrors, FormProvider } from "../../../../components/form/FormProvider.js"; @@ -28,16 +28,17 @@ import { Input } from "../../../../components/form/Input.js"; import { InputDate } from "../../../../components/form/InputDate.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { useSessionContext } from "../../../../context/session.js"; type Entity = TalerMerchantApi.TokenFamilyUpdateRequest; interface Props { - onUpdate: (d: TalerMerchantApi.TokenFamilyUpdateRequest) => Promise<void>; + onUpdated: () => void; onBack?: () => void; - tokenFamily: TalerMerchantApi.TokenFamilyUpdateRequest; + tokenFamily: TalerMerchantApi.TokenFamilyDetails; } -export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { +export function UpdatePage({ onUpdated, onBack, tokenFamily }: Props) { const [value, valueHandler] = useState<Partial<Entity>>(tokenFamily); const { i18n } = useTranslationContext(); const errors = undefinedIfEmpty<FormErrors<Entity>>({ @@ -47,18 +48,24 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { valid_before: !value.valid_before ? i18n.str`Required` : undefined, duration: !value.duration ? i18n.str`Required` : undefined, }); + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const hasErrors = errors !== undefined; - const submitForm = () => { - if (hasErrors) return Promise.reject(); - - return onUpdate(value as Entity); + const update = safeFunctionHandler(lib.instance.updateTokenFamily, !session.token || !!errors ? undefined : [session.token, tokenFamily.slug, value as Entity]); + update.onSuccess = onUpdated + update.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: return i18n.str`Unaouthorized` + case HttpStatusCode.NotFound: return i18n.str`Not found` + } } return ( <div> + <LocalNotificationBannerBulma notification={notification} /> <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -124,13 +131,12 @@ export function UpdatePage({ onUpdate, onBack, tokenFamily }: Props) { </button> )} <ButtonBetterBulma - disabled={hasErrors} data-tooltip={ hasErrors ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } - onClick={submitForm} + onClick={update} > <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> 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 @@ -49,10 +49,6 @@ export default function UpdateTokenFamily({ }: Props): VNode { const result = useTokenFamilyDetails(slug); - const { lib, state } = useSessionContext(); - - const { i18n } = useTranslationContext(); - if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />; @@ -71,48 +67,39 @@ export default function UpdateTokenFamily({ } } - const family: Entity = { - name: result.body.name, - description: result.body.description, - description_i18n: result.body.description_i18n || {}, - duration: result.body.duration, - valid_after: result.body.valid_after, - valid_before: result.body.valid_before, - }; - return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> <UpdatePage - tokenFamily={family} + tokenFamily={result.body} onBack={onBack} - onUpdate={(data) => { - return lib.instance - .updateTokenFamily(state.token, slug, data) - .then((resp) => { - if (resp.type === "ok") { - setNotif({ - message: i18n.str`Token family updated successfully`, - type: "SUCCESS", - }); - onConfirm(); - } else { - setNotif({ - message: i18n.str`Could not update token family`, - type: "ERROR", - description: resp.detail?.hint, - }); - } - }) - .catch((error) => { - setNotif({ - message: i18n.str`Could not update token family`, - type: "ERROR", - description: - error instanceof Error ? error.message : String(error), - }); - }); - }} + onUpdated={onConfirm} + // onUpdate={(data) => { + // return lib.instance + // .updateTokenFamily(state.token, slug, data) + // .then((resp) => { + // if (resp.type === "ok") { + // setNotif({ + // message: i18n.str`Token family updated successfully`, + // type: "SUCCESS", + // }); + // onConfirm(); + // } else { + // setNotif({ + // message: i18n.str`Could not update token family`, + // type: "ERROR", + // description: resp.detail?.hint, + // }); + // } + // }) + // .catch((error) => { + // setNotif({ + // message: i18n.str`Could not update token family`, + // type: "ERROR", + // description: + // error instanceof Error ? error.message : String(error), + // }); + // }); + // }} /> </Fragment> );