some fixes and validations
This commit is contained in:
parent
5f681813cf
commit
96d110379e
@ -24,8 +24,7 @@ import { InputProps, useField } from "./useField.js";
|
||||
interface Props<T> extends InputProps<T> {
|
||||
readonly?: boolean;
|
||||
expand?: boolean;
|
||||
values: string[];
|
||||
convert?: (v: string) => any;
|
||||
values: any[];
|
||||
toStr?: (v?: any) => string;
|
||||
fromStr?: (s: string) => any;
|
||||
}
|
||||
@ -42,11 +41,11 @@ export function InputSelector<T>({
|
||||
label,
|
||||
help,
|
||||
values,
|
||||
convert,
|
||||
fromStr = defaultFromString,
|
||||
toStr = defaultToString,
|
||||
}: Props<keyof T>): VNode {
|
||||
const { error, value, onChange } = useField<T>(name);
|
||||
|
||||
console.log(error);
|
||||
return (
|
||||
<div class="field is-horizontal">
|
||||
<div class="field-label is-normal">
|
||||
@ -68,18 +67,17 @@ export function InputSelector<T>({
|
||||
disabled={readonly}
|
||||
readonly={readonly}
|
||||
onChange={(e) => {
|
||||
const v = convert
|
||||
? convert(e.currentTarget.value)
|
||||
: e.currentTarget.value;
|
||||
onChange(v);
|
||||
onChange(fromStr(e.currentTarget.value));
|
||||
}}
|
||||
>
|
||||
{placeholder && <option>{placeholder}</option>}
|
||||
{values.map((v, i) => (
|
||||
<option key={i} value={v} selected={value === v}>
|
||||
{toStr(v)}
|
||||
</option>
|
||||
))}
|
||||
{values.map((v, i) => {
|
||||
return (
|
||||
<option key={i} value={v} selected={value === v}>
|
||||
{toStr(v)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
{help}
|
||||
</p>
|
||||
|
@ -96,7 +96,6 @@ export function InputWithAddon<T>({
|
||||
<i class="mdi mdi-alert" />
|
||||
</span>
|
||||
)}
|
||||
{help}
|
||||
{children}
|
||||
</p>
|
||||
{addonAfter && (
|
||||
@ -106,6 +105,7 @@ export function InputWithAddon<T>({
|
||||
)}
|
||||
</div>
|
||||
{error && <p class="help is-danger">{error}</p>}
|
||||
<span class="has-text-grey">{help}</span>
|
||||
</div>
|
||||
{side}
|
||||
</div>
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import { ComponentChildren, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { useFormContext } from "./FormProvider.js";
|
||||
|
||||
interface Use<V> {
|
||||
@ -37,10 +38,11 @@ export function useField<T>(name: keyof T): Use<T[typeof name]> {
|
||||
useFormContext<T>();
|
||||
type P = typeof name;
|
||||
type V = T[P];
|
||||
|
||||
const [isDirty, setDirty] = useState(false);
|
||||
const updateField =
|
||||
(field: P) =>
|
||||
(value: V): void => {
|
||||
setDirty(true);
|
||||
return valueHandler((prev) => {
|
||||
return setValueDeeper(prev, String(field).split("."), value);
|
||||
});
|
||||
@ -50,7 +52,6 @@ export function useField<T>(name: keyof T): Use<T[typeof name]> {
|
||||
const defaultFromString = (v: string): V => v as any;
|
||||
const value = readField(object, String(name));
|
||||
const initial = readField(initialObject, String(name));
|
||||
const isDirty = value !== initial;
|
||||
const hasError = readField(errors, String(name));
|
||||
return {
|
||||
error: isDirty ? hasError : undefined,
|
||||
|
@ -144,12 +144,18 @@ export function CreatePage({
|
||||
|
||||
const { i18n } = useTranslationContext();
|
||||
|
||||
const parsedPrice = !value.pricing?.order_price
|
||||
? undefined
|
||||
: Amounts.parse(value.pricing.order_price);
|
||||
|
||||
const errors: FormErrors<Entity> = {
|
||||
pricing: undefinedIfEmpty({
|
||||
summary: !value.pricing?.summary ? i18n.str`required` : undefined,
|
||||
order_price: !value.pricing?.order_price
|
||||
? i18n.str`required`
|
||||
: Amounts.isZero(value.pricing.order_price)
|
||||
: !parsedPrice
|
||||
? i18n.str`not valid`
|
||||
: Amounts.isZero(parsedPrice)
|
||||
? i18n.str`must be greater than 0`
|
||||
: undefined,
|
||||
}),
|
||||
@ -333,8 +339,8 @@ export function CreatePage({
|
||||
}, [hasProducts, totalAsString]);
|
||||
|
||||
const discountOrRise = rate(
|
||||
value.pricing?.order_price || `${config.currency}:0`,
|
||||
totalAsString,
|
||||
parsedPrice ?? Amounts.zeroOfCurrency(config.currency),
|
||||
totalPrice.amount,
|
||||
);
|
||||
|
||||
const minAgeByProducts = allProducts.reduce(
|
||||
|
@ -19,6 +19,10 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import {
|
||||
Amounts,
|
||||
MerchantTemplateContractDetails,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
|
||||
import { h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
@ -35,7 +39,10 @@ import { InputSelector } from "../../../../components/form/InputSelector.js";
|
||||
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
||||
import { useBackendContext } from "../../../../context/backend.js";
|
||||
import { MerchantBackend } from "../../../../declaration.js";
|
||||
import { randomBase32Key } from "../../../../utils/crypto.js";
|
||||
import {
|
||||
isBase32RFC3548Charset,
|
||||
randomBase32Key,
|
||||
} from "../../../../utils/crypto.js";
|
||||
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
||||
|
||||
type Entity = MerchantBackend.Template.TemplateAddDetails;
|
||||
@ -45,17 +52,14 @@ interface Props {
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
const algorithms = ["0", "1", "2"];
|
||||
const algorithmsNames = [
|
||||
"off",
|
||||
"30s 8d TOTP-SHA1 without amount",
|
||||
"30s 8d eTOTP-SHA1 with amount",
|
||||
];
|
||||
const algorithms = [0, 1, 2];
|
||||
const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
|
||||
|
||||
export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
const backend = useBackendContext();
|
||||
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [state, setState] = useState<Partial<Entity>>({
|
||||
template_contract: {
|
||||
minimum_age: 0,
|
||||
@ -65,6 +69,10 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||
},
|
||||
});
|
||||
|
||||
const parsedPrice = !state.template_contract?.amount
|
||||
? undefined
|
||||
: Amounts.parse(state.template_contract?.amount);
|
||||
|
||||
const errors: FormErrors<Entity> = {
|
||||
template_id: !state.template_id ? i18n.str`should not be empty` : undefined,
|
||||
template_description: !state.template_description
|
||||
@ -73,6 +81,13 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||
template_contract: !state.template_contract
|
||||
? undefined
|
||||
: undefinedIfEmpty({
|
||||
amount: !state.template_contract?.amount
|
||||
? undefined
|
||||
: !parsedPrice
|
||||
? i18n.str`not valid`
|
||||
: Amounts.isZero(parsedPrice)
|
||||
? i18n.str`must be greater than 0`
|
||||
: undefined,
|
||||
minimum_age:
|
||||
state.template_contract.minimum_age < 0
|
||||
? i18n.str`should be greater that 0`
|
||||
@ -84,7 +99,16 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||
: state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second
|
||||
? i18n.str`to short`
|
||||
: undefined,
|
||||
}),
|
||||
} as Partial<MerchantTemplateContractDetails>),
|
||||
pos_key: !state.pos_key
|
||||
? !state.pos_algorithm
|
||||
? undefined
|
||||
: i18n.str`required`
|
||||
: !isBase32RFC3548Charset(state.pos_key)
|
||||
? i18n.str`just letters and numbers from 2 to 7`
|
||||
: state.pos_key.length !== 32
|
||||
? i18n.str`size of the key should be 32`
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const hasErrors = Object.keys(errors).some(
|
||||
@ -144,21 +168,32 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||
/>
|
||||
<InputSelector<Entity>
|
||||
name="pos_algorithm"
|
||||
label={i18n.str`Veritifaction algorithm`}
|
||||
label={i18n.str`Verification algorithm`}
|
||||
tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
|
||||
values={algorithms}
|
||||
toStr={(v) => algorithmsNames[v]}
|
||||
convert={(v) => Number(v)}
|
||||
fromStr={(v) => Number(v)}
|
||||
/>
|
||||
{state.pos_algorithm && state.pos_algorithm > 0 ? (
|
||||
<Input<Entity>
|
||||
<InputWithAddon<Entity>
|
||||
name="pos_key"
|
||||
label={i18n.str`Point-of-sale key`}
|
||||
help=""
|
||||
help="Be sure to be very hard to guess or use the random generator"
|
||||
tooltip={i18n.str`Useful to validate the purchase`}
|
||||
fromStr={(v) => v.toUpperCase()}
|
||||
addonAfter={
|
||||
<span class="icon">
|
||||
{showKey ? (
|
||||
<i class="mdi mdi-eye" />
|
||||
) : (
|
||||
<i class="mdi mdi-eye-off" />
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
side={
|
||||
<span data-tooltip={i18n.str`generate random secret key`}>
|
||||
<span style={{ display: "flex" }}>
|
||||
<button
|
||||
data-tooltip={i18n.str`generate random secret key`}
|
||||
class="button is-info mr-3"
|
||||
onClick={(e) => {
|
||||
const pos_key = randomBase32Key();
|
||||
@ -167,6 +202,23 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
|
||||
>
|
||||
<i18n.Translate>random</i18n.Translate>
|
||||
</button>
|
||||
<button
|
||||
data-tooltip={
|
||||
showKey
|
||||
? i18n.str`show secret key`
|
||||
: i18n.str`hide secret key`
|
||||
}
|
||||
class="button is-info mr-3"
|
||||
onClick={(e) => {
|
||||
setShowKey(!showKey);
|
||||
}}
|
||||
>
|
||||
{showKey ? (
|
||||
<i18n.Translate>hide</i18n.Translate>
|
||||
) : (
|
||||
<i18n.Translate>show</i18n.Translate>
|
||||
)}
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
@ -127,6 +127,15 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
|
||||
<div class="columns">
|
||||
<div class="column" />
|
||||
<div class="column is-four-fifths">
|
||||
<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>
|
||||
<FormProvider
|
||||
object={state}
|
||||
valueHandler={setState}
|
||||
@ -134,7 +143,11 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
|
||||
>
|
||||
<InputCurrency<Entity>
|
||||
name="amount"
|
||||
label={i18n.str`Amount`}
|
||||
label={
|
||||
fixedAmount
|
||||
? i18n.str`Fixed amount`
|
||||
: i18n.str`Default amount`
|
||||
}
|
||||
readonly={fixedAmount}
|
||||
tooltip={i18n.str`Amount of the order`}
|
||||
/>
|
||||
@ -142,7 +155,11 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
|
||||
name="summary"
|
||||
inputType="multiline"
|
||||
readonly={fixedSummary}
|
||||
label={i18n.str`Order summary`}
|
||||
label={
|
||||
fixedSummary
|
||||
? i18n.str`Fixed summary`
|
||||
: i18n.str`Default summary`
|
||||
}
|
||||
tooltip={i18n.str`Title of the order to be shown to the customer`}
|
||||
/>
|
||||
</FormProvider>
|
||||
|
@ -19,6 +19,10 @@
|
||||
* @author Sebastian Javier Marchano (sebasjm)
|
||||
*/
|
||||
|
||||
import {
|
||||
Amounts,
|
||||
MerchantTemplateContractDetails,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
|
||||
import { h, VNode } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
@ -35,7 +39,10 @@ import { InputSelector } from "../../../../components/form/InputSelector.js";
|
||||
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
|
||||
import { useBackendContext } from "../../../../context/backend.js";
|
||||
import { MerchantBackend, WithId } from "../../../../declaration.js";
|
||||
import { randomBase32Key } from "../../../../utils/crypto.js";
|
||||
import {
|
||||
isBase32RFC3548Charset,
|
||||
randomBase32Key,
|
||||
} from "../../../../utils/crypto.js";
|
||||
import { undefinedIfEmpty } from "../../../../utils/table.js";
|
||||
|
||||
type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
|
||||
@ -46,12 +53,8 @@ interface Props {
|
||||
template: Entity;
|
||||
}
|
||||
|
||||
const algorithms = ["0", "1", "2"];
|
||||
const algorithmsNames = [
|
||||
"off",
|
||||
"30s 8d TOTP-SHA1 without amount",
|
||||
"30s 8d eTOTP-SHA1 with amount",
|
||||
];
|
||||
const algorithms = [0, 1, 2];
|
||||
const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
|
||||
|
||||
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||
const { i18n } = useTranslationContext();
|
||||
@ -60,6 +63,10 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [state, setState] = useState<Partial<Entity>>(template);
|
||||
|
||||
const parsedPrice = !state.template_contract?.amount
|
||||
? undefined
|
||||
: Amounts.parse(state.template_contract?.amount);
|
||||
|
||||
const errors: FormErrors<Entity> = {
|
||||
template_description: !state.template_description
|
||||
? i18n.str`should not be empty`
|
||||
@ -67,6 +74,13 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||
template_contract: !state.template_contract
|
||||
? undefined
|
||||
: undefinedIfEmpty({
|
||||
amount: !state.template_contract?.amount
|
||||
? undefined
|
||||
: !parsedPrice
|
||||
? i18n.str`not valid`
|
||||
: Amounts.isZero(parsedPrice)
|
||||
? i18n.str`must be greater than 0`
|
||||
: undefined,
|
||||
minimum_age:
|
||||
state.template_contract.minimum_age < 0
|
||||
? i18n.str`should be greater that 0`
|
||||
@ -78,7 +92,16 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||
: state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second
|
||||
? i18n.str`to short`
|
||||
: undefined,
|
||||
}),
|
||||
} as Partial<MerchantTemplateContractDetails>),
|
||||
pos_key: !state.pos_key
|
||||
? !state.pos_algorithm
|
||||
? undefined
|
||||
: i18n.str`required`
|
||||
: !isBase32RFC3548Charset(state.pos_key)
|
||||
? i18n.str`just letters and numbers from 2 to 7`
|
||||
: state.pos_key.length !== 32
|
||||
? i18n.str`size of the key should be 32`
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const hasErrors = Object.keys(errors).some(
|
||||
@ -155,20 +178,21 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||
/>
|
||||
<InputSelector<Entity>
|
||||
name="pos_algorithm"
|
||||
label={i18n.str`Veritifaction algorithm`}
|
||||
label={i18n.str`Verification algorithm`}
|
||||
tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
|
||||
values={algorithms}
|
||||
toStr={(v) => algorithmsNames[v]}
|
||||
convert={(v) => Number(v)}
|
||||
fromStr={(v) => Number(v)}
|
||||
/>
|
||||
{state.pos_algorithm && state.pos_algorithm > 0 ? (
|
||||
<InputWithAddon<Entity>
|
||||
name="pos_key"
|
||||
label={i18n.str`Point-of-sale key`}
|
||||
inputType={showKey ? "text" : "password"}
|
||||
help=""
|
||||
help="Be sure to be very hard to guess or use the random generator"
|
||||
expand
|
||||
tooltip={i18n.str`Useful to validate the purchase`}
|
||||
fromStr={(v) => v.toUpperCase()}
|
||||
addonAfter={
|
||||
<span class="icon">
|
||||
{showKey ? (
|
||||
@ -179,7 +203,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
|
||||
</span>
|
||||
}
|
||||
side={
|
||||
<span>
|
||||
<span style={{ display: "flex" }}>
|
||||
<button
|
||||
data-tooltip={i18n.str`generate random secret key`}
|
||||
class="button is-info mr-3"
|
||||
|
@ -59,14 +59,12 @@ export function mergeRefunds(
|
||||
return prev;
|
||||
}
|
||||
|
||||
export const rate = (one: string, two: string): number => {
|
||||
const a = Amounts.parseOrThrow(one);
|
||||
const b = Amounts.parseOrThrow(two);
|
||||
export function rate(a: AmountJson, b: AmountJson): number {
|
||||
const af = toFloat(a);
|
||||
const bf = toFloat(b);
|
||||
if (bf === 0) return 0;
|
||||
return af / bf;
|
||||
};
|
||||
}
|
||||
|
||||
function toFloat(amount: AmountJson): number {
|
||||
return amount.value + amount.fraction / amountFractionalBase;
|
||||
|
@ -46,6 +46,14 @@ function encodeBase32(data: ArrayBuffer) {
|
||||
return sb;
|
||||
}
|
||||
|
||||
export function isBase32RFC3548Charset(s: string): boolean {
|
||||
for (let idx = 0; idx < s.length; idx++) {
|
||||
const c = s.charAt(idx);
|
||||
if (encTable.indexOf(c) === -1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function randomBase32Key(): string {
|
||||
var buf = new Uint8Array(20);
|
||||
window.crypto.getRandomValues(buf);
|
||||
|
Loading…
Reference in New Issue
Block a user