diff options
author | Sebastian <sebasjm@gmail.com> | 2023-03-13 11:12:46 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2023-03-13 11:27:52 -0300 |
commit | 96d110379e9bfbffedfeebf44c1c972b12fffff4 (patch) | |
tree | e7065608cd1561d000f9d2eef79a4ff3e4611e16 | |
parent | 5f681813cf1bb7bb5c0baa41f29011d0029d003d (diff) |
some fixes and validations
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); |