commit bf04461f0981fb6cbe327b0a2c360989c2aa769c
parent 23e40c74dfba6c22fb7a59ef3fa0d8901fc56981
Author: Sebastian <sebasjm@taler-systems.com>
Date: Wed, 15 Jul 2026 12:10:13 -0300
fix #10531
Diffstat:
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;