diff options
Diffstat (limited to 'packages')
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); | 
