aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-03-13 11:12:46 -0300
committerSebastian <sebasjm@gmail.com>2023-03-13 11:27:52 -0300
commit96d110379e9bfbffedfeebf44c1c972b12fffff4 (patch)
treee7065608cd1561d000f9d2eef79a4ff3e4611e16
parent5f681813cf1bb7bb5c0baa41f29011d0029d003d (diff)
some fixes and validations
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx24
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/useField.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx12
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx78
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx21
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx48
-rw-r--r--packages/merchant-backoffice-ui/src/utils/amount.ts6
-rw-r--r--packages/merchant-backoffice-ui/src/utils/crypto.ts8
9 files changed, 154 insertions, 50 deletions
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
index 021977dfe..495c93897 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx
@@ -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>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
index dbf4e2409..34feec202 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx
@@ -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>
diff --git a/packages/merchant-backoffice-ui/src/components/form/useField.tsx b/packages/merchant-backoffice-ui/src/components/form/useField.tsx
index dffb0cc66..c7559faae 100644
--- a/packages/merchant-backoffice-ui/src/components/form/useField.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/useField.tsx
@@ -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,
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 f4a82f377..d5c888f1c 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
@@ -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(
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 144e968c5..f6aa9a9ae 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
@@ -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>
}
/>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
index a6b616907..64e9a86fe 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
@@ -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>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
index 9fcfcc4bf..d12d1d2d3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx
@@ -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"
diff --git a/packages/merchant-backoffice-ui/src/utils/amount.ts b/packages/merchant-backoffice-ui/src/utils/amount.ts
index 93d6a3a4a..475489d3e 100644
--- a/packages/merchant-backoffice-ui/src/utils/amount.ts
+++ b/packages/merchant-backoffice-ui/src/utils/amount.ts
@@ -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;
diff --git a/packages/merchant-backoffice-ui/src/utils/crypto.ts b/packages/merchant-backoffice-ui/src/utils/crypto.ts
index 7bab8abf1..27e6ade02 100644
--- a/packages/merchant-backoffice-ui/src/utils/crypto.ts
+++ b/packages/merchant-backoffice-ui/src/utils/crypto.ts
@@ -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);