diff options
Diffstat (limited to 'packages/merchant-backoffice-ui')
4 files changed, 315 insertions, 154 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx new file mode 100644 index 000000000..61ddf3c84 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx @@ -0,0 +1,91 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import { InputProps, useField } from "./useField.js"; + +interface Props<T> extends InputProps<T> { + name: T; + readonly?: boolean; + expand?: boolean; + threeState?: boolean; + toBoolean?: (v?: any) => boolean | undefined; + fromBoolean?: (s: boolean | undefined) => any; +} + +const defaultToBoolean = (f?: any): boolean | undefined => f || ""; +const defaultFromBoolean = (v: boolean | undefined): any => v as any; + +export function InputToggle<T>({ + name, + readonly, + placeholder, + tooltip, + label, + help, + threeState, + expand, + fromBoolean = defaultFromBoolean, + toBoolean = defaultToBoolean, +}: Props<keyof T>): VNode { + const { error, value, onChange } = useField<T>(name); + + const onCheckboxClick = (): void => { + const c = toBoolean(value); + if (c === false && threeState) return onChange(undefined as any); + return onChange(fromBoolean(!c)); + }; + + return ( + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label" style={{ width: 200 }}> + {label} + {tooltip && ( + <span class="icon has-tooltip-right" data-tooltip={tooltip}> + <i class="mdi mdi-information" /> + </span> + )} + </label> + </div> + <div class="field-body is-flex-grow-1"> + <div class="field"> + <p class={expand ? "control is-expanded" : "control"}> + <label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}> + <input + type="checkbox" + class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"} + checked={toBoolean(value)} + placeholder={placeholder} + readonly={readonly} + name={String(name)} + disabled={readonly} + onChange={onCheckboxClick} + /> + <div class="toggle-switch"></div> + </label> + {help} + </p> + {error && <p class="help is-danger">{error}</p>} + </div> + </div> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts index 10e77716e..b77b9dea8 100644 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ b/packages/merchant-backoffice-ui/src/hooks/index.ts @@ -53,15 +53,17 @@ export function useBackendURL( export function useBackendDefaultToken( initialValue?: string, -): [string | undefined, ((d:string | undefined) => void)] { - const {update, value} = useMemoryStorage(`backend-token`, initialValue) +): [string | undefined, ((d: string | undefined) => void)] { + // uncomment for testing + initialValue = "secret-token:secret" as string | undefined + const { update, value } = useMemoryStorage(`backend-token`, initialValue) return [value, update]; } export function useBackendInstanceToken( id: string, -): [string | undefined, ((d:string | undefined) => void)] { - const {update:setToken, value:token, reset} = useMemoryStorage(`backend-token-${id}`) +): [string | undefined, ((d: string | undefined) => void)] { + const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`) const [defaultToken, defaultSetToken] = useBackendDefaultToken(); // instance named 'default' use the default token diff --git a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts new file mode 100644 index 000000000..5c0932f27 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts @@ -0,0 +1,59 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; +import { + Codec, + buildCodecForObject, + codecForBoolean, +} from "@gnu-taler/taler-util"; + +function parse_json_or_undefined<T>(str: string | undefined): T | undefined { + if (str === undefined) return undefined; + try { + return JSON.parse(str); + } catch { + return undefined; + } +} + +export interface Settings { + advanceOrderMode: boolean +} + +const defaultSettings: Settings = { + advanceOrderMode: false, +} + +export const codecForSettings = (): Codec<Settings> => + buildCodecForObject<Settings>() + .property("advanceOrderMode", codecForBoolean()) + .build("Settings"); + +const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings()); + +export function useSettings(): [ + Readonly<Settings>, + <T extends keyof Settings>(key: T, value: Settings[T]) => void, +] { + const { value, update } = useLocalStorage(SETTINGS_KEY); + + const parsed: Settings = value ?? defaultSettings; + function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { + update({ ...parsed, [k]: v }); + } + return [parsed, updateField]; +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx index c8cc20ae0..fa9347c6e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -43,6 +43,7 @@ import { Duration, MerchantBackend, WithId } from "../../../../declaration.js"; import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { useSettings } from "../../../../hooks/useSettings.js"; interface Props { onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; @@ -62,8 +63,8 @@ function with_defaults(config: InstanceConfig): Partial<Entity> { !config.default_pay_delay || config.default_pay_delay.d_us === "forever" ? undefined : add(new Date(), { - seconds: config.default_pay_delay.d_us / (1000 * 1000), - }); + seconds: config.default_pay_delay.d_us / (1000 * 1000), + }); return { inventoryProducts: {}, @@ -138,7 +139,7 @@ export function CreatePage({ const [value, valueHandler] = useState(with_defaults(instanceConfig)); const config = useConfigContext(); const zero = Amounts.zeroOfCurrency(config.currency); - + const [settings] = useSettings() const inventoryList = Object.values(value.inventoryProducts || {}); const productList = Object.values(value.products || {}); @@ -154,10 +155,10 @@ export function CreatePage({ order_price: !value.pricing?.order_price ? i18n.str`required` : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, }), extra: value.extra && !stringIsValidJSON(value.extra) @@ -167,47 +168,47 @@ export function CreatePage({ refund_deadline: !value.payments?.refund_deadline ? undefined : !isFuture(value.payments.refund_deadline) - ? i18n.str`should be in the future` - : value.payments.pay_deadline && - isBefore(value.payments.refund_deadline, value.payments.pay_deadline) - ? i18n.str`refund deadline cannot be before pay deadline` - : value.payments.wire_transfer_deadline && - isBefore( - value.payments.wire_transfer_deadline, - value.payments.refund_deadline, - ) - ? i18n.str`wire transfer deadline cannot be before refund deadline` - : undefined, + ? i18n.str`should be in the future` + : value.payments.pay_deadline && + isBefore(value.payments.refund_deadline, value.payments.pay_deadline) + ? i18n.str`refund deadline cannot be before pay deadline` + : value.payments.wire_transfer_deadline && + isBefore( + value.payments.wire_transfer_deadline, + value.payments.refund_deadline, + ) + ? i18n.str`wire transfer deadline cannot be before refund deadline` + : undefined, pay_deadline: !value.payments?.pay_deadline ? undefined : !isFuture(value.payments.pay_deadline) - ? i18n.str`should be in the future` - : value.payments.wire_transfer_deadline && - isBefore( - value.payments.wire_transfer_deadline, - value.payments.pay_deadline, - ) - ? i18n.str`wire transfer deadline cannot be before pay deadline` - : undefined, + ? i18n.str`should be in the future` + : value.payments.wire_transfer_deadline && + isBefore( + value.payments.wire_transfer_deadline, + value.payments.pay_deadline, + ) + ? i18n.str`wire transfer deadline cannot be before pay deadline` + : undefined, auto_refund_deadline: !value.payments?.auto_refund_deadline ? undefined : !isFuture(value.payments.auto_refund_deadline) - ? i18n.str`should be in the future` - : !value.payments?.refund_deadline - ? i18n.str`should have a refund deadline` - : !isAfter( - value.payments.refund_deadline, - value.payments.auto_refund_deadline, - ) - ? i18n.str`auto refund cannot be after refund deadline` - : undefined, + ? i18n.str`should be in the future` + : !value.payments?.refund_deadline + ? i18n.str`should have a refund deadline` + : !isAfter( + value.payments.refund_deadline, + value.payments.auto_refund_deadline, + ) + ? i18n.str`auto refund cannot be after refund deadline` + : undefined, }), shipping: undefinedIfEmpty({ delivery_date: !value.shipping?.delivery_date ? undefined : !isFuture(value.shipping.delivery_date) - ? i18n.str`should be in the future` - : undefined, + ? i18n.str`should be in the future` + : undefined, }), }; const hasErrors = Object.keys(errors).some( @@ -227,27 +228,27 @@ export function CreatePage({ extra: value.extra, pay_deadline: value.payments.pay_deadline ? { - t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000), - } + t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000), + } : undefined, wire_transfer_deadline: value.payments.wire_transfer_deadline ? { - t_s: Math.floor( - value.payments.wire_transfer_deadline.getTime() / 1000, - ), - } + t_s: Math.floor( + value.payments.wire_transfer_deadline.getTime() / 1000, + ), + } : undefined, refund_deadline: value.payments.refund_deadline ? { - t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000), - } + t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000), + } : undefined, auto_refund: value.payments.auto_refund_deadline ? { - d_us: Math.floor( - value.payments.auto_refund_deadline.getTime() * 1000, - ), - } + d_us: Math.floor( + value.payments.auto_refund_deadline.getTime() * 1000, + ), + } : undefined, wire_fee_amortization: value.payments.wire_fee_amortization as number, max_fee: value.payments.max_fee as string, @@ -374,13 +375,15 @@ export function CreatePage({ inventory={instanceInventory} /> - <NonInventoryProductFrom - productToEdit={editingProduct} - onAddProduct={(p) => { - setEditingProduct(undefined); - return addNewProduct(p); - }} - /> + {settings.advanceOrderMode && + <NonInventoryProductFrom + productToEdit={editingProduct} + onAddProduct={(p) => { + setEditingProduct(undefined); + return addNewProduct(p); + }} + /> + } {allProducts.length > 0 && ( <ProductList @@ -423,8 +426,8 @@ export function CreatePage({ discountOrRise > 0 && (discountOrRise < 1 ? `discount of %${Math.round( - (1 - discountOrRise) * 100, - )}` + (1 - discountOrRise) * 100, + )}` : `rise of %${Math.round((discountOrRise - 1) * 100)}`) } tooltip={i18n.str`Amount to be paid by the customer`} @@ -445,102 +448,108 @@ export function CreatePage({ tooltip={i18n.str`Title of the order to be shown to the customer`} /> - <InputGroup - name="shipping" - label={i18n.str`Shipping and Fulfillment`} - initialActive - > - <InputDate - name="shipping.delivery_date" - label={i18n.str`Delivery date`} - tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`} - /> - {value.shipping?.delivery_date && ( - <InputGroup - name="shipping.delivery_location" - label={i18n.str`Location`} - tooltip={i18n.str`address where the products will be delivered`} - > - <InputLocation name="shipping.delivery_location" /> - </InputGroup> - )} - <Input - name="shipping.fullfilment_url" - label={i18n.str`Fulfillment URL`} - tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} - /> - </InputGroup> + {settings.advanceOrderMode && + <InputGroup + name="shipping" + label={i18n.str`Shipping and Fulfillment`} + initialActive + > + <InputDate + name="shipping.delivery_date" + label={i18n.str`Delivery date`} + tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`} + /> + {value.shipping?.delivery_date && ( + <InputGroup + name="shipping.delivery_location" + label={i18n.str`Location`} + tooltip={i18n.str`address where the products will be delivered`} + > + <InputLocation name="shipping.delivery_location" /> + </InputGroup> + )} + <Input + name="shipping.fullfilment_url" + label={i18n.str`Fulfillment URL`} + tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} + /> + </InputGroup> + } - <InputGroup - name="payments" - label={i18n.str`Taler payment options`} - tooltip={i18n.str`Override default Taler payment settings for this order`} - > - <InputDate - name="payments.pay_deadline" - label={i18n.str`Payment deadline`} - tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`} - /> - <InputDate - name="payments.refund_deadline" - label={i18n.str`Refund deadline`} - tooltip={i18n.str`Time until which the order can be refunded by the merchant.`} - /> - <InputDate - name="payments.wire_transfer_deadline" - label={i18n.str`Wire transfer deadline`} - tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`} - /> - <InputDate - name="payments.auto_refund_deadline" - label={i18n.str`Auto-refund deadline`} - tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} - /> + {settings.advanceOrderMode && + <InputGroup + name="payments" + label={i18n.str`Taler payment options`} + tooltip={i18n.str`Override default Taler payment settings for this order`} + > + <InputDate + name="payments.pay_deadline" + label={i18n.str`Payment deadline`} + tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`} + /> + <InputDate + name="payments.refund_deadline" + label={i18n.str`Refund deadline`} + tooltip={i18n.str`Time until which the order can be refunded by the merchant.`} + /> + <InputDate + name="payments.wire_transfer_deadline" + label={i18n.str`Wire transfer deadline`} + tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`} + /> + <InputDate + name="payments.auto_refund_deadline" + label={i18n.str`Auto-refund deadline`} + tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} + /> - <InputCurrency - name="payments.max_fee" - label={i18n.str`Maximum deposit fee`} - tooltip={i18n.str`Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} - /> - <InputCurrency - name="payments.max_wire_fee" - label={i18n.str`Maximum wire fee`} - tooltip={i18n.str`Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.`} - /> - <InputNumber - name="payments.wire_fee_amortization" - label={i18n.str`Wire fee amortization`} - tooltip={i18n.str`Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.`} - /> - <InputBoolean - name="payments.createToken" - label={i18n.str`Create token`} - tooltip={i18n.str`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`} - /> - <InputNumber - name="payments.minimum_age" - label={i18n.str`Minimum age required`} - tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} - help={ - minAgeByProducts > 0 - ? i18n.str`Min age defined by the producs is ${minAgeByProducts}` - : undefined - } - /> - </InputGroup> + <InputCurrency + name="payments.max_fee" + label={i18n.str`Maximum deposit fee`} + tooltip={i18n.str`Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} + /> + <InputCurrency + name="payments.max_wire_fee" + label={i18n.str`Maximum wire fee`} + tooltip={i18n.str`Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.`} + /> + <InputNumber + name="payments.wire_fee_amortization" + label={i18n.str`Wire fee amortization`} + tooltip={i18n.str`Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.`} + /> + <InputBoolean + name="payments.createToken" + label={i18n.str`Create token`} + tooltip={i18n.str`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`} + /> + <InputNumber + name="payments.minimum_age" + label={i18n.str`Minimum age required`} + tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} + help={ + minAgeByProducts > 0 + ? i18n.str`Min age defined by the producs is ${minAgeByProducts}` + : undefined + } + /> + </InputGroup> + } - <InputGroup - name="extra" - label={i18n.str`Additional information`} - tooltip={i18n.str`Custom information to be included in the contract for this order.`} - > - <Input + {settings.advanceOrderMode && + <InputGroup name="extra" - inputType="multiline" - label={`Value`} - tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} - /> - </InputGroup> + label={i18n.str`Additional information`} + tooltip={i18n.str`Custom information to be included in the contract for this order.`} + > + <Input + name="extra" + inputType="multiline" + label={`Value`} + tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} + /> + </InputGroup> + } </FormProvider> <div class="buttons is-right mt-5"> |