taler-typescript-core

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

commit bf04461f0981fb6cbe327b0a2c360989c2aa769c
parent 23e40c74dfba6c22fb7a59ef3fa0d8901fc56981
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Wed, 15 Jul 2026 12:10:13 -0300

fix #10531

Diffstat:
Apackages/taler-merchant-webui/src/components/form/InputCustom.tsx | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-merchant-webui/src/components/modal/index.tsx | 2+-
Mpackages/taler-merchant-webui/src/hooks/preference.ts | 2++
Mpackages/taler-merchant-webui/src/paths/instance/orders/create/CreatePage.tsx | 52+++++++++++++++++++++++-----------------------------
Mpackages/taler-merchant-webui/src/paths/instance/statistics/list/index.tsx | 15++++++++++++---
Mpackages/taler-merchant-webui/src/paths/instance/templates/create/CreatePage.tsx | 212++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mpackages/taler-merchant-webui/src/paths/instance/templates/update/UpdatePage.tsx | 249++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mpackages/taler-util/src/types-taler-merchant.ts | 2+-
8 files changed, 492 insertions(+), 127 deletions(-)

diff --git a/packages/taler-merchant-webui/src/components/form/InputCustom.tsx b/packages/taler-merchant-webui/src/components/form/InputCustom.tsx @@ -0,0 +1,85 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { ComponentChildren, h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; +import { Tooltip } from "../Tooltip.js"; + +interface Props<T> extends InputProps<T> { + expand?: boolean; + side?: ComponentChildren; + children?: ComponentChildren; + focus?: boolean; +} + + +export function InputCustom<T>({ + name, + readonly, + placeholder, + tooltip, + label, + expand, + help, + children, + focus, + side, +}: Props<keyof T>): VNode { + const { error, value, onChange, required } = useField<T>(name); + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + {label} + {required && ( + <span class="has-text-danger" style={{ marginLeft: 5 }}> + * + </span> + )} + {tooltip && ( + <Tooltip text={tooltip}> + <i class="icon mdi mdi-information" /> + </Tooltip> + )} + </label> + </div> + <div class="field-body is-flex-grow-3"> + <div class="field"> + <p + class={ + expand + ? "control is-expanded has-icons-right" + : "control has-icons-right" + } + > + {children} + {help} + </p> + {error && ( + <p class="help is-danger" style={{ fontSize: 16 }}> + {error} + </p> + )} + </div> + {side} + </div> + </div> + ); +} diff --git a/packages/taler-merchant-webui/src/components/modal/index.tsx b/packages/taler-merchant-webui/src/components/modal/index.tsx @@ -123,7 +123,7 @@ export function ConfirmModal({ <Button submit focus - class={danger ? "button is-danger " : "button is-info "} + class={danger ? "button is-danger " : "button is-success "} onClick={confirm.handler} > <i18n.Translate>{confirm.label}</i18n.Translate> diff --git a/packages/taler-merchant-webui/src/hooks/preference.ts b/packages/taler-merchant-webui/src/hooks/preference.ts @@ -62,6 +62,8 @@ export enum UIElement { option_refreshableScopes, option_advanceProductTaxes, option_exraWireSubject, + option_orderChoices, + option_templateTypes, } export interface Preferences { diff --git a/packages/taler-merchant-webui/src/paths/instance/orders/create/CreatePage.tsx b/packages/taler-merchant-webui/src/paths/instance/orders/create/CreatePage.tsx @@ -594,18 +594,22 @@ export function CreatePage({ } tooltip={i18n.str`Amount to be paid by the customer`} side={ - <Tooltip - text={i18n.str`Switch to multiple payment choices`} + <FragmentPersonaFlag + point={UIElement.option_orderChoices} > - <button - class="button is-info" - type="button" - accessKey="+" - onClick={() => setEditChoices(true)} + <Tooltip + text={i18n.str`Switch to multiple payment choices`} > - <i class="icon mdi mdi-menu mdi-36px" /> - </button> - </Tooltip> + <button + class="button is-info" + type="button" + accessKey="+" + onClick={() => setEditChoices(true)} + > + <i class="icon mdi mdi-menu mdi-36px" /> + </button> + </Tooltip> + </FragmentPersonaFlag> } /> </Fragment> @@ -618,6 +622,9 @@ export function CreatePage({ tooltip={i18n.str`Final order price`} readonly={value.choices !== undefined} side={ + <FragmentPersonaFlag + point={UIElement.option_orderChoices} + > <Tooltip text={i18n.str`Switch to multiple payment choices`} > @@ -630,6 +637,7 @@ export function CreatePage({ <i class="icon mdi mdi-menu mdi-36px" /> </button> </Tooltip> + </FragmentPersonaFlag> } /> </Fragment> @@ -983,16 +991,7 @@ function DeadlineHelp({ duration }: { duration?: Duration }): VNode { ); } -function getAll(s: object): string[] { - return Object.entries(s).flatMap(([key, value]) => { - if (typeof value === "object") - return getAll(value).map((v) => `${key}.${v}`); - if (!value) return []; - return key; - }); -} - -function ChoicesListForm({ +export function ChoicesListForm({ focus, initial, onUpdate, @@ -1192,7 +1191,7 @@ function ChoicesListForm({ ); } -function RenderChoices({ +export function RenderChoices({ choices, onEdit, onRemove, @@ -1274,14 +1273,9 @@ function RenderChoices({ .join(", ")} </td> <td> - {(c.outputs ?? []) - .map((i) => { - switch (i.type) { - case OrderOutputType.Token: - return i.token_family_slug; - case OrderOutputType.TaxReceipt: - return i.donau_urls.join(", "); - } + {Object.entries(tOutput) + .map(([id, value]) => { + return `${id} (${value})`; }) .join(", ")} </td> diff --git a/packages/taler-merchant-webui/src/paths/instance/statistics/list/index.tsx b/packages/taler-merchant-webui/src/paths/instance/statistics/list/index.tsx @@ -35,6 +35,8 @@ import { } from "../../../../hooks/statistics.js"; import { OrdersChart } from "./OrdersChart.js"; import { RevenueChart } from "./RevenueChart.js"; +import { useCurrenciesContext } from "../../../../context/currency.js"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; interface Props {} export interface StatSlug { @@ -72,13 +74,13 @@ export type RelevantTimeUnit = | "year"; export default function Statistics({}: Props): VNode { - const { config } = useSessionContext(); const lastMonthAbs = AbsoluteTime.subtractDuraction( AbsoluteTime.now(), Duration.fromSpec({ months: 1 }), ); + const { i18n } = useTranslationContext(); + const { currency, list: availableCurrencies } = useCurrenciesContext(); - const availableCurrencies = Object.keys(config.currencies); // FIXME: throw meaningful error if backend is missconfigured // it should always have at least 1 currency @@ -86,12 +88,19 @@ export default function Statistics({}: Props): VNode { useState<RevenueChartFilter>({ range: StatisticBucketRange.Quarter, rangeCount: 4, - currency: availableCurrencies[0], + currency: currency?.id as string, }); const [startOrdersFromDate, setStartOrdersFromDate] = useState< AbsoluteTime | undefined >(lastMonthAbs); + if (!currency?.id) { + return ( + <section class="section is-main-section"> + <i18n.Translate>No currrency selected</i18n.Translate> + </section> + ); + } return ( <section class="section is-main-section"> <div> diff --git a/packages/taler-merchant-webui/src/paths/instance/templates/create/CreatePage.tsx b/packages/taler-merchant-webui/src/paths/instance/templates/create/CreatePage.tsx @@ -24,8 +24,12 @@ import { Amounts, Duration, HttpStatusCode, + OrderChoice, TalerError, TalerMerchantApi, + TalerProtocolDuration, + TemplateContractCommon, + TemplateContractDetails, TemplateType, TranslatedString, assertUnreachable, @@ -44,6 +48,7 @@ import { } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; +import { InputCustom } from "../../../../components/form/InputCustom.js"; import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; @@ -62,6 +67,10 @@ import { LimitedKycActionWarning, MissingBankAccountsWarning, } from "../../accounts/list/index.js"; +import { + ChoicesListForm, + RenderChoices, +} from "../../orders/create/CreatePage.js"; const TALER_SCREEN_ID = 61; @@ -77,6 +86,9 @@ type Entity = { summary_editable?: boolean; amount_editable?: boolean; currency_editable?: boolean; + type: TemplateType; + paivana_regex?: string; + paivana_choices?: OrderChoice[]; }; interface Props { @@ -85,13 +97,22 @@ interface Props { defaultSettingsDuration: Duration; } +export function isValidRegex(str: string): boolean { + try { + new RegExp(str); + return true; + } catch (e) { + return false; + } +} + export function CreatePage({ defaultSettingsDuration, onCreated, onBack, }: Props): VNode { const { i18n } = useTranslationContext(); - const { config, state: session, lib } = useSessionContext(); + const { state: session, lib } = useSessionContext(); const devices = useInstanceOtpDevices(); const { actionHandler, showError } = useNotificationContext(); @@ -100,6 +121,7 @@ export function CreatePage({ amount_editable: true, summary_editable: true, pay_duration: defaultSettingsDuration, + type: TemplateType.FIXED_ORDER, }); function updateState(up: (s: Partial<Entity>) => Partial<Entity>) { @@ -126,16 +148,33 @@ export function CreatePage({ ? undefined : i18n.str`Required` : undefined, // more summary validations? none - amount: !state.amount - ? state.amount_editable + amount: + state.type !== TemplateType.FIXED_ORDER ? undefined - : i18n.str`Required` - : !parsedPrice // more summary validations? is valid amount... - ? i18n.str`Invalid` - : Amounts.isZero(parsedPrice) + : !state.amount ? state.amount_editable ? undefined - : i18n.str`Must be greater than 0` + : i18n.str`Required` + : !parsedPrice // more summary validations? is valid amount... + ? i18n.str`Invalid` + : Amounts.isZero(parsedPrice) + ? state.amount_editable + ? undefined + : i18n.str`Must be greater than 0` + : undefined, + paivana_regex: + state.type !== TemplateType.PAIVANA + ? undefined + : !state.paivana_regex + ? i18n.str`Required` + : !isValidRegex(state.paivana_regex) + ? i18n.str`Is not a valid regular expression` + : undefined, + paivana_choices: + state.type !== TemplateType.PAIVANA + ? undefined + : !state.paivana_choices || state.paivana_choices.length < 1 + ? i18n.str`Required` : undefined, minimum_age: state.minimum_age && state.minimum_age < 0 @@ -162,23 +201,71 @@ export function CreatePage({ ? undefined : (state.amount as AmountString); const contract_summary = state.summary_editable ? undefined : state.summary; + const [editChoices, setEditChoices] = useState(false); + + function getTemplateContract(currency: string): TemplateContractDetails { + const t = state.type ?? TemplateType.FIXED_ORDER; + const templateContractCommon: TemplateContractCommon = { + minimum_age: state.minimum_age, + pay_duration: state.pay_duration + ? Duration.toTalerProtocolDuration(state.pay_duration) + : TalerProtocolDuration.forever(), + summary: contract_summary, + currency: + cList.length > 1 && state.currency_editable ? undefined : currency, + }; + + switch (t) { + case TalerMerchantApi.TemplateType.FIXED_ORDER: { + return { + template_type: TemplateType.FIXED_ORDER, + ...templateContractCommon, + amount: contract_amount, + }; + } + case TalerMerchantApi.TemplateType.PAIVANA: { + return { + template_type: TemplateType.PAIVANA, + ...templateContractCommon, + website_regex: state.paivana_regex, + choices: state.paivana_choices ?? [], + }; + } + case TalerMerchantApi.TemplateType.INVENTORY_CART: { + return { + template_type: TemplateType.INVENTORY_CART, + ...templateContractCommon, + // Inventory-cart: allow any inventory item to be selected. + // Since protocol **v25**. + // selected_all?: boolean; + + // Inventory-cart: only products in these categories are selectable. + // Since protocol **v25**. + // selected_categories?: Integer[]; + + // Inventory-cart: only these products are selectable. + // Since protocol **v25**. + // selected_products?: string[]; + + // Inventory-cart: require exactly one selection entry. + // Since protocol **v25**. + // choose_one?: boolean; + + // Inventory-cart: backend-provided payload with selectable data. + // Only present in GET /templates/$TEMPLATE_ID responses. + // Since protocol **v25**. + // inventory_payload?: InventoryPayload; + }; + } + } + } const data: undefined | TalerMerchantApi.TemplateAddDetails = !zero ? undefined : { template_id: state.id!, template_description: state.description!, - template_contract: { - template_type: TemplateType.FIXED_ORDER, - 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 - : zero.currency, - }, + template_contract: getTemplateContract(zero.currency), editable_defaults: { amount: !state.amount_editable ? undefined @@ -228,6 +315,17 @@ export function CreatePage({ return ( <div> + {editChoices ? ( + <ChoicesListForm + initial={state.paivana_choices ?? []} + onUpdate={(cs) => { + setState((d) => ({ ...d, paivana_choices: cs })); + setEditChoices(false); + }} + focus + /> + ) : undefined} + <section class="section is-main-section"> <div class="columns"> <div class="column" /> @@ -239,6 +337,30 @@ export function CreatePage({ valueHandler={updateState} errors={errors} > + <FragmentPersonaFlag point={UIElement.option_templateTypes}> + <InputSelector<Entity> + name="type" + label={i18n.str`Type`} + values={Object.values(TemplateType).filter( + (t) => t !== TemplateType.INVENTORY_CART, // FIXME not yet supported + )} + toStr={(v?: TemplateType) => { + switch (v) { + case undefined: + return i18n.str`Select one`; + case TemplateType.FIXED_ORDER: + return i18n.str`Fixed order`; + case TemplateType.INVENTORY_CART: + return i18n.str`Inventory cart`; + case TemplateType.PAIVANA: + return i18n.str`Paivana`; + default: + assertUnreachable(v); + } + }} + tooltip={i18n.str`Use to verify transactions in offline mode.`} + /> + </FragmentPersonaFlag> <InputWithAddon<Entity> name="id" help={ @@ -268,16 +390,20 @@ export function CreatePage({ tooltip={i18n.str`Allow the user to change the summary.`} /> - <InputCurrency<Entity> - name="amount" - label={i18n.str`Amount`} - tooltip={i18n.str`If specified here, this template will create orders 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.`} - /> + {state.type === TemplateType.FIXED_ORDER ? ( + <InputCurrency<Entity> + name="amount" + label={i18n.str`Amount`} + tooltip={i18n.str`If specified here, this template will create orders with the same price`} + /> + ) : undefined} + {state.type === TemplateType.FIXED_ORDER ? ( + <InputToggle<Entity> + name="amount_editable" + label={i18n.str`Amount is editable`} + tooltip={i18n.str`Allow the user to select the amount to pay.`} + /> + ) : undefined} {cList.length > 1 && ( <Fragment> <InputToggle<Entity> @@ -293,6 +419,34 @@ export function CreatePage({ </TextField> </Fragment> )} + {state.type === TemplateType.PAIVANA ? ( + <Input<Entity> + name="paivana_regex" + label={i18n.str`Website regex`} + tooltip={i18n.str`Regular expression over URLs for which this template is valid.`} + /> + ) : undefined} + {state.type === TemplateType.PAIVANA ? ( + <InputCustom + name="paivana_choices" + label={i18n.str`Payment options`} + tooltip={i18n.str`Allowedd methods to pay for the contract.`} + > + <button + type="button" + class="button is-info" + onClick={() => setEditChoices(true)} + > + <i18n.Translate>Configure</i18n.Translate> + </button> + {!state.paivana_choices ? undefined : ( + <Fragment> + <RenderChoices choices={state.paivana_choices} /> + </Fragment> + )} + </InputCustom> + ) : undefined} + <FragmentPersonaFlag point={UIElement.option_ageRestriction}> <InputNumber<Entity> name="minimum_age" diff --git a/packages/taler-merchant-webui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/taler-merchant-webui/src/paths/instance/templates/update/UpdatePage.tsx @@ -24,9 +24,12 @@ import { Amounts, Duration, HttpStatusCode, + OrderChoice, TalerError, TalerMerchantApi, TalerProtocolDuration, + TemplateContractCommon, + TemplateContractDetails, TemplateType, TranslatedString, assertUnreachable, @@ -60,6 +63,13 @@ import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; import { UIElement } from "../../../../hooks/preference.js"; +import { useCurrenciesContext } from "../../../../context/currency.js"; +import { + ChoicesListForm, + RenderChoices, +} from "../../orders/create/CreatePage.js"; +import { InputCustom } from "../../../../components/form/InputCustom.js"; +import { isValidRegex } from "../create/CreatePage.js"; const TALER_SCREEN_ID = 65; @@ -74,6 +84,9 @@ type Entity = { summary_editable?: boolean; amount_editable?: boolean; currency_editable?: boolean; + type: TemplateType; + paivana_regex?: string; + paivana_choices?: OrderChoice[]; }; interface Props { @@ -100,28 +113,24 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { config, state: session, lib } = useSessionContext(); const { actionHandler, showError } = useNotificationContext(); + const { currency, list: supportedCurrencies } = useCurrenciesContext(); - if (template.template_contract.template_type !== TemplateType.FIXED_ORDER) { - return <Fragment />; - } - - const cList = Object.values(config.currencies).map((d) => d.name); - const supportedCurrencies = Object.keys(config.currencies); const currentAmount = - template.editable_defaults?.amount ?? - (template.template_contract.amount as AmountString | undefined); - - const default_currency = supportedCurrencies[0]; + template.template_contract.template_type !== TemplateType.FIXED_ORDER + ? undefined + : (template.editable_defaults?.amount ?? + (template.template_contract.amount as AmountString | undefined)); const template_currency = currentAmount ? Amounts.currencyOf(currentAmount) - : default_currency; + : undefined; const unsupportedCurrency = + template_currency === undefined || supportedCurrencies.indexOf(template_currency) === -1; const startingAmount = unsupportedCurrency - ? changeToCurrency(currentAmount, default_currency) + ? changeToCurrency(currentAmount, currency?.id) : currentAmount; const [state, setState] = useState<Partial<Entity>>({ @@ -135,11 +144,20 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { : undefined, summary: template.editable_defaults?.summary ?? template.template_contract.summary, - currency: unsupportedCurrency ? default_currency : template_currency, + currency: unsupportedCurrency ? currency?.id : template_currency, amount: startingAmount, currency_editable: !!template.editable_defaults?.currency, summary_editable: template.editable_defaults?.summary !== undefined, amount_editable: template.editable_defaults?.amount !== undefined, + type: template.template_contract.template_type, + paivana_regex: + template.template_contract.template_type === TemplateType.PAIVANA + ? template.template_contract.website_regex + : undefined, + paivana_choices: + template.template_contract.template_type === TemplateType.PAIVANA + ? template.template_contract.choices + : undefined, }); function updateState(up: (s: Partial<Entity>) => Partial<Entity>) { @@ -175,16 +193,33 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { ? undefined : i18n.str`Required` : undefined, // more summary validations? none - amount: !state.amount - ? state.amount_editable + amount: + state.type !== TemplateType.FIXED_ORDER ? undefined - : i18n.str`Required` - : !parsedPrice // more summary validations? is valid amount... - ? i18n.str`Invalid` - : Amounts.isZero(parsedPrice) + : !state.amount ? state.amount_editable ? undefined - : i18n.str`Must be greater than 0` + : i18n.str`Required` + : !parsedPrice // more summary validations? is valid amount... + ? i18n.str`Invalid` + : Amounts.isZero(parsedPrice) + ? state.amount_editable + ? undefined + : i18n.str`Must be greater than 0` + : undefined, + paivana_regex: + state.type !== TemplateType.PAIVANA + ? undefined + : !state.paivana_regex + ? i18n.str`Required` + : !isValidRegex(state.paivana_regex) + ? i18n.str`Is not a valid regular expression` + : undefined, + paivana_choices: + state.type !== TemplateType.PAIVANA + ? undefined + : !state.paivana_choices || state.paivana_choices.length === 0 + ? i18n.str`Required` : undefined, minimum_age: state.minimum_age && state.minimum_age < 0 @@ -200,32 +235,83 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { }); const zero = Amounts.stringify( - Amounts.zeroOfCurrency(state.currency ?? default_currency), + Amounts.zeroOfCurrency(state.currency ?? currency?.id ?? ""), ); 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 = { - template_type: TemplateType.FIXED_ORDER, - minimum_age: state.minimum_age!, - pay_duration: state.pay_duration - ? Duration.toTalerProtocolDuration(state.pay_duration) - : TalerProtocolDuration.forever(), - amount: contract_amount, - summary: contract_summary, - currency: - cList.length > 1 && state.currency_editable ? undefined : state.currency, - }; + const [editChoices, setEditChoices] = useState(false); + + function getTemplateContract(): TemplateContractDetails { + const t = state.type ?? TemplateType.FIXED_ORDER; + + const templateContractCommon: TemplateContractCommon = { + minimum_age: state.minimum_age, + pay_duration: state.pay_duration + ? Duration.toTalerProtocolDuration(state.pay_duration) + : TalerProtocolDuration.forever(), + summary: contract_summary, + currency: + supportedCurrencies.length > 1 && state.currency_editable + ? undefined + : state.currency, + }; + + switch (t) { + case TalerMerchantApi.TemplateType.FIXED_ORDER: { + return { + template_type: TemplateType.FIXED_ORDER, + ...templateContractCommon, + amount: contract_amount, + }; + } + case TalerMerchantApi.TemplateType.PAIVANA: { + return { + template_type: TemplateType.PAIVANA, + ...templateContractCommon, + website_regex: state.paivana_regex, + choices: state.paivana_choices ?? [], + }; + } + case TalerMerchantApi.TemplateType.INVENTORY_CART: { + return { + template_type: TemplateType.INVENTORY_CART, + ...templateContractCommon, + // Inventory-cart: allow any inventory item to be selected. + // Since protocol **v25**. + // selected_all?: boolean; + + // Inventory-cart: only products in these categories are selectable. + // Since protocol **v25**. + // selected_categories?: Integer[]; + + // Inventory-cart: only these products are selectable. + // Since protocol **v25**. + // selected_products?: string[]; + + // Inventory-cart: require exactly one selection entry. + // Since protocol **v25**. + // choose_one?: boolean; + + // Inventory-cart: backend-provided payload with selectable data. + // Only present in GET /templates/$TEMPLATE_ID responses. + // Since protocol **v25**. + // inventory_payload?: InventoryPayload; + }; + } + } + } + const data: TalerMerchantApi.TemplatePatchDetails = { template_description: state.description!, - template_contract, + template_contract: getTemplateContract(), editable_defaults: { amount: !state.amount_editable ? undefined : (state.amount ?? zero), summary: !state.summary_editable ? undefined : (state.summary ?? ""), currency: - cList.length === 1 || !state.currency_editable + supportedCurrencies.length === 1 || !state.currency_editable ? undefined : state.currency, }, @@ -254,6 +340,16 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { return ( <div> + {editChoices ? ( + <ChoicesListForm + initial={state.paivana_choices ?? []} + onUpdate={(cs) => { + setState((d) => ({ ...d, paivana_choices: cs })); + setEditChoices(false); + }} + focus + /> + ) : undefined} <section class="section"> <section class="hero is-hero-bar"> <div class="hero-body"> @@ -274,11 +370,12 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { </div> </section> <hr /> - {unsupportedCurrency ? ( + {template.template_contract.template_type === + TemplateType.FIXED_ORDER && unsupportedCurrency ? ( <NotificationCardBulma notification={{ message: i18n.str`The template configuration needs to be fixed.`, - description: i18n.str`The currency of the template is ${template_currency} and is not in the list of supported currencies.`, + description: i18n.str`The currency of the template is ${!template_currency ? "missing" : template_currency} and is not in the list of supported currencies.`, type: "WARN", }} /> @@ -316,24 +413,28 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { /> ) : undefined} - <InputWithAddon<Entity> - name="amount" - label={i18n.str`Amount`} - addonBefore={state.currency} - inputType="decimal" - toStr={(v?: AmountString) => v?.split(":")[1] || ""} - fromStr={(v: string) => - !v ? undefined : `${state.currency}:${v}` - } - inputExtra={{ min: 0, step: 0.001 }} - tooltip={i18n.str`If specified, this template will create orders 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 && ( + {state.type === TemplateType.FIXED_ORDER ? ( + <InputWithAddon<Entity> + name="amount" + label={i18n.str`Amount`} + addonBefore={state.currency} + inputType="decimal" + toStr={(v?: AmountString) => v?.split(":")[1] || ""} + fromStr={(v: string) => + !v ? undefined : `${state.currency}:${v}` + } + inputExtra={{ min: 0, step: 0.001 }} + tooltip={i18n.str`If specified, this template will create orders with the same price`} + /> + ) : undefined} + {state.type === TemplateType.FIXED_ORDER ? ( + <InputToggle<Entity> + name="amount_editable" + label={i18n.str`Amount is editable`} + tooltip={i18n.str`Allow the user to select the amount to pay.`} + /> + ) : undefined} + {supportedCurrencies.length > 1 && ( <Fragment> <InputToggle<Entity> name="currency_editable" @@ -343,6 +444,34 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { /> </Fragment> )} + {state.type === TemplateType.PAIVANA ? ( + <Input<Entity> + name="paivana_regex" + label={i18n.str`Website regex`} + tooltip={i18n.str`Regular expression over URLs for which this template is valid.`} + /> + ) : undefined} + {state.type === TemplateType.PAIVANA ? ( + <InputCustom + name="paivana_choices" + label={i18n.str`Payment options`} + tooltip={i18n.str`Allowedd methods to pay for the contract.`} + > + <button + type="button" + class="button is-info" + onClick={() => setEditChoices(true)} + > + <i18n.Translate>Configure</i18n.Translate> + </button> + {!state.paivana_choices ? undefined : ( + <Fragment> + <RenderChoices choices={state.paivana_choices} /> + </Fragment> + )} + </InputCustom> + ) : undefined} + <FragmentPersonaFlag point={UIElement.option_ageRestriction}> <InputNumber<Entity> name="minimum_age" @@ -418,21 +547,13 @@ function UpdateFixedOrderPage({ template, onUpdated, onBack }: Props): VNode { </div> ); } -function UpdatePaivanaPage({ template, onUpdated, onBack }: Props): VNode { - return <div>unsupported paivana template</div>; -} -function UpdateInventoryPage({ template, onUpdated, onBack }: Props): VNode { - return <div>unsupported inventory template</div>; -} export function UpdatePage(props: Props): VNode { switch (props.template.template_contract.template_type) { + case TalerMerchantApi.TemplateType.PAIVANA: case TalerMerchantApi.TemplateType.FIXED_ORDER: - return UpdateFixedOrderPage(props); case TalerMerchantApi.TemplateType.INVENTORY_CART: - return UpdateInventoryPage(props); - case TalerMerchantApi.TemplateType.PAIVANA: - return UpdatePaivanaPage(props); + return UpdateFixedOrderPage(props); default: assertUnreachable(props.template.template_contract); } diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -3418,7 +3418,7 @@ export interface TemplateContractCommon { // Template type to apply. Defaults to "fixed-order" if omitted. // Prescribes which interface has to be followed // Since protocol **v25**. - template_type: TemplateType; + // template_type: TemplateType; // Human-readable summary for the template. summary?: string;