diff options
author | Sebastian <sebasjm@gmail.com> | 2024-04-09 10:58:23 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2024-04-09 10:58:23 -0300 |
commit | 7b5afb41336fe4557fe8872236122dea398d2cf8 (patch) | |
tree | 68f37fabe4f0d02b0ba35ffe90ee0693aaeab0f1 /packages | |
parent | 03677567034a59c9ce3a033f1d12bec9715a6aae (diff) |
fix #8638
Diffstat (limited to 'packages')
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> <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> <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>, |