diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance/templates')
16 files changed, 583 insertions, 544 deletions
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx index c9d17ea3b..53025f153 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software 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 index 947f3572c..78d7c83ac 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -20,10 +20,16 @@ */ import { + AmountString, Amounts, - MerchantTemplateContractDetails, + Duration, + TalerError, + TalerMerchantApi, + TranslatedString, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; @@ -35,109 +41,127 @@ 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 { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; +import { TextField } from "../../../../components/form/TextField.js"; +import { useSessionContext } from "../../../../context/session.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { InputTab } from "../../../../components/form/InputTab.js"; - -enum Steps { - BOTH_FIXED, - FIXED_PRICE, - FIXED_SUMMARY, - NON_FIXED, -} -type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps }; +// type Entity = TalerMerchantApi.TemplateAddDetails & { type: Steps }; +type Entity = { + id?: string; + description?: string; + otpId?: string; + summary?: string; + amount?: AmountString; + minimum_age?: number; + pay_duration?: Duration; + summary_editable?: boolean; + amount_editable?: boolean; + currency_editable?: boolean; +}; interface Props { - onCreate: (d: Entity) => Promise<void>; + onCreate: (d: TalerMerchantApi.TemplateAddDetails) => Promise<void>; onBack?: () => void; } export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const devices = useInstanceOtpDevices() + const { config } = useSessionContext(); + const {state:session} = useSessionContext(); + const devices = useInstanceOtpDevices(); const [state, setState] = useState<Partial<Entity>>({ - template_contract: { - minimum_age: 0, - pay_duration: { - d_us: 1000 * 1000 * 60 * 30, //30 min - }, + minimum_age: 0, + pay_duration: { + d_ms: 1000 * 60 * 30, //30 min }, - type: Steps.NON_FIXED, }); - const parsedPrice = !state.template_contract?.amount - ? undefined - : Amounts.parse(state.template_contract?.amount); + function updateState(up: (s: Partial<Entity>) => Partial<Entity>) { + setState((old) => { + const newState = up(old); + if (!newState.amount_editable) { + newState.currency_editable = false; + } + return newState; + }); + } + + const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount); const errors: FormErrors<Entity> = { - template_id: !state.template_id + id: !state.id ? i18n.str`should not be empty` - : !/[a-zA-Z0-9]*/.test(state.template_id) + : !/[a-zA-Z0-9]*/.test(state.id) ? i18n.str`no valid. only characters and numbers` : undefined, - template_description: !state.template_description - ? i18n.str`should not be empty` - : undefined, - template_contract: !state.template_contract + description: !state.description ? i18n.str`should not be empty` : undefined, + amount: !state.amount ? undefined - : undefinedIfEmpty({ - amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.amount - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.summary - ? i18n.str`required` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` - : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" - ? undefined - : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second - ? i18n.str`to short` - : undefined, - } as Partial<MerchantTemplateContractDetails>), + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + minimum_age: + state.minimum_age && state.minimum_age < 0 + ? i18n.str`should be greater that 0` + : undefined, + pay_duration: !state.pay_duration + ? i18n.str`can't be empty` + : state.pay_duration.d_ms === "forever" + ? undefined + : state.pay_duration.d_ms < 1000 //less than one second + ? i18n.str`to short` + : undefined, }; + const cList = Object.values(config.currencies).map((d) => d.name); + const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); const submitForm = () => { if (hasErrors) return Promise.reject(); - if (state.template_contract) { - if (state.type === Steps.NON_FIXED) { - delete state.template_contract.amount; - delete state.template_contract.summary; - } else if (state.type === Steps.FIXED_SUMMARY) { - delete state.template_contract.amount; - } else if (state.type === Steps.FIXED_PRICE) { - delete state.template_contract.summary; - } - } - delete state.type - return onCreate(state as any); + return onCreate({ + template_id: state.id!, + template_description: state.description!, + template_contract: { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: state.amount_editable ? undefined : state.amount, + summary: state.summary_editable ? undefined : state.summary, + currency: + cList.length > 1 && state.currency_editable + ? undefined + : config.currency, + }, + editable_defaults: { + amount: !state.amount_editable ? undefined : state.amount, + summary: !state.summary_editable ? undefined : state.summary, + currency: + cList.length === 1 || !state.currency_editable + ? undefined + : config.currency, + }, + otp_id: state.otpId!, + }); }; - - const deviceList = !devices.ok ? [] : devices.data.otp_devices - + const deviceList = + !devices || devices instanceof TalerError || devices.type === "fail" + ? [] + : devices.body.otp_devices; + const deviceMap = deviceList.reduce( + (prev, cur) => { + prev[cur.otp_device_id] = cur.device_description as TranslatedString; + return prev; + }, + {} as Record<string, TranslatedString>, + ); return ( <div> <section class="section is-main-section"> @@ -146,90 +170,99 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <div class="column is-four-fifths"> <FormProvider object={state} - valueHandler={setState} + valueHandler={updateState} errors={errors} > <InputWithAddon<Entity> - name="template_id" - help={`${backendURL}/templates/${state.template_id ?? ""}`} + name="id" + help={ + new URL(`templates/${state.id ?? ""}`, session.backendUrl.href).href + } label={i18n.str`Identifier`} tooltip={i18n.str`Name of the template in URLs.`} /> <Input<Entity> - name="template_description" + name="description" label={i18n.str`Description`} help="" tooltip={i18n.str`Describe what this template stands for`} /> - <InputTab - name="type" - label={i18n.str`Type`} - help={(() => { - switch (state.type) { - case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` - case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` - case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` - case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` - } - })()} - tooltip={i18n.str`Define what the user be allowed to modify`} - values={[ - Steps.NON_FIXED, - Steps.FIXED_PRICE, - Steps.FIXED_SUMMARY, - Steps.BOTH_FIXED, - ]} - toStr={(v: Steps): string => { - switch (v) { - case Steps.NON_FIXED: return i18n.str`Simple` - case Steps.FIXED_PRICE: return i18n.str`With price` - case Steps.FIXED_SUMMARY: return i18n.str`With summary` - case Steps.BOTH_FIXED: return i18n.str`With price and summary` - } - }} + + <Input<Entity> + name="summary" + inputType="multiline" + label={i18n.str`Summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} /> - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? - <Input - name="template_contract.summary" - inputType="multiline" - label={i18n.str`Fixed summary`} - tooltip={i18n.str`If specified, this template will create order with the same summary`} - /> - : undefined} - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? - <InputCurrency - name="template_contract.amount" - label={i18n.str`Fixed price`} - tooltip={i18n.str`If specified, this template will create order with the same price`} - /> - : undefined} - <InputNumber - name="template_contract.minimum_age" + <InputToggle<Entity> + name="summary_editable" + label={i18n.str`Summary is editable`} + tooltip={i18n.str`Allow the user to change the summary.`} + /> + + <InputCurrency<Entity> + name="amount" + label={i18n.str`Amount`} + tooltip={i18n.str`If specified, this template will create order with the same price`} + /> + <InputToggle<Entity> + name="amount_editable" + label={i18n.str`Amount is editable`} + tooltip={i18n.str`Allow the user to select the amount to pay.`} + /> + {cList.length > 1 && ( + <Fragment> + <InputToggle<Entity> + name="currency_editable" + readonly={!state.amount_editable} + label={i18n.str`Currency is editable`} + tooltip={i18n.str`Allow the user to change currency.`} + /> + <TextField name="sc" label={i18n.str`Supported currencies`}> + <i18n.Translate>supported currencies: {cList.join(", ")}</i18n.Translate> + </TextField> + </Fragment> + )} + <InputNumber<Entity> + name="minimum_age" label={i18n.str`Minimum age`} help="" tooltip={i18n.str`Is this contract restricted to some age?`} /> - <InputDuration - name="template_contract.pay_duration" + <InputDuration<Entity> + name="pay_duration" label={i18n.str`Payment timeout`} help="" tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} /> - <Input<Entity> - name="otp_id" - label={i18n.str`OTP device`} - readonly - tooltip={i18n.str`Use to verify transaction in offline mode.`} - /> - <InputSearchOnList - label={i18n.str`Search device`} - onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))} - list={deviceList.map(e => ({ - description: e.device_description, - id: e.otp_device_id - }))} - /> - + {!deviceList.length ? ( + <TextField + name="otpId" + label={i18n.str`OTP device`} + tooltip={i18n.str`Use to verify transaction while offline.`} + > + <i18n.Translate>No OTP device.</i18n.Translate> + <a href="/otp-devices/new"> + <i18n.Translate>Add one first</i18n.Translate> + </a> + </TextField> + ) : ( + <InputSelector<Entity> + name="otpId" + label={i18n.str`OTP device`} + values={[ + undefined, + ...deviceList.map((e) => e.otp_device_id), + ]} + toStr={(v?: string) => { + if (!v) { + return i18n.str`No device`; + } + return deviceMap[v]; + }} + tooltip={i18n.str`Use to verify transaction in offline mode.`} + /> + )} </FormProvider> <div class="buttons is-right mt-5"> 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 index a29ee53b6..499c7c859 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,23 +19,24 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTemplateAPI } from "../../../../hooks/templates.js"; +import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -export type Entity = MerchantBackend.Transfers.TransferInformation; +export type Entity = TalerMerchantApi.TransferInformation; interface Props { onBack?: () => void; onConfirm: () => void; } export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { - const { createTemplate } = useTemplateAPI(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); @@ -44,8 +45,8 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: MerchantBackend.Template.TemplateAddDetails) => { - return createTemplate(request) + onCreate={(request: TalerMerchantApi.TemplateAddDetails) => { + return lib.instance.addTemplate(state.token, request) .then(() => onConfirm()) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx index 702e9ba4a..707324d40 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx index bf6062c34..66d8a2f7e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,20 +19,19 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { CardTable } from "./Table.js"; export interface Props { - templates: MerchantBackend.Template.TemplateEntry[]; + templates: TalerMerchantApi.TemplateEntry[]; onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; onCreate: () => void; - onDelete: (e: MerchantBackend.Template.TemplateEntry) => void; - onSelect: (e: MerchantBackend.Template.TemplateEntry) => void; - onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void; - onQR: (e: MerchantBackend.Template.TemplateEntry) => void; + onDelete: (e: TalerMerchantApi.TemplateEntry) => void; + onSelect: (e: TalerMerchantApi.TemplateEntry) => void; + onNewOrder: (e: TalerMerchantApi.TemplateEntry) => void; + onQR: (e: TalerMerchantApi.TemplateEntry) => void; } export function ListPage({ @@ -45,9 +44,7 @@ export function ListPage({ onLoadMoreBefore, onLoadMoreAfter, }: Props): VNode { - const form = { payto_uri: "" }; - const { i18n } = useTranslationContext(); return ( <CardTable templates={templates.map((o) => ({ @@ -60,9 +57,7 @@ export function ListPage({ onSelect={onSelect} onNewOrder={onNewOrder} onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} /> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx index 9fdf4ead9..082e622e3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,12 +19,12 @@ * @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 { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../../declaration.js"; -type Entity = MerchantBackend.Template.TemplateEntry; +type Entity = TalerMerchantApi.TemplateEntry; interface Props { templates: Entity[]; @@ -34,8 +34,6 @@ interface Props { onQR: (e: Entity) => void; onCreate: () => void; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } @@ -48,8 +46,6 @@ export function CardTable({ onNewOrder, onLoadMoreAfter, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -91,8 +87,6 @@ export function CardTable({ rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} /> ) : ( <EmptyTable /> @@ -112,16 +106,9 @@ interface TableProps { onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - function Table({ instances, onLoadMoreAfter, @@ -130,19 +117,17 @@ function Table({ onQR, onSelect, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class="table-container"> - {hasMoreBefore && ( + {onLoadMoreBefore && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more templates before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load newer templates</i18n.Translate> + <i18n.Translate>load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -203,13 +188,13 @@ function Table({ })} </tbody> </table> - {hasMoreAfter && ( + {onLoadMoreAfter && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more templates after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load older templates</i18n.Translate> + <i18n.Translate>load next page</i18n.Translate> </button> )} </div> @@ -222,7 +207,7 @@ function EmptyTable(): VNode { <div class="content has-text-grey has-text-centered"> <p> <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> + <i class="mdi mdi-magnify mdi-48px" /> </span> </p> <p> 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 index b9767442f..9e59609c7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,30 +19,27 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +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 { MerchantBackend } from "../../../../declaration.js"; +import { ConfirmModal } from "../../../../components/modal/index.js"; +import { useSessionContext } from "../../../../context/session.js"; import { - useInstanceTemplates, - useTemplateAPI, + useInstanceTemplates } from "../../../../hooks/templates.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; -import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; -import { ConfirmModal } from "../../../../components/modal/index.js"; -import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; onCreate: () => void; onSelect: (id: string) => void; onNewOrder: (id: string) => void; @@ -50,35 +47,35 @@ interface Props { } export default function ListTemplates({ - onUnauthorized, - onLoadError, onCreate, onQR, onSelect, onNewOrder, - onNotFound, }: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteTemplate, testTemplateExist } = useTemplateAPI(); - const result = useInstanceTemplates({ position }, (id) => setPosition(id)); + const { lib } = useSessionContext(); + const result = useInstanceTemplates(); const [deleting, setDeleting] = - useState<MerchantBackend.Template.TemplateEntry | null>(null); + useState<TalerMerchantApi.TemplateEntry | null>(null); + const { state } = useSessionContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } } return ( @@ -86,18 +83,21 @@ export default function ListTemplates({ <NotificationCard notification={notif} /> <JumpToElementById - testIfExist={testTemplateExist} + testIfExist={async (id) => { + const resp = await lib.instance.getTemplateDetails(state.token, id) + return resp.type === "ok" + }} onSelect={onSelect} description={i18n.str`jump to template with the given template ID`} - palceholder={i18n.str`template id`} + placeholder={i18n.str`template id`} /> <ListPage - templates={result.data.templates} + templates={result.body} onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined + result.isFirstPage ? undefined: result.loadFirst } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} onCreate={onCreate} onSelect={(e) => { onSelect(e.template_id); @@ -108,7 +108,7 @@ export default function ListTemplates({ onQR={(e) => { onQR(e.template_id); }} - onDelete={(e: MerchantBackend.Template.TemplateEntry) => { + onDelete={(e: TalerMerchantApi.TemplateEntry) => { setDeleting(e) } } @@ -123,7 +123,7 @@ export default function ListTemplates({ onCancel={() => setDeleting(null)} onConfirm={async (): Promise<void> => { try { - await deleteTemplate(deleting.template_id); + await lib.instance.deleteTemplate(state.token, deleting.template_id); setNotif({ message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`, type: "SUCCESS", diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx index eb853c8ff..c0059c7bc 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx index 5140aae3a..7322ca169 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,113 +19,101 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { stringifyPayTemplateUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + TalerMerchantApi, + stringifyPayTemplateUri +} from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { useState } from "preact/hooks"; import { QR } from "../../../../components/exception/QR.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { useInstanceContext } from "../../../../context/instance.js"; -import { MerchantBackend } from "../../../../declaration.js"; - -type Entity = MerchantBackend.Template.UsingTemplateDetails; +import { useSessionContext } from "../../../../context/session.js"; + +// type Entity = TalerMerchantApi.UsingTemplateDetails; interface Props { - contract: MerchantBackend.Template.TemplateContractDetails; + contract: TalerMerchantApi.TemplateContractDetails; id: string; onBack?: () => void; } -export function QrPage({ contract, id: templateId, onBack }: Props): VNode { +export function QrPage({ id: templateId, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const { id: instanceId } = useInstanceContext(); - const config = useConfigContext(); + const { state } = useSessionContext(); - const [state, setState] = useState<Partial<Entity>>({ - amount: contract.amount, - summary: contract.summary, - }); + // const [state, setState] = useState<Partial<Entity>>({ + // amount: contract.amount, + // summary: contract.summary, + // }); - const errors: FormErrors<Entity> = {}; + // const errors: FormErrors<Entity> = {}; - const fixedAmount = !!contract.amount; - const fixedSummary = !!contract.summary; + // const fixedAmount = !!contract.amount; + // const fixedSummary = !!contract.summary; - const templateParams: Record<string, string> = {} - if (!fixedAmount) { - if (state.amount) { - templateParams.amount = state.amount - } else { - templateParams.amount = config.currency - } - } + // const templateParams: Record<string, string> = {}; + // if (!fixedAmount) { + // if (state.amount) { + // templateParams.amount = state.amount; + // } else { + // templateParams.amount = config.currency; + // } + // } - if (!fixedSummary) { - templateParams.summary = state.summary ?? "" - } + // if (!fixedSummary) { + // templateParams.summary = state.summary ?? ""; + // } - const merchantBaseUrl = new URL(backendURL).href; + const merchantBaseUrl = state.backendUrl.href; const payTemplateUri = stringifyPayTemplateUri({ merchantBaseUrl, templateId, - templateParams - }) - - const issuer = encodeURIComponent( - `${new URL(backendURL).host}/${instanceId}`, - ); + templateParams: {}, + }); return ( <div> + <section id="printThis"> + <QR text={payTemplateUri} /> + <pre style={{ textAlign: "center" }}> + <a href={payTemplateUri}>{payTemplateUri}</a> + </pre> + </section> + <section class="section is-main-section"> <div class="columns"> <div class="column" /> <div class="column is-four-fifths"> - <p class="is-size-5 mt-5 mb-5"> + {/* <p class="is-size-5 mt-5 mb-5"> <i18n.Translate> Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment. </i18n.Translate> - </p> + </p> */} <p></p> - <FormProvider + {/* <FormProvider object={state} valueHandler={setState} errors={errors} > <InputCurrency<Entity> name="amount" - label={ - fixedAmount - ? i18n.str`Fixed amount` - : i18n.str`Default amount` - } - readonly={fixedAmount} + label={i18n.str`Amount`} + readonly tooltip={i18n.str`Amount of the order`} /> <Input<Entity> name="summary" inputType="multiline" - readonly={fixedSummary} - label={ - fixedSummary - ? i18n.str`Fixed summary` - : i18n.str`Default summary` - } + readonly + label={i18n.str`Summary`} tooltip={i18n.str`Title of the order to be shown to the customer`} /> - </FormProvider> + </FormProvider> */} <div class="buttons is-right mt-5"> {onBack && ( @@ -144,12 +132,6 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode { <div class="column" /> </div> </section> - <section id="printThis"> - <QR text={payTemplateUri} /> - <pre style={{ textAlign: "center" }}> - <a href={payTemplateUri}>{payTemplateUri}</a> - </pre> - </section> </div> ); } @@ -167,6 +149,6 @@ function saveAsPDF(name: string): void { printWindow.document.body.appendChild(divContents.cloneNode(true)); printWindow.addEventListener("load", () => { printWindow.print(); - printWindow.close(); + // printWindow.close(); }); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx index 7db7478f7..ed809c7b3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,62 +19,48 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { - ErrorType, - HttpError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { - useTemplateAPI, - useTemplateDetails, + useTemplateDetails } from "../../../../hooks/templates.js"; -import { Notification } from "../../../../utils/types.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { QrPage } from "./QrPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { LoginPage } from "../../../login/index.js"; -export type Entity = MerchantBackend.Transfers.TransferInformation; +export type Entity = TalerMerchantApi.TransferInformation; interface Props { onBack?: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; tid: string; } export default function TemplateQrPage({ tid, onBack, - onLoadError, - onNotFound, - onUnauthorized, }: Props): VNode { const result = useTemplateDetails(tid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } + } + return ( - <> - <NotificationCard notification={notif} /> - <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} /> - </> + <QrPage contract={result.body.template_contract} id={tid} onBack={onBack} /> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx index 8d07cb31f..303d17b72 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software 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 index b578d4664..eedb77f28 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -20,10 +20,16 @@ */ import { + AmountString, Amounts, - MerchantTemplateContractDetails, + Duration, + TalerError, + TalerMerchantApi, + TranslatedString, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; @@ -35,102 +41,133 @@ 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 { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { InputTab } from "../../../../components/form/InputTab.js"; - -enum Steps { - BOTH_FIXED, - FIXED_PRICE, - FIXED_SUMMARY, - NON_FIXED, -} +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; +import { TextField } from "../../../../components/form/TextField.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; -type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; +type Entity = { + description?: string; + otpId?: string | null; + summary?: string; + amount?: AmountString; + minimum_age?: number; + pay_duration?: Duration; + summary_editable?: boolean; + amount_editable?: boolean; + currency_editable?: boolean; +}; interface Props { - onUpdate: (d: Entity) => Promise<void>; + onUpdate: (d: TalerMerchantApi.TemplatePatchDetails) => Promise<void>; onBack?: () => void; - template: Entity; + template: TalerMerchantApi.TemplateDetails & WithId; } export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() + const { config } = useSessionContext(); + const {state:session} = useSessionContext(); + + const [state, setState] = useState<Partial<Entity>>({ + description: template.template_description, + minimum_age: template.template_contract.minimum_age, + otpId: template.otp_id, + pay_duration: template.template_contract.pay_duration + ? Duration.fromTalerProtocolDuration( + template.template_contract.pay_duration, + ) + : undefined, + summary: + template.editable_defaults?.summary ?? template.template_contract.summary, + amount: + template.editable_defaults?.amount ?? + (template.template_contract.amount as AmountString | undefined), + currency_editable: !!template.editable_defaults?.currency, + summary_editable: !!template.editable_defaults?.summary, + amount_editable: !!template.editable_defaults?.amount, + }); - const intialStep = - template.template_contract?.amount === undefined && template.template_contract?.summary === undefined - ? Steps.NON_FIXED - : template.template_contract?.summary === undefined - ? Steps.FIXED_PRICE - : template.template_contract?.amount === undefined - ? Steps.FIXED_SUMMARY - : Steps.BOTH_FIXED; + function updateState(up: (s: Partial<Entity>) => Partial<Entity>) { + setState((old) => { + const newState = up(old); + if (!newState.amount_editable) { + newState.currency_editable = false; + } + return newState; + }); + } - const [state, setState] = useState<Partial<Entity & { type: Steps }>>({ ...template, type: intialStep }); + const devices = useInstanceOtpDevices(); + const deviceList = + !devices || devices instanceof TalerError || devices.type === "fail" + ? [] + : devices.body.otp_devices; + const deviceMap = deviceList.reduce( + (prev, cur) => { + prev[cur.otp_device_id] = cur.device_description as TranslatedString; + return prev; + }, + {} as Record<string, TranslatedString>, + ); - const parsedPrice = !state.template_contract?.amount - ? undefined - : Amounts.parse(state.template_contract?.amount); + const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount); const errors: FormErrors<Entity> = { - template_description: !state.template_description - ? i18n.str`should not be empty` - : undefined, - template_contract: !state.template_contract + description: !state.description ? i18n.str`should not be empty` : undefined, + amount: !state.amount ? undefined - : undefinedIfEmpty({ - amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.amount - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.summary - ? i18n.str`required` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` - : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" - ? undefined - : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second - ? i18n.str`to short` - : undefined, - } as Partial<MerchantTemplateContractDetails>), + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + minimum_age: + state.minimum_age && state.minimum_age < 0 + ? i18n.str`should be greater that 0` + : undefined, + pay_duration: !state.pay_duration + ? i18n.str`can't be empty` + : state.pay_duration.d_ms === "forever" + ? undefined + : state.pay_duration.d_ms < 1000 // less than one second + ? i18n.str`to short` + : undefined, }; + const cList = Object.values(config.currencies).map((d) => d.name); + const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); const submitForm = () => { if (hasErrors) return Promise.reject(); - if (state.template_contract) { - if (state.type === Steps.NON_FIXED) { - delete state.template_contract.amount; - delete state.template_contract.summary; - } else if (state.type === Steps.FIXED_SUMMARY) { - delete state.template_contract.amount; - } else if (state.type === Steps.FIXED_PRICE) { - delete state.template_contract.summary; - } - } - delete state.type - return onUpdate(state as any); + return onUpdate({ + template_description: state.description!, + template_contract: { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: state.amount_editable ? undefined : state.amount, + summary: state.summary_editable ? undefined : state.summary, + currency: + cList.length > 1 && state.currency_editable + ? undefined + : config.currency, + }, + editable_defaults: { + amount: !state.amount_editable ? undefined : state.amount, + summary: !state.summary_editable ? undefined : state.summary, + currency: + cList.length === 1 || !state.currency_editable + ? undefined + : config.currency, + }, + otp_id: state.otpId!, + }); }; - return ( <div> <section class="section"> @@ -140,7 +177,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - {backendURL}/templates/{template.id} + {new URL(`templates/${template.id}`, session.backendUrl.href).href} </span> </div> </div> @@ -154,77 +191,91 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { <div class="column is-four-fifths"> <FormProvider object={state} - valueHandler={setState} + valueHandler={updateState} errors={errors} > - <InputWithAddon<Entity> - name="id" - addonBefore={`templates/`} - readonly - label={i18n.str`Identifier`} - tooltip={i18n.str`Name of the template in URLs.`} - /> - <Input<Entity> - name="template_description" + name="description" label={i18n.str`Description`} help="" tooltip={i18n.str`Describe what this template stands for`} /> - <InputTab - name="type" - label={i18n.str`Type`} - help={(() => { - switch (state.type) { - case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` - case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` - case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` - case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` - } - })()} - tooltip={i18n.str`Define what the user be allowed to modify`} - values={[ - Steps.NON_FIXED, - Steps.FIXED_PRICE, - Steps.FIXED_SUMMARY, - Steps.BOTH_FIXED, - ]} - toStr={(v: Steps): string => { - switch (v) { - case Steps.NON_FIXED: return i18n.str`Simple` - case Steps.FIXED_PRICE: return i18n.str`With price` - case Steps.FIXED_SUMMARY: return i18n.str`With summary` - case Steps.BOTH_FIXED: return i18n.str`With price and summary` - } - }} + <Input<Entity> + name="summary" + inputType="multiline" + label={i18n.str`Summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} /> - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? - <Input - name="template_contract.summary" - inputType="multiline" - label={i18n.str`Fixed summary`} - tooltip={i18n.str`If specified, this template will create order with the same summary`} - /> - : undefined} - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? - <InputCurrency - name="template_contract.amount" - label={i18n.str`Fixed price`} - tooltip={i18n.str`If specified, this template will create order with the same price`} - /> - : undefined} - <InputNumber - name="template_contract.minimum_age" + <InputToggle<Entity> + name="summary_editable" + label={i18n.str`Summary is editable`} + tooltip={i18n.str`Allow the user to change the summary.`} + /> + <InputCurrency<Entity> + name="amount" + label={i18n.str`Amount`} + tooltip={i18n.str`If specified, this template will create order with the same price`} + /> + <InputToggle<Entity> + name="amount_editable" + label={i18n.str`Amount is editable`} + tooltip={i18n.str`Allow the user to select the amount to pay.`} + /> + {cList.length > 1 && ( + <Fragment> + <InputToggle<Entity> + name="currency_editable" + readonly={!state.amount_editable} + label={i18n.str`Currency is editable`} + tooltip={i18n.str`Allow the user to change currency.`} + /> + <TextField name="sc" label={i18n.str`Supported currencies`}> + <i18n.Translate> + supported currencies: {cList.join(", ")} + </i18n.Translate> + </TextField> + </Fragment> + )} + <InputNumber<Entity> + name="minimum_age" label={i18n.str`Minimum age`} help="" tooltip={i18n.str`Is this contract restricted to some age?`} /> - <InputDuration - name="template_contract.pay_duration" + <InputDuration<Entity> + name="pay_duration" label={i18n.str`Payment timeout`} help="" tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} /> + {!deviceList.length ? ( + <TextField + name="otpId" + label={i18n.str`OTP device`} + tooltip={i18n.str`Use to verify transaction while offline.`} + > + <i18n.Translate>No OTP device.</i18n.Translate> + <a href="/otp-devices/new"> + <i18n.Translate>Add one first</i18n.Translate> + </a> + </TextField> + ) : ( + <InputSelector<Entity> + name="otpId" + label={i18n.str`OTP device`} + values={[ + undefined, + ...deviceList.map((e) => e.otp_device_id), + ]} + toStr={(v?: string) => { + if (!v) { + return i18n.str`No device`; + } + return deviceMap[v]; + }} + tooltip={i18n.str`Use to verify transaction in offline mode.`} + /> + )} </FormProvider> <div class="buttons is-right mt-5"> 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 index 3adca45db..6185bd2a9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,71 +19,69 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { useSessionContext } from "../../../../context/session.js"; import { - useTemplateAPI, 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 { HttpStatusCode } from "@gnu-taler/taler-util"; -export type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; +export type Entity = TalerMerchantApi.TemplatePatchDetails & WithId; interface Props { onBack?: () => void; onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; tid: string; } export default function UpdateTemplate({ tid, onConfirm, onBack, - onUnauthorized, - onNotFound, - onLoadError, }: Props): VNode { - const { updateTemplate } = useTemplateAPI(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const result = useTemplateDetails(tid); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } } return ( <Fragment> <NotificationCard notification={notif} /> <UpdatePage - template={{ ...result.data, id: tid }} + template={{...result.body, id: tid}} onBack={onBack} onUpdate={(data) => { - return updateTemplate(tid, data) + return lib.instance.updateTemplate(state.token, tid, data) .then(onConfirm) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx index 13576d94d..d91888b97 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software 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 index 983804d3e..360c9d373 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -29,13 +30,12 @@ import { } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { MerchantBackend } from "../../../../declaration.js"; -type Entity = MerchantBackend.Template.UsingTemplateDetails; +type Entity = TalerMerchantApi.TemplateContractDetails; interface Props { id: string; - template: MerchantBackend.Template.TemplateDetails; + template: TalerMerchantApi.TemplateDetails; onCreateOrder: (d: Entity) => Promise<void>; onBack?: () => void; } @@ -44,17 +44,18 @@ export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>({ - amount: template.template_contract.amount, - summary: template.template_contract.summary, + currency: template.editable_defaults?.currency ?? template.template_contract.currency, + amount: template.editable_defaults?.amount ?? template.template_contract.amount, + summary: template.editable_defaults?.summary ?? template.template_contract.summary, }); const errors: FormErrors<Entity> = { amount: - !template.template_contract.amount && !state.amount + !state.amount ? i18n.str`Amount is required` : undefined, summary: - !template.template_contract.summary && !state.summary + !state.summary ? i18n.str`Order summary is required` : undefined, }; 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 index ed1242ef5..00cb2b827 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (C) 2021-2024 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -19,31 +19,28 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { - useTemplateAPI, - useTemplateDetails, + 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 { HttpStatusCode } from "@gnu-taler/taler-util"; +import { useSessionContext } from "../../../../context/session.js"; -export type Entity = MerchantBackend.Transfers.TransferInformation; +export type Entity = TalerMerchantApi.TransferInformation; interface Props { onBack?: () => void; onOrderCreated: (id: string) => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; tid: string; } @@ -51,42 +48,52 @@ export default function TemplateUsePage({ tid, onOrderCreated, onBack, - onLoadError, - onNotFound, - onUnauthorized, }: Props): VNode { - const { createOrderFromTemplate } = useTemplateAPI(); + const { lib } = useSessionContext(); const result = useTemplateDetails(tid); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } } return ( <> <NotificationCard notification={notif} /> <UsePage - template={result.data} + template={result.body} id={tid} onBack={onBack} onCreateOrder={( - request: MerchantBackend.Template.UsingTemplateDetails, + request: TalerMerchantApi.UsingTemplateDetails, ) => { - return createOrderFromTemplate(tid, request) - .then((res) => onOrderCreated(res.data.order_id)) + + return lib.instance.useTemplateCreateOrder(tid, request) + .then((res) => { + if (res.type === "ok") { + onOrderCreated(res.body.order_id) + } else { + setNotif({ + message: i18n.str`could not create order from template`, + type: "ERROR", + }); + } + }) .catch((error) => { setNotif({ message: i18n.str`could not create order from template`, |