diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx')
-rw-r--r-- | packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx | 309 |
1 files changed, 138 insertions, 171 deletions
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 31e525226..50262be17 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 @@ -23,13 +23,14 @@ import { AmountString, Amounts, Duration, + TalerError, TalerMerchantApi, - assertUnreachable, + TranslatedString, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -40,19 +41,13 @@ 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 { useSessionContext } from "../../../../context/session.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; @@ -62,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 { @@ -72,9 +69,8 @@ interface Props { export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { - state: { backendUrl }, - } = useSessionContext(); + const { config } = useSessionContext(); + const { state: session } = useSessionContext(); const devices = useInstanceOtpDevices(); const [state, setState] = useState<Partial<Entity>>({ @@ -82,9 +78,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> = { @@ -94,24 +99,13 @@ 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` - : !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, + amount: !state.amount + ? state.amount_editable ? undefined : i18n.str`required` + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? state.amount_editable ? undefined : i18n.str`must be greater than 0` + : undefined, minimum_age: state.minimum_age && state.minimum_age < 0 ? i18n.str`should be greater that 0` @@ -125,68 +119,55 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { : undefined, }; + const cList = Object.values(config.currencies).map((d) => d.name); + const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); + const zero = Amounts.stringify(Amounts.zeroOfCurrency(config.currency)) + 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(); + const contract_amount = state.amount_editable ? undefined : state.amount as AmountString + const contract_summary = state.summary_editable ? undefined : state.summary + const template_contract: TalerMerchantApi.TemplateContractDetails = { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: contract_amount, + summary: contract_summary, + currency: + cList.length > 1 && state.currency_editable + ? undefined + : config.currency, } + return onCreate({ + template_id: state.id!, + template_description: state.description!, + template_contract, + required_currency: contract_amount !== undefined ? undefined : config.currency, + editable_defaults: { + amount: !state.amount_editable ? undefined : (state.amount ?? zero), + summary: !state.summary_editable ? undefined : (state.summary ?? ""), + currency: + cList.length === 1 || !state.currency_editable + ? undefined + : config.currency, + }, + otp_id: state.otpId!, + }); }; - const deviceList = !devices.ok ? [] : devices.data.otp_devices; - + const deviceList = + !devices || devices instanceof TalerError || devices.type === "fail" + ? [] + : devices.body.otp_devices; + const deviceMap = deviceList.reduce( + (prev, cur) => { + prev[cur.otp_device_id] = cur.device_description as TranslatedString; + return prev; + }, + {} as Record<string, TranslatedString>, + ); return ( <div> <section class="section is-main-section"> @@ -195,12 +176,14 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <div class="column is-four-fifths"> <FormProvider object={state} - valueHandler={setState} + valueHandler={updateState} errors={errors} > <InputWithAddon<Entity> name="id" - help={new URL(`templates/${state.id ?? ""}`, backendUrl).href} + help={ + new URL(`templates/${state.id ?? ""}`, session.backendUrl.href).href + } label={i18n.str`Identifier`} tooltip={i18n.str`Name of the template in URLs.`} /> @@ -210,59 +193,42 @@ 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`} + /> + <InputToggle<Entity> + name="summary_editable" + label={i18n.str`Summary is editable`} + tooltip={i18n.str`Allow the user to change the 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} + + <InputCurrency<Entity> + name="amount" + label={i18n.str`Amount`} + tooltip={i18n.str`If specified, this template will create order with the same price`} + /> + <InputToggle<Entity> + name="amount_editable" + label={i18n.str`Amount is editable`} + tooltip={i18n.str`Allow the user to select the amount to pay.`} + /> + {cList.length > 1 && ( + <Fragment> + <InputToggle<Entity> + name="currency_editable" + readonly={!state.amount_editable} + label={i18n.str`Currency is editable`} + tooltip={i18n.str`Allow the user to change currency.`} + /> + <TextField name="sc" label={i18n.str`Supported currencies`}> + <i18n.Translate>supported currencies: {cList.join(", ")}</i18n.Translate> + </TextField> + </Fragment> + )} <InputNumber<Entity> name="minimum_age" label={i18n.str`Minimum age`} @@ -275,33 +241,34 @@ 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> - 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, - }))} - /> + {!deviceList.length ? ( + <TextField + name="otpId" + label={i18n.str`OTP device`} + tooltip={i18n.str`Use to verify transaction while offline.`} + > + <i18n.Translate>No OTP device.</i18n.Translate> + <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"> |