diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths')
4 files changed, 129 insertions, 30 deletions
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" |