gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: fix #8638


From: gnunet
Subject: [taler-wallet-core] branch master updated: fix #8638
Date: Tue, 09 Apr 2024 15:58:30 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 7b5afb413 fix #8638
7b5afb413 is described below

commit 7b5afb41336fe4557fe8872236122dea398d2cf8
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Apr 9 10:58:23 2024 -0300

    fix #8638
---
 .../src/components/form/InputToggle.tsx            |   2 +-
 .../paths/instance/templates/create/CreatePage.tsx | 249 ++++++++-------------
 .../src/paths/instance/templates/qr/QrPage.tsx     |  77 +++----
 .../paths/instance/templates/update/UpdatePage.tsx | 234 ++++++++-----------
 .../src/paths/instance/templates/use/UsePage.tsx   |  11 +-
 .../merchant-backoffice-ui/src/scss/toggle.scss    |  20 +-
 packages/taler-util/src/http-client/types.ts       |  96 ++++++++
 packages/web-util/src/hooks/index.ts               |   2 +-
 packages/web-util/src/hooks/useLocalStorage.ts     |   7 +-
 9 files changed, 343 insertions(+), 355 deletions(-)

diff --git 
a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx 
b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
index 89b815b4b..8c935f33b 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
@@ -79,7 +79,7 @@ export function InputToggle<T>({
                 disabled={readonly}
                 onChange={onCheckboxClick}
               />
-              <div class="toggle-switch"></div>
+              <div class={`toggle-switch ${readonly ? "disabled" : ""}`} 
style={{ cursor: readonly ? "default" : undefined }}></div>
             </label>
             {help}
           </p>
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 2ba637f44..ad36df3cc 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
@@ -25,7 +25,7 @@ import {
   Duration,
   TalerError,
   TalerMerchantApi,
-  assertUnreachable,
+  TranslatedString
 } from "@gnu-taler/taler-util";
 import {
   useMerchantApiContext,
@@ -42,18 +42,12 @@ 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 { InputTab } from "../../../../components/form/InputTab.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
 import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
+import { TextField } from "../../../../components/form/TextField.js";
 import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
 
-enum Steps {
-  BOTH_FIXED,
-  FIXED_PRICE,
-  FIXED_SUMMARY,
-  NON_FIXED,
-}
-
 // type Entity = TalerMerchantApi.TemplateAddDetails & { type: Steps };
 type Entity = {
   id?: string;
@@ -63,7 +57,9 @@ type Entity = {
   amount?: AmountString;
   minimum_age?: number;
   pay_duration?: Duration;
-  type: Steps;
+  summary_editable?: boolean;
+  amount_editable?: boolean;
+  currency_editable?: boolean;
 };
 
 interface Props {
@@ -81,9 +77,18 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
     pay_duration: {
       d_ms: 1000 * 60 * 30, //30 min
     },
-    type: Steps.NON_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 parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount);
 
   const errors: FormErrors<Entity> = {
@@ -93,24 +98,14 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
         ? i18n.str`no valid. only characters and numbers`
         : undefined,
     description: !state.description ? i18n.str`should not be empty` : 
undefined,
-    amount: !(
-      state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED
-    )
-      ? undefined
-      : !state.amount
-        ? i18n.str`required`
+    amount:
+      !state.amount
+        ? undefined
         : !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.summary
-        ? i18n.str`required`
-        : undefined,
     minimum_age:
       state.minimum_age && state.minimum_age < 0
         ? i18n.str`should be greater that 0`
@@ -125,67 +120,33 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
   };
 
   const hasErrors = Object.keys(errors).some(
-    (k) => (errors as Record<string,unknown>)[k] !== undefined,
+    (k) => (errors as Record<string, unknown>)[k] !== undefined,
   );
 
   const submitForm = () => {
-    if (hasErrors || state.type === undefined) return Promise.reject();
-    switch (state.type) {
-      case Steps.FIXED_PRICE:
-        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!,
-            // summary: state.summary,
-          },
-          otp_id: state.otpId!,
-        });
-      case Steps.FIXED_SUMMARY:
-        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!,
-            summary: state.summary,
-          },
-          otp_id: state.otpId!,
-        });
-      case Steps.NON_FIXED:
-        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!,
-            // summary: state.summary,
-          },
-          otp_id: state.otpId!,
-        });
-      case Steps.BOTH_FIXED:
-        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!,
-            summary: state.summary,
-          },
-          otp_id: state.otpId!,
-        });
-      default:
-        assertUnreachable(state.type);
-      // return onCreate(state);
-    }
+    if (hasErrors) return Promise.reject();
+    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,
+      },
+      editable_defaults: {
+        amount: !state.amount_editable ? undefined : state.amount,
+        summary: !state.summary_editable ? undefined : state.summary,
+      },
+      otp_id: state.otpId!,
+    });
+
   };
   const deviceList = !devices || devices instanceof TalerError || devices.type 
