show simple order creation unless advance mode is selected

This commit is contained in:
Sebastian 2023-08-07 08:13:29 -03:00
parent ef148b1501
commit 7d53aa2755
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
4 changed files with 315 additions and 154 deletions

View File

@ -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>
);
}

View File

@ -53,15 +53,17 @@ export function useBackendURL(
export function useBackendDefaultToken( export function useBackendDefaultToken(
initialValue?: string, initialValue?: string,
): [string | undefined, ((d:string | undefined) => void)] { ): [string | undefined, ((d: string | undefined) => void)] {
const {update, value} = useMemoryStorage(`backend-token`, initialValue) // uncomment for testing
initialValue = "secret-token:secret" as string | undefined
const { update, value } = useMemoryStorage(`backend-token`, initialValue)
return [value, update]; return [value, update];
} }
export function useBackendInstanceToken( export function useBackendInstanceToken(
id: string, id: string,
): [string | undefined, ((d:string | undefined) => void)] { ): [string | undefined, ((d: string | undefined) => void)] {
const {update:setToken, value:token, reset} = useMemoryStorage(`backend-token-${id}`) const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`)
const [defaultToken, defaultSetToken] = useBackendDefaultToken(); const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token // instance named 'default' use the default token

View File

@ -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];
}

View File

@ -43,6 +43,7 @@ import { Duration, MerchantBackend, WithId } from "../../../../declaration.js";
import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
import { rate } from "../../../../utils/amount.js"; import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js"; import { undefinedIfEmpty } from "../../../../utils/table.js";
import { useSettings } from "../../../../hooks/useSettings.js";
interface Props { interface Props {
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; 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" !config.default_pay_delay || config.default_pay_delay.d_us === "forever"
? undefined ? undefined
: add(new Date(), { : add(new Date(), {
seconds: config.default_pay_delay.d_us / (1000 * 1000), seconds: config.default_pay_delay.d_us / (1000 * 1000),
}); });
return { return {
inventoryProducts: {}, inventoryProducts: {},
@ -138,7 +139,7 @@ export function CreatePage({
const [value, valueHandler] = useState(with_defaults(instanceConfig)); const [value, valueHandler] = useState(with_defaults(instanceConfig));
const config = useConfigContext(); const config = useConfigContext();
const zero = Amounts.zeroOfCurrency(config.currency); const zero = Amounts.zeroOfCurrency(config.currency);
const [settings] = useSettings()
const inventoryList = Object.values(value.inventoryProducts || {}); const inventoryList = Object.values(value.inventoryProducts || {});
const productList = Object.values(value.products || {}); const productList = Object.values(value.products || {});
@ -154,10 +155,10 @@ export function CreatePage({
order_price: !value.pricing?.order_price order_price: !value.pricing?.order_price
? i18n.str`required` ? i18n.str`required`
: !parsedPrice : !parsedPrice
? i18n.str`not valid` ? i18n.str`not valid`
: Amounts.isZero(parsedPrice) : Amounts.isZero(parsedPrice)
? i18n.str`must be greater than 0` ? i18n.str`must be greater than 0`
: undefined, : undefined,
}), }),
extra: extra:
value.extra && !stringIsValidJSON(value.extra) value.extra && !stringIsValidJSON(value.extra)
@ -167,47 +168,47 @@ export function CreatePage({
refund_deadline: !value.payments?.refund_deadline refund_deadline: !value.payments?.refund_deadline
? undefined ? undefined
: !isFuture(value.payments.refund_deadline) : !isFuture(value.payments.refund_deadline)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: value.payments.pay_deadline && : value.payments.pay_deadline &&
isBefore(value.payments.refund_deadline, value.payments.pay_deadline) isBefore(value.payments.refund_deadline, value.payments.pay_deadline)
? i18n.str`refund deadline cannot be before pay deadline` ? i18n.str`refund deadline cannot be before pay deadline`
: value.payments.wire_transfer_deadline && : value.payments.wire_transfer_deadline &&
isBefore( isBefore(
value.payments.wire_transfer_deadline, value.payments.wire_transfer_deadline,
value.payments.refund_deadline, value.payments.refund_deadline,
) )
? i18n.str`wire transfer deadline cannot be before refund deadline` ? i18n.str`wire transfer deadline cannot be before refund deadline`
: undefined, : undefined,
pay_deadline: !value.payments?.pay_deadline pay_deadline: !value.payments?.pay_deadline
? undefined ? undefined
: !isFuture(value.payments.pay_deadline) : !isFuture(value.payments.pay_deadline)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: value.payments.wire_transfer_deadline && : value.payments.wire_transfer_deadline &&
isBefore( isBefore(
value.payments.wire_transfer_deadline, value.payments.wire_transfer_deadline,
value.payments.pay_deadline, value.payments.pay_deadline,
) )
? i18n.str`wire transfer deadline cannot be before pay deadline` ? i18n.str`wire transfer deadline cannot be before pay deadline`
: undefined, : undefined,
auto_refund_deadline: !value.payments?.auto_refund_deadline auto_refund_deadline: !value.payments?.auto_refund_deadline
? undefined ? undefined
: !isFuture(value.payments.auto_refund_deadline) : !isFuture(value.payments.auto_refund_deadline)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: !value.payments?.refund_deadline : !value.payments?.refund_deadline
? i18n.str`should have a refund deadline` ? i18n.str`should have a refund deadline`
: !isAfter( : !isAfter(
value.payments.refund_deadline, value.payments.refund_deadline,
value.payments.auto_refund_deadline, value.payments.auto_refund_deadline,
) )
? i18n.str`auto refund cannot be after refund deadline` ? i18n.str`auto refund cannot be after refund deadline`
: undefined, : undefined,
}), }),
shipping: undefinedIfEmpty({ shipping: undefinedIfEmpty({
delivery_date: !value.shipping?.delivery_date delivery_date: !value.shipping?.delivery_date
? undefined ? undefined
: !isFuture(value.shipping.delivery_date) : !isFuture(value.shipping.delivery_date)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: undefined, : undefined,
}), }),
}; };
const hasErrors = Object.keys(errors).some( const hasErrors = Object.keys(errors).some(
@ -227,27 +228,27 @@ export function CreatePage({
extra: value.extra, extra: value.extra,
pay_deadline: value.payments.pay_deadline 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, : undefined,
wire_transfer_deadline: value.payments.wire_transfer_deadline wire_transfer_deadline: value.payments.wire_transfer_deadline
? { ? {
t_s: Math.floor( t_s: Math.floor(
value.payments.wire_transfer_deadline.getTime() / 1000, value.payments.wire_transfer_deadline.getTime() / 1000,
), ),
} }
: undefined, : undefined,
refund_deadline: value.payments.refund_deadline 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, : undefined,
auto_refund: value.payments.auto_refund_deadline auto_refund: value.payments.auto_refund_deadline
? { ? {
d_us: Math.floor( d_us: Math.floor(
value.payments.auto_refund_deadline.getTime() * 1000, value.payments.auto_refund_deadline.getTime() * 1000,
), ),
} }
: undefined, : undefined,
wire_fee_amortization: value.payments.wire_fee_amortization as number, wire_fee_amortization: value.payments.wire_fee_amortization as number,
max_fee: value.payments.max_fee as string, max_fee: value.payments.max_fee as string,
@ -374,13 +375,15 @@ export function CreatePage({
inventory={instanceInventory} inventory={instanceInventory}
/> />
<NonInventoryProductFrom {settings.advanceOrderMode &&
productToEdit={editingProduct} <NonInventoryProductFrom
onAddProduct={(p) => { productToEdit={editingProduct}
setEditingProduct(undefined); onAddProduct={(p) => {
return addNewProduct(p); setEditingProduct(undefined);
}} return addNewProduct(p);
/> }}
/>
}
{allProducts.length > 0 && ( {allProducts.length > 0 && (
<ProductList <ProductList
@ -423,8 +426,8 @@ export function CreatePage({
discountOrRise > 0 && discountOrRise > 0 &&
(discountOrRise < 1 (discountOrRise < 1
? `discount of %${Math.round( ? `discount of %${Math.round(
(1 - discountOrRise) * 100, (1 - discountOrRise) * 100,
)}` )}`
: `rise of %${Math.round((discountOrRise - 1) * 100)}`) : `rise of %${Math.round((discountOrRise - 1) * 100)}`)
} }
tooltip={i18n.str`Amount to be paid by the customer`} 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`} tooltip={i18n.str`Title of the order to be shown to the customer`}
/> />
<InputGroup {settings.advanceOrderMode &&
name="shipping" <InputGroup
label={i18n.str`Shipping and Fulfillment`} name="shipping"
initialActive label={i18n.str`Shipping and Fulfillment`}
> initialActive
<InputDate >
name="shipping.delivery_date" <InputDate
label={i18n.str`Delivery date`} name="shipping.delivery_date"
tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`} label={i18n.str`Delivery date`}
/> tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`}
{value.shipping?.delivery_date && ( />
<InputGroup {value.shipping?.delivery_date && (
name="shipping.delivery_location" <InputGroup
label={i18n.str`Location`} name="shipping.delivery_location"
tooltip={i18n.str`address where the products will be delivered`} label={i18n.str`Location`}
> tooltip={i18n.str`address where the products will be delivered`}
<InputLocation name="shipping.delivery_location" /> >
</InputGroup> <InputLocation name="shipping.delivery_location" />
)} </InputGroup>
<Input )}
name="shipping.fullfilment_url" <Input
label={i18n.str`Fulfillment URL`} name="shipping.fullfilment_url"
tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} label={i18n.str`Fulfillment URL`}
/> tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
</InputGroup> />
</InputGroup>
}
<InputGroup {settings.advanceOrderMode &&
name="payments" <InputGroup
label={i18n.str`Taler payment options`} name="payments"
tooltip={i18n.str`Override default Taler payment settings for this order`} label={i18n.str`Taler payment options`}
> tooltip={i18n.str`Override default Taler payment settings for this order`}
<InputDate >
name="payments.pay_deadline" <InputDate
label={i18n.str`Payment deadline`} name="payments.pay_deadline"
tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this 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" <InputDate
label={i18n.str`Refund deadline`} name="payments.refund_deadline"
tooltip={i18n.str`Time until which the order can be refunded by the merchant.`} 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" <InputDate
label={i18n.str`Wire transfer deadline`} name="payments.wire_transfer_deadline"
tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`} label={i18n.str`Wire transfer deadline`}
/> tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`}
<InputDate />
name="payments.auto_refund_deadline" <InputDate
label={i18n.str`Auto-refund deadline`} name="payments.auto_refund_deadline"
tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} label={i18n.str`Auto-refund deadline`}
/> tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
/>
<InputCurrency <InputCurrency
name="payments.max_fee" name="payments.max_fee"
label={i18n.str`Maximum deposit 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.`} 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 <InputCurrency
name="payments.max_wire_fee" name="payments.max_wire_fee"
label={i18n.str`Maximum 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.`} 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 <InputNumber
name="payments.wire_fee_amortization" name="payments.wire_fee_amortization"
label={i18n.str`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.`} 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 <InputBoolean
name="payments.createToken" name="payments.createToken"
label={i18n.str`Create token`} 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.`} tooltip={i18n.str`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`}
/> />
<InputNumber <InputNumber
name="payments.minimum_age" name="payments.minimum_age"
label={i18n.str`Minimum age required`} 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`} 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={ help={
minAgeByProducts > 0 minAgeByProducts > 0
? i18n.str`Min age defined by the producs is ${minAgeByProducts}` ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
: undefined : undefined
} }
/> />
</InputGroup> </InputGroup>
}
<InputGroup {settings.advanceOrderMode &&
name="extra" <InputGroup
label={i18n.str`Additional information`}
tooltip={i18n.str`Custom information to be included in the contract for this order.`}
>
<Input
name="extra" name="extra"
inputType="multiline" label={i18n.str`Additional information`}
label={`Value`} tooltip={i18n.str`Custom information to be included in the contract for this order.`}
tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} >
/> <Input
</InputGroup> name="extra"
inputType="multiline"
label={`Value`}
tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
/>
</InputGroup>
}
</FormProvider> </FormProvider>
<div class="buttons is-right mt-5"> <div class="buttons is-right mt-5">