aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-04-09 10:58:23 -0300
committerSebastian <sebasjm@gmail.com>2024-04-09 10:58:23 -0300
commit7b5afb41336fe4557fe8872236122dea398d2cf8 (patch)
tree68f37fabe4f0d02b0ba35ffe90ee0693aaeab0f1 /packages
parent03677567034a59c9ce3a033f1d12bec9715a6aae (diff)
fix #8638
Diffstat (limited to 'packages')
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx249
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx77
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx234
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx11
-rw-r--r--packages/merchant-backoffice-ui/src/scss/toggle.scss20
-rw-r--r--packages/taler-util/src/http-client/types.ts96
-rw-r--r--packages/web-util/src/hooks/index.ts2
-rw-r--r--packages/web-util/src/hooks/useLocalStorage.ts7
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>,