=== "fail" ? [] : devices.body;
-
+  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">
@@ -194,7 +155,7 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
           <div class="column is-four-fifths">
             <FormProvider
               object={state}
-              valueHandler={setState}
+              valueHandler={updateState}
               errors={errors}
             >
               <InputWithAddon<Entity>
@@ -209,59 +170,36 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
                 help=""
                 tooltip={i18n.str`Describe what this template stands for`}
               />
-              <InputTab<Entity>
-                name="type"
-                label={i18n.str`Type`}
-                help={(() => {
-                  if (state.type === undefined) return "";
-                  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<Entity>
-                  name="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<Entity>
-                  name="amount"
-                  label={i18n.str`Fixed price`}
-                  tooltip={i18n.str`If specified, this template will create 
order with the same price`}
-                />
-              ) : undefined}
+              <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.`}
+              />
+              {/* <InputToggle<Entity>
+                name="currency_editable"
+                readonly={!state.amount_editable}
+                label={i18n.str`Currency is editable`}
+                tooltip={i18n.str`Allow the user to change currency.`}
+              /> */}
+
               <InputNumber<Entity>
                 name="minimum_age"
                 label={i18n.str`Minimum age`}
@@ -274,33 +212,26 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
                 help=""
                 tooltip={i18n.str`How much time has the customer to complete 
the payment once the order was created.`}
               />
-              <Input<Entity>
+              {!deviceList.length ? <TextField
                 name="otpId"
                 label={i18n.str`OTP device`}
-                readonly
-                side={
-                  <button
-                    class="button is-danger"
-                    data-tooltip={i18n.str`without otp device`}
-                    onClick={(): void => {
-                      setState((v) => ({ ...v, otpId: undefined }));
-                    }}
-                  >
-                    <span>
-                      <i18n.Translate>remove</i18n.Translate>
-                    </span>
-                  </button>
-                }
-                tooltip={i18n.str`Use to verify transaction in offline mode.`}
-              />
-              <InputSearchOnList
-                label={i18n.str`Search device`}
-                onChange={(p) => setState((v) => ({ ...v, otpId: p?.id }))}
-                list={deviceList.map((e) => ({
-                  description: e.device_description,
-                  id: e.otp_device_id,
-                }))}
-              />
+                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">
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 0749f45d3..cd6b8b45c 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
@@ -46,79 +46,78 @@ export function QrPage({ contract, id: templateId, onBack 
}: Props): VNode {
   const { i18n } = useTranslationContext();
   const { config, url: backendUrl } = useMerchantApiContext();
 
-  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 = backendUrl.href;
 
   const payTemplateUri = stringifyPayTemplateUri({
     merchantBaseUrl,
     templateId,
-    templateParams,
+    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 && (
@@ -137,12 +136,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>
   );
 }
@@ -160,6 +153,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/update/UpdatePage.tsx
 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
index b99549825..f9e2a3b01 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
@@ -25,7 +25,7 @@ import {
   Duration,
   TalerError,
   TalerMerchantApi,
-  assertUnreachable
+  TranslatedString
 } from "@gnu-taler/taler-util";
 import { useMerchantApiContext, useTranslationContext } from 
"@gnu-taler/web-util/browser";
 import { VNode, h } from "preact";
@@ -39,17 +39,11 @@ 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 { InputTab } from "../../../../components/form/InputTab.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { InputToggle } from "../../../../components/form/InputToggle.js";
+import { TextField } from "../../../../components/form/TextField.js";
 import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
 
-enum Steps {
-  BOTH_FIXED,
-  FIXED_PRICE,
-  FIXED_SUMMARY,
-  NON_FIXED,
-}
-
 type Entity = {
   description?: string,
   otpId?: string | null,
@@ -57,6 +51,9 @@ type Entity = {
   amount?: AmountString,
   minimum_age?: number,
   pay_duration?: Duration,
+  summary_editable?: boolean;
+  amount_editable?: boolean;
+  currency_editable?: boolean;
 };
 
 interface Props {
@@ -70,26 +67,35 @@ export function UpdatePage({ template, onUpdate, onBack }: 
Props): VNode {
   const { url: backendUrl } = useMerchantApiContext();
 
 
-  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;
 
-  const [state, setState] = useState<Partial<Entity & { type: Steps }>>({
-    amount: template.template_contract.amount as AmountString | undefined,
+  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.template_contract.summary,
-    type: intialStep,
+    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
+  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
@@ -99,20 +105,14 @@ export function UpdatePage({ template, onUpdate, onBack }: 
Props): VNode {
     description: !state.description
       ? i18n.str`should not be empty`
       : undefined,
-    amount: !(state.type === Steps.FIXED_PRICE || state.type === 
Steps.BOTH_FIXED)
-      ? undefined
-      : !state.amount
-        ? i18n.str`required`
+    amount:
+      !state.amount
+        ? undefined
         : !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.summary
-        ? i18n.str`required`
-        : undefined,
     minimum_age:
       state.minimum_age && state.minimum_age < 0
         ? i18n.str`should be greater that 0`
@@ -127,54 +127,26 @@ export function UpdatePage({ template, onUpdate, onBack 
}: Props): VNode {
   };
 
   const hasErrors = Object.keys(errors).some(
-    (k) => (errors as Record<string,unknown>)[k] !== undefined,
+    (k) => (errors as Record<string, unknown>)[k] !== undefined,
   );
 
   const submitForm = () => {
-    if (hasErrors || state.type === undefined) return Promise.reject();
-    switch (state.type) {
-      case Steps.FIXED_PRICE: return onUpdate({
-        template_description: state.description!,
-        template_contract: {
-          minimum_age: state.minimum_age!,
-          pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
-          amount: state.amount!,
-          // summary: state.summary,
-        },
-        otp_id: state.otpId!
-      })
-      case Steps.FIXED_SUMMARY: return onUpdate({
-        template_description: state.description!,
-        template_contract: {
-          minimum_age: state.minimum_age!,
-          pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
-          // amount: state.amount!,
-          summary: state.summary,
-        },
-        otp_id: state.otpId!,
-      })
-      case Steps.NON_FIXED: return onUpdate({
-        template_description: state.description!,
-        template_contract: {
-          minimum_age: state.minimum_age!,
-          pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
-          // amount: state.amount!,
-          // summary: state.summary,
-        },
-        otp_id: state.otpId!,
-      })
-      case Steps.BOTH_FIXED: return onUpdate({
-        template_description: state.description!,
-        template_contract: {
-          minimum_age: state.minimum_age!,
-          pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
-          amount: state.amount!,
-          summary: state.summary,
-        },
-        otp_id: state.otpId!,
-      })
-      default: assertUnreachable(state.type)
-    }
+    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,
+      },
+      editable_defaults: {
+        amount: !state.amount_editable ? undefined : state.amount,
+        summary: !state.summary_editable ? undefined : state.summary,
+      },
+      otp_id: state.otpId!,
+    })
+
   };
 
 
@@ -187,7 +159,7 @@ export function UpdatePage({ template, onUpdate, onBack }: 
Props): VNode {
               <div class="level-left">
                 <div class="level-item">
                   <span class="is-size-4">
-                    {new URL(`templates/${template.id}`,backendUrl.href).href}
+                    {new URL(`templates/${template.id}`, backendUrl.href).href}
                   </span>
                 </div>
               </div>
@@ -201,7 +173,7 @@ export function UpdatePage({ template, onUpdate, onBack }: 
Props): VNode {
             <div class="column is-four-fifths">
               <FormProvider
                 object={state}
-                valueHandler={setState}
+                valueHandler={updateState}
                 errors={errors}
               >
 
@@ -211,48 +183,33 @@ export function UpdatePage({ template, onUpdate, onBack 
}: Props): VNode {
                   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<Entity>
-                    name="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<Entity>
-                    name="amount"
-                    label={i18n.str`Fixed price`}
-                    tooltip={i18n.str`If specified, this template will create 
order with the same price`}
-                  />
-                  : undefined}
+                <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.`}
+                />
+                {/* <InputToggle<Entity>
+                  name="currency_editable"
+                  readonly={!state.amount_editable}
+                  label={i18n.str`Currency is editable`}
+                  tooltip={i18n.str`Allow the user to change currency.`}
+                /> */}
                 <InputNumber<Entity>
                   name="minimum_age"
                   label={i18n.str`Minimum age`}
@@ -265,31 +222,26 @@ export function UpdatePage({ template, onUpdate, onBack 
}: Props): VNode {
                   help=""
                   tooltip={i18n.str`How much time has the customer to complete 
the payment once the order was created.`}
                 />
-                <Input<Entity>
+                {!deviceList.length ? <TextField
                   name="otpId"
                   label={i18n.str`OTP device`}
-                  readonly
-                  side={<button
-                    class="button is-danger"
-                    data-tooltip={i18n.str`remove otp device for this 
template`}
-                    onClick={(): void => {
-                      setState((v) => ({ ...v, otpId: null }));
+                  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]
                     }}
-                  >
-                    <span>
-                      <i18n.Translate>remove</i18n.Translate>
-                    </span>
-                  </button>}
-                  tooltip={i18n.str`Use to verify transaction in offline 
mode.`}
-                />
-                <InputSearchOnList
-                  label={i18n.str`Search device`}
-                  onChange={(p) => setState((v) => ({ ...v, otpId: p?.id }))}
-                  list={deviceList.map(e => ({
-                    description: e.device_description,
-                    id: e.otp_device_id
-                  }))}
-                />
+                    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/use/UsePage.tsx 
b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx
index 58e63cc8e..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
@@ -31,7 +31,7 @@ import {
 import { Input } from "../../../../components/form/Input.js";
 import { InputCurrency } from "../../../../components/form/InputCurrency.js";
 
-type Entity = TalerMerchantApi.UsingTemplateDetails;
+type Entity = TalerMerchantApi.TemplateContractDetails;
 
 interface Props {
   id: string;
@@ -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/scss/toggle.scss 
b/packages/merchant-backoffice-ui/src/scss/toggle.scss
index 24636da2f..6c7346eb3 100644
--- a/packages/merchant-backoffice-ui/src/scss/toggle.scss
+++ b/packages/merchant-backoffice-ui/src/scss/toggle.scss
@@ -4,6 +4,7 @@ $green: #56c080;
   cursor: pointer;
   display: inline-block;
 }
+
 .toggle-switch {
   display: inline-block;
   background: #ccc;
@@ -13,10 +14,12 @@ $green: #56c080;
   position: relative;
   vertical-align: middle;
   transition: background 0.25s;
+
   &:before,
   &:after {
     content: "";
   }
+
   &:before {
     display: block;
     background: linear-gradient(to bottom, #fff 0%, #eee 100%);
@@ -29,23 +32,36 @@ $green: #56c080;
     left: 4px;
     transition: left 0.25s;
   }
+
   .toggle:hover &:before {
     background: linear-gradient(to bottom, #fff 0%, #fff 100%);
     box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
   }
-  .toggle-checkbox:checked + & {
+
+  &.disabled:before {
+    background: linear-gradient(to bottom, #ccc 0%, #bbb 100%);
+  }
+
+  .toggle:hover &.disabled:before {
+    background: linear-gradient(to bottom, #ccc 0%, #bbb 100%);
+  }
+
+  .toggle-checkbox:checked+& {
     background: $green;
+
     &:before {
       left: 30px;
     }
   }
 }
+
 .toggle-checkbox {
   position: absolute;
   visibility: hidden;
 }
+
 .toggle-label {
   margin-left: 5px;
   position: relative;
   top: 2px;
-}
+}
\ No newline at end of file
diff --git a/packages/taler-util/src/http-client/types.ts 
b/packages/taler-util/src/http-client/types.ts
index 72189cf0a..c843e075a 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -824,6 +824,8 @@ export const codecForTemplateDetails =
       .property("template_description", codecForString())
       .property("otp_id", codecOptional(codecForString()))
       .property("template_contract", codecForTemplateContractDetails())
+      .property("required_currency", codecOptional(codecForString()))
+      .property("editable_defaults", 
codecOptional(codecForTemplateContractDetailsDefaults()))
       .build("TalerMerchantApi.TemplateDetails");
 
 export const codecForTemplateContractDetails =
@@ -836,10 +838,22 @@ export const codecForTemplateContractDetails =
       .property("pay_duration", codecForDuration)
       .build("TalerMerchantApi.TemplateContractDetails");
 
+export const codecForTemplateContractDetailsDefaults =
+  (): Codec<TalerMerchantApi.TemplateContractDetailsDefaults> =>
+    buildCodecForObject<TalerMerchantApi.TemplateContractDetailsDefaults>()
+      .property("summary", codecOptional(codecForString()))
+      .property("currency", codecOptional(codecForString()))
+      .property("amount", codecOptional(codecForAmountString()))
+      .property("minimum_age", codecOptional(codecForNumber()))
+      .property("pay_duration", codecOptional(codecForDuration))
+      .build("TalerMerchantApi.TemplateContractDetailsDefaults");
+
 export const codecForWalletTemplateDetails =
   (): Codec<TalerMerchantApi.WalletTemplateDetails> =>
     buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>()
       .property("template_contract", codecForTemplateContractDetails())
+      .property("required_currency", codecOptional(codecForString()))
+      .property("editable_defaults", 
codecOptional(codecForTemplateContractDetailsDefaults()))
       .build("TalerMerchantApi.WalletTemplateDetails");
 
 export const codecForWebhookSummaryResponse =
@@ -4605,6 +4619,24 @@ export namespace TalerMerchantApi {
 
     // Additional information in a separate template.
     template_contract: TemplateContractDetails;
+
+    // Key-value pairs matching a subset of the
+    // fields from template_contract that are
+    // user-editable defaults for this template.
+    // Since protocol **v13**.
+    editable_defaults?: TemplateContractDetailsDefaults;
+
+    // Required currency for payments.  Useful if no
+    // amount is specified in the template_contract
+    // but the user should be required to pay in a
+    // particular currency anyway.  Merchant backends
+    // may reject requests if the template_contract
+    // or editable_defaults do
+    // specify an amount in a different currency.
+    // This parameter is optional.
+    // Since protocol **v13**.
+    required_currency?: string;
+
   }
   export interface TemplateContractDetails {
     // Human-readable summary for the template.
@@ -4628,6 +4660,18 @@ export namespace TalerMerchantApi {
     // It is deleted if the customer did not pay and if the duration is over.
     pay_duration: RelativeTime;
   }
+
+  export interface TemplateContractDetailsDefaults {
+    summary?: string;
+
+    currency?: string;
+
+    amount?: AmountString;
+
+    minimum_age?: Integer;
+
+    pay_duration?: RelativeTime;
+  }
   export interface TemplatePatchDetails {
     // Human-readable description for the template.
     template_description: string;
@@ -4638,6 +4682,24 @@ export namespace TalerMerchantApi {
 
     // Additional information in a separate template.
     template_contract: TemplateContractDetails;
+
+    // Key-value pairs matching a subset of the
+    // fields from template_contract that are
+    // user-editable defaults for this template.
+    // Since protocol **v13**.
+    editable_defaults?: TemplateContractDetailsDefaults;
+
+    // Required currency for payments.  Useful if no
+    // amount is specified in the template_contract
+    // but the user should be required to pay in a
+    // particular currency anyway.  Merchant backends
+    // may reject requests if the template_contract
+    // or editable_defaults do
+    // specify an amount in a different currency.
+    // This parameter is optional.
+    // Since protocol **v13**.
+    required_currency?: string;
+
   }
 
   export interface TemplateSummaryResponse {
@@ -4657,6 +4719,23 @@ export namespace TalerMerchantApi {
     // Hard-coded information about the contrac terms
     // for this template.
     template_contract: TemplateContractDetails;
+
+    // Key-value pairs matching a subset of the
+    // fields from template_contract that are
+    // user-editable defaults for this template.
+    // Since protocol **v13**.
+    editable_defaults?: TemplateContractDetailsDefaults;
+
+    // Required currency for payments.  Useful if no
+    // amount is specified in the template_contract
+    // but the user should be required to pay in a
+    // particular currency anyway.  Merchant backends
+    // may reject requests if the template_contract
+    // or editable_defaults do
+    // specify an amount in a different currency.
+    // This parameter is optional.
+    // Since protocol **v13**.
+    required_currency?: string;
   }
 
   export interface TemplateDetails {
@@ -4669,6 +4748,23 @@ export namespace TalerMerchantApi {
 
     // Additional information in a separate template.
     template_contract: TemplateContractDetails;
+
+    // Key-value pairs matching a subset of the
+    // fields from template_contract that are
+    // user-editable defaults for this template.
+    // Since protocol **v13**.
+    editable_defaults?: TemplateContractDetailsDefaults;
+
+    // Required currency for payments.  Useful if no
+    // amount is specified in the template_contract
+    // but the user should be required to pay in a
+    // particular currency anyway.  Merchant backends
+    // may reject requests if the template_contract
+    // or editable_defaults do
+    // specify an amount in a different currency.
+    // This parameter is optional.
+    // Since protocol **v13**.
+    required_currency?: string;
   }
   export interface UsingTemplateDetails {
     // Summary of the template
diff --git a/packages/web-util/src/hooks/index.ts 
b/packages/web-util/src/hooks/index.ts
index f6c74ff22..ba1b6e222 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,5 +1,5 @@
 export { useLang } from "./useLang.js";
-export { useLocalStorage, buildStorageKey } from "./useLocalStorage.js";
+export { useLocalStorage, buildStorageKey, StorageKey, StorageState } from 
"./useLocalStorage.js";
 export { useMemoryStorage } from "./useMemoryStorage.js";
 export * from "./useNotifications.js";
 export {
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts 
b/packages/web-util/src/hooks/useLocalStorage.ts
index 7c41f98be..abd80bacc 100644
--- a/packages/web-util/src/hooks/useLocalStorage.ts
+++ b/packages/web-util/src/hooks/useLocalStorage.ts
@@ -61,9 +61,9 @@ const supportLocalStorage = typeof window !== "undefined";
 const supportBrowserStorage =
   typeof chrome !== "undefined" && typeof chrome.storage !== "undefined";
 
-  /**
-   * Build setting storage
-   */
+/**
+ * Build setting storage
+ */
 const storage: ObservableMap<string, string> = (function buildStorage() {
   if (supportBrowserStorage) {
     //browser storage is like local storage but
@@ -83,7 +83,6 @@ const storage: ObservableMap<string, string> = (function 
buildStorage() {
     return memoryMap<string>();
   }
 })();
-
 //with initial value
 export function useLocalStorage<Type = string>(
   key: StorageKey<Type>,

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]