some fixes and validations

This commit is contained in:
Sebastian 2023-03-13 11:12:46 -03:00
parent 5f681813cf
commit 96d110379e
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
9 changed files with 154 additions and 50 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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(

View File

@ -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>
}
/>

View File

@ -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>

View File

@ -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"

View File

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

View File

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