summaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx')
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx305
1 files changed, 305 insertions, 0 deletions
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
new file mode 100644
index 000000000..eedb77f28
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -0,0 +1,305 @@
+/*
+ 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 {
+ AmountString,
+ Amounts,
+ Duration,
+ TalerError,
+ TalerMerchantApi,
+ TranslatedString,
+} from "@gnu-taler/taler-util";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { InputCurrency } from "../../../../components/form/InputCurrency.js";
+import { InputDuration } from "../../../../components/form/InputDuration.js";
+import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
+import { TextField } from "../../../../components/form/TextField.js";
+import { useSessionContext } from "../../../../context/session.js";
+import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
+
+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: TalerMerchantApi.TemplatePatchDetails) => Promise<void>;
+ onBack?: () => void;
+ template: TalerMerchantApi.TemplateDetails & WithId;
+}
+
+export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+ 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,
+ });
+
+ 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 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.amount ? undefined : Amounts.parse(state.amount);
+
+ const errors: FormErrors<Entity> = {
+ description: !state.description ? i18n.str`should not be empty` : undefined,
+ amount: !state.amount
+ ? undefined
+ : !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 Record<string, unknown>)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ 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">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ {new URL(`templates/${template.id}`, session.backendUrl.href).href}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={updateState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="description"
+ label={i18n.str`Description`}
+ help=""
+ tooltip={i18n.str`Describe what this template stands for`}
+ />
+ <Input<Entity>
+ name="summary"
+ inputType="multiline"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`If specified, this template will create order with the same summary`}
+ />
+ <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<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>&nbsp;
+ <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">
+ {onBack && (
+ <button class="button" onClick={onBack}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ )}
+ <AsyncButton
+ disabled={hasErrors}
+ data-tooltip={
+ hasErrors
+ ? i18n.str`Need to complete marked fields`
+ : "confirm operation"
+ }
+ onClick={submitForm}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}