taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 2dbdc140c6ded0dfdeaa0501acfaef802b1af44a
parent d3a7f648cf13d73d12ce73b748f699297e90f880
Author: Sebastian <sebasjm@gmail.com>
Date:   Mon, 17 Nov 2025 14:19:58 -0300

fix #10581 more feedback online with vlada and florian

Diffstat:
Apackages/merchant-backoffice-ui/src/assets/question.svg | 3+++
Mpackages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx | 23+++++++++++------------
Mpackages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx | 7++++++-
Mpackages/merchant-backoffice-ui/src/components/menu/SideBar.tsx | 45++++++++++++++++++++++++---------------------
Mpackages/merchant-backoffice-ui/src/components/menu/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/components/modal/index.tsx | 29+++++++++++++----------------
Mpackages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx | 26+++++++++++++++-----------
Mpackages/merchant-backoffice-ui/src/components/product/ProductForm.tsx | 23+++++++++++++----------
Mpackages/merchant-backoffice-ui/src/hooks/preference.ts | 10++++++----
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 154++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx | 80+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx | 58+++++++++++++++++++++++++++++++++++++---------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx | 33+++++++++++++++++++++++++++++++++
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx | 4++--
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx | 47++++++++++++++++++++++++++++++-----------------
Mpackages/merchant-backoffice-ui/src/paths/settings/index.tsx | 15+++++++++++++--
18 files changed, 330 insertions(+), 233 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/assets/question.svg b/packages/merchant-backoffice-ui/src/assets/question.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> + <path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" /> +</svg> diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx @@ -60,19 +60,18 @@ export function DefaultInstanceFormFields({ tooltip={i18n.str`Legal name of the business represented by this instance.`} /> - <FragmentPersonaFlag point={UIElement.option_advanceInstanceSettings}> - <Input<Entity> - name="email" - label={i18n.str`Email`} - tooltip={i18n.str`Contact email`} - /> - - <Input<Entity> - name="phone_number" - label={i18n.str`Phone number`} - tooltip={i18n.str`Contact phone`} - /> + <Input<Entity> + name="email" + label={i18n.str`Email`} + tooltip={i18n.str`Contact email`} + /> + <Input<Entity> + name="phone_number" + label={i18n.str`Phone number`} + tooltip={i18n.str`Contact phone`} + /> + <FragmentPersonaFlag point={UIElement.option_advanceInstanceSettings}> <Input<Entity> name="website" label={i18n.str`Website URL`} diff --git a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx @@ -21,6 +21,7 @@ import { h, VNode } from "preact"; import logo from "../../assets/logo-2021.svg"; +import question from "../../assets/question.svg"; interface Props { onMobileMenu: () => void; @@ -58,11 +59,15 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode { <div class="navbar-menu "> <a class="navbar-start is-justify-content-center is-flex-grow-1" - href="https://tutorials.taler.net/merchant-backoffice" + href="https://tutorials.taler.net/merchant/merchant-backoffice" target="_blank" rel="noreferrer" > <img src={logo} style={{ height: 35, margin: 10 }} /> + <img + src={question} + style={{ height: 20, marginTop: "auto", marginBottom: "auto" }} + /> </a> </div> </nav> diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -168,15 +168,32 @@ export function Sidebar({ mobile }: Props): VNode { point={UIElement.sidebar_tokenFamilies} > <a href={"#/tokenfamilies"} class="has-icon"> + <span class="menu-item-label"> <span class="icon"> <i class="mdi mdi-clock" /> </span> - <span class="menu-item-label"> <i18n.Translate>Subscriptions and Discounts</i18n.Translate> </span> </a> </HtmlPersonaFlag> - + </ul> + <p class="menu-label"> + <i18n.Translate>Configuration</i18n.Translate> + </p> + <ul class="menu-list"> + <HtmlPersonaFlag + htmlElement="li" + point={UIElement.sidebar_bankAccounts} + > + <a href={"#/bank"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-bank" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Bank account</i18n.Translate> + </span> + </a> + </HtmlPersonaFlag> {hideKycMenuItem ? undefined : ( <HtmlPersonaFlag htmlElement="li" @@ -226,24 +243,6 @@ export function Sidebar({ mobile }: Props): VNode { </a> </HtmlPersonaFlag> )} - </ul> - <p class="menu-label"> - <i18n.Translate>Configuration</i18n.Translate> - </p> - <ul class="menu-list"> - <HtmlPersonaFlag - htmlElement="li" - point={UIElement.sidebar_bankAccounts} - > - <a href={"#/bank"} class="has-icon"> - <span class="icon"> - <i class="mdi mdi-bank" /> - </span> - <span class="menu-item-label"> - <i18n.Translate>Bank account</i18n.Translate> - </span> - </a> - </HtmlPersonaFlag> <HtmlPersonaFlag htmlElement="li" point={UIElement.sidebar_otpDevices} @@ -419,10 +418,14 @@ function getAvailableForPersona(p: MerchantPersona): ElementMap { [UIElement.sidebar_wireTransfers]: true, [UIElement.sidebar_inventory]: true, [UIElement.sidebar_otpDevices]: true, - [UIElement.action_manuallyCreatingOrders]: true, + [UIElement.action_createOrderManually]: true, [UIElement.option_advanceInstanceSettings]: true, [UIElement.option_advanceOrderCreation]: true, [UIElement.option_otpDevicesOnTemplate]: true, + [UIElement.option_paymentTimeoutOnTemplate]: true, + // [UIElement.option_ageRestriction]: true, + [UIElement.action_useRevenueApi]: true, + [UIElement.option_inventoryTaxes]: true, }; case "developer": return ALL_ELEMENTS; diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx @@ -74,7 +74,7 @@ function getInstanceTitle(path: string, id: string): string { case InstancePaths.templates_use: return `${id}: Use template`; case InstancePaths.interface: - return `${id}: Interface`; + return `${id}: Personalization`; case InstancePaths.token_family_list: return `${id}: Token families`; case InstancePaths.token_family_new: diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -442,7 +442,7 @@ export function ValidBankAccount({ <i18n.Translate> In order to prove that you are the beneficial owner of the bank account, you are required to wire a small amount to a specified bank - account with the subject given hereinafter. + account with the subject below. </i18n.Translate> </p> <div class="table-container"> @@ -453,28 +453,27 @@ export function ValidBankAccount({ <i18n.Translate>Step 1:</i18n.Translate> &nbsp; <i18n.Translate> - Copy this string and paste it into the 'Subject' or 'Purpose' - field in your preferred banking app or online banking website. + Copy and paste this IBAN and the legal name of the beneficial + owner into the respective fields in your preferred banking app + or online banking website. </i18n.Translate> </td> </tr> - <Row name={i18n.str`Subject`} value={subject} literal /> - + {accountPart} + {receiverName ? ( + <Row name={i18n.str`Receiver name`} value={receiverName} /> + ) : undefined} <tr> <td colSpan={3}> <i18n.Translate>Step 2:</i18n.Translate> &nbsp; <i18n.Translate> - Copy and paste this IBAN and the legal name of the beneficial - owner into the respective fields in your preferred banking app - or online banking website. + Copy this string and paste it into the 'Subject' or 'Purpose' + field in your preferred banking app or online banking website. </i18n.Translate> </td> </tr> - {accountPart} - {receiverName ? ( - <Row name={i18n.str`Receiver name`} value={receiverName} /> - ) : undefined} + <Row name={i18n.str`Subject`} value={subject} literal /> {receiverPostalCode ? ( <Row name={i18n.str`Receiver postal code`} @@ -504,11 +503,10 @@ export function ValidBankAccount({ /> } /> */} - <tr> <td colSpan={3}> {/* <WarningBox style={{ margin: 0 }}> */} - <b> + <b > <i18n.Translate> Make sure ALL data is correct, especially the subject, and that you are choosing the bank account from which you @@ -520,9 +518,8 @@ export function ValidBankAccount({ {/* </WarningBox> */} </td> </tr> - <tr> - <td colSpan={2} width="100%" style={{ wordBreak: "break-all" }}> + <td colSpan={2} width="100%"> <i18n.Translate> As an alternative, in case that your bank already supports the 'PayTo URI' standard, you can use this{" "} diff --git a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx @@ -16,7 +16,7 @@ import { AmountString, Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useCommonPreferences, - useTranslationContext + useTranslationContext, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks"; @@ -28,6 +28,8 @@ import { InputCurrency } from "../form/InputCurrency.js"; import { InputImage } from "../form/InputImage.js"; import { InputNumber } from "../form/InputNumber.js"; import { InputTaxes } from "../form/InputTaxes.js"; +import { FragmentPersonaFlag } from "../menu/SideBar.js"; +import { UIElement } from "../../hooks/preference.js"; type Entity = TalerMerchantApi.Product; @@ -70,7 +72,8 @@ export function NonInventoryProductFrom({ return ( <Fragment> <div class="buttons"> - <button type="button" + <button + type="button" class="button is-success" data-tooltip={i18n.str`Describe and add a product that is not in the inventory list`} onClick={() => setShowCreateProduct(true)} @@ -87,7 +90,8 @@ export function NonInventoryProductFrom({ <div class="modal-card"> <header class="modal-card-head"> <p class="modal-card-title">{i18n.str`Complete information of the product`}</p> - <button type="button" + <button + type="button" class="delete " aria-label="close" onClick={() => setShowCreateProduct(false)} @@ -101,13 +105,15 @@ export function NonInventoryProductFrom({ </section> <footer class="modal-card-foot"> <div class="buttons is-right" style={{ width: "100%" }}> - <button type="button" + <button + type="button" class="button " onClick={() => setShowCreateProduct(false)} > <i18n.Translate>Cancel</i18n.Translate> </button> - <button type="button" + <button + type="button" class="button is-info " disabled={!submitForm} onClick={submitForm} @@ -117,7 +123,8 @@ export function NonInventoryProductFrom({ </div> </footer> </div> - <button type="button" + <button + type="button" class="modal-close is-large " aria-label="close" onClick={() => setShowCreateProduct(false)} @@ -144,7 +151,6 @@ interface NonInventoryProduct { export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { const { i18n } = useTranslationContext(); - const [{ showDebugInfo }] = useCommonPreferences(); const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({ taxes: [], ...initial, @@ -213,14 +219,12 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { tooltip={i18n.str`How many products will be added.`} /> - {showDebugInfo ? ( + <FragmentPersonaFlag point={UIElement.option_inventoryTaxes}> <InputTaxes<NonInventoryProduct> name="taxes" label={i18n.str`Taxes`} /> - ) : ( - <Fragment /> - )} + </FragmentPersonaFlag> </FormProvider> </div> ); diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx @@ -29,19 +29,20 @@ import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks"; import { useSessionContext } from "../../context/session.js"; +import { useInstanceCategories } from "../../hooks/category.js"; import { undefinedIfEmpty } from "../../utils/table.js"; +import { ErrorLoadingMerchant } from "../ErrorLoadingMerchant.js"; import { FormProvider } from "../form/FormProvider.js"; import { Input } from "../form/Input.js"; +import { InputArray } from "../form/InputArray.js"; import { InputCurrency } from "../form/InputCurrency.js"; import { InputImage } from "../form/InputImage.js"; import { InputNumber } from "../form/InputNumber.js"; import { InputStock, Stock } from "../form/InputStock.js"; import { InputTaxes } from "../form/InputTaxes.js"; import { InputWithAddon } from "../form/InputWithAddon.js"; -import { InputArray } from "../form/InputArray.js"; -import { useInstanceCategories } from "../../hooks/category.js"; -import { ErrorLoadingMerchant } from "../ErrorLoadingMerchant.js"; -import { usePreference } from "../../hooks/preference.js"; +import { FragmentPersonaFlag } from "../menu/SideBar.js"; +import { UIElement } from "../../hooks/preference.js"; type Entity = TalerMerchantApi.ProductDetail & { product_id: string; @@ -222,12 +223,14 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { label={i18n.str`Description`} tooltip={i18n.str`Product description for customers.`} /> - <InputNumber<Entity> - name="minimum_age" - label={i18n.str`Age restriction`} - tooltip={i18n.str`Is this product restricted for customer below certain age?`} - help={i18n.str`Minimum age of the customer`} - /> + <FragmentPersonaFlag point={UIElement.option_ageRestriction}> + <InputNumber<Entity> + name="minimum_age" + label={i18n.str`Age restriction`} + tooltip={i18n.str`Is this product restricted for customer below certain age?`} + help={i18n.str`Minimum age of the customer`} + /> + </FragmentPersonaFlag> <Input<Entity> name="unit" label={i18n.str`Unit name`} diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts @@ -42,13 +42,14 @@ export enum UIElement { sidebar_settings, sidebar_password, sidebar_accessTokens, - // sidebar_interfaces, - // sidebar_instanceNew, - // sidebar_instanceList, - action_manuallyCreatingOrders, + action_createOrderManually, option_otpDevicesOnTemplate, option_advanceOrderCreation, option_advanceInstanceSettings, + action_useRevenueApi, + option_paymentTimeoutOnTemplate, + option_ageRestriction, + option_inventoryTaxes, } export interface Preferences { @@ -81,6 +82,7 @@ export const codecForPreferences = (): Codec<Preferences> => "persona", codecOptional( codecForEither( + codecForConstString("developer"), codecForConstString("expert"), codecForConstString("offline-vending-machine"), codecForConstString("point-of-sale"), diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -53,6 +53,8 @@ import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { TestRevenueErrorType, testRevenueAPI } from "../create/index.js"; +import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; +import { UIElement, usePreference } from "../../../../hooks/preference.js"; type Entity = TalerMerchantApi.BankAccountDetail & WithId; type FormType = TalerMerchantApi.AccountPatchDetails & { @@ -68,8 +70,8 @@ interface Props { export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const [{ showDebugInfo }] = useCommonPreferences(); - const accountAuthType = showDebugInfo + const [{ persona }] = usePreference(); + const accountAuthType = persona === "developer" ? ["unedit", "none", "basic", "bearer"] : ["unedit", "none", "basic"]; @@ -323,81 +325,79 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { name="payto_uri" label={i18n.str`Account`} /> - {!showDebugInfo ? undefined : ( - <Fragment> - <div class="message-body" style={{ marginBottom: 10 }}> - <p> - <i18n.Translate> - If the bank supports Taler Revenue API then you can - add the endpoint URL below to keep the revenue - information in sync. - </i18n.Translate> - </p> - </div> - <Input<Entity> - name="credit_facade_url" - label={i18n.str`Endpoint URL`} - help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/" - expand - tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} - /> - <InputSelector - name="credit_facade_credentials.type" - label={i18n.str`Auth type`} - tooltip={i18n.str`Choose the authentication type for the account info URL`} - values={accountAuthType} - toStr={(str) => { - if (str === "none") - return i18n.str`Without authentication`; - if (str === "basic") - return i18n.str`With username and password`; - if (str === "bearer") return i18n.str`With token`; - return i18n.str`Do not change`; - }} - /> - {state.credit_facade_credentials?.type === "basic" ? ( - <Fragment> - <Input - name="credit_facade_credentials.username" - label={i18n.str`Username`} - tooltip={i18n.str`Username to access the account information.`} - /> - <Input - name="credit_facade_credentials.password" - inputType="password" - label={i18n.str`Password`} - tooltip={i18n.str`Password to access the account information.`} - /> - </Fragment> - ) : undefined} - {state.credit_facade_credentials?.type === "bearer" ? ( - <Fragment> - <Input - name="credit_facade_credentials.token" - label={i18n.str`Token`} - inputType="password" - tooltip={i18n.str`Access token to access the account information.`} - /> - </Fragment> - ) : undefined} - <InputToggle<FormType> - label={i18n.str`Match`} - tooltip={i18n.str`Check where the information match against the server info.`} - name="verified" - readonly - threeState - side={ - <ButtonBetterBulma - class="button is-info" - data-tooltip={i18n.str`Compare info from server with account form`} - onClick={test} - > - <i18n.Translate>Test</i18n.Translate> - </ButtonBetterBulma> - } - /> - </Fragment> - )} + <FragmentPersonaFlag point={UIElement.action_useRevenueApi}> + <div class="message-body" style={{ marginBottom: 10 }}> + <p> + <i18n.Translate> + If the bank supports Taler Revenue API then you can add + the endpoint URL below to keep the revenue information + in sync. + </i18n.Translate> + </p> + </div> + <Input<Entity> + name="credit_facade_url" + label={i18n.str`Endpoint URL`} + help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/" + expand + tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} + /> + <InputSelector + name="credit_facade_credentials.type" + label={i18n.str`Auth type`} + tooltip={i18n.str`Choose the authentication type for the account info URL`} + values={accountAuthType} + toStr={(str) => { + if (str === "none") + return i18n.str`Without authentication`; + if (str === "basic") + return i18n.str`With username and password`; + if (str === "bearer") return i18n.str`With token`; + return i18n.str`Do not change`; + }} + /> + {state.credit_facade_credentials?.type === "basic" ? ( + <Fragment> + <Input + name="credit_facade_credentials.username" + label={i18n.str`Username`} + tooltip={i18n.str`Username to access the account information.`} + /> + <Input + name="credit_facade_credentials.password" + inputType="password" + label={i18n.str`Password`} + tooltip={i18n.str`Password to access the account information.`} + /> + </Fragment> + ) : undefined} + {state.credit_facade_credentials?.type === "bearer" ? ( + <Fragment> + <Input + name="credit_facade_credentials.token" + label={i18n.str`Token`} + inputType="password" + tooltip={i18n.str`Access token to access the account information.`} + /> + </Fragment> + ) : undefined} + <InputToggle<FormType> + label={i18n.str`Match`} + tooltip={i18n.str`Check where the information match against the server info.`} + name="verified" + readonly + threeState + side={ + <ButtonBetterBulma + class="button is-info" + data-tooltip={i18n.str`Compare info from server with account form`} + onClick={test} + > + <i18n.Translate>Test</i18n.Translate> + </ButtonBetterBulma> + } + /> + </FragmentPersonaFlag> </FormProvider> <div class="buttons is-right mt-5"> 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 @@ -717,7 +717,7 @@ export function CreatePage({ /> </FragmentPersonaFlag> <FragmentPersonaFlag - point={UIElement.option_advanceOrderCreation} + point={UIElement.option_ageRestriction} showAnywayIf={errors?.payments?.minimum_age !== undefined} > <InputNumber diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -94,7 +94,7 @@ export function CardTable({ <HtmlPersonaFlag htmlElement="div" - point={UIElement.action_manuallyCreatingOrders} + point={UIElement.action_createOrderManually} class="card-header-icon" aria-label="more options" > diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -19,7 +19,12 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AmountString, Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + AmountString, + Amounts, + HttpStatusCode, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import { useCommonPreferences, useLocalNotificationBetter, @@ -40,8 +45,10 @@ import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; import { dateFormatForSettings, + UIElement, usePreference, } from "../../../../hooks/preference.js"; +import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; type Entity = TalerMerchantApi.ProductDetail & WithId; @@ -138,7 +145,6 @@ function Table({ onLoadMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); - const [{ showDebugInfo }] = useCommonPreferences(); const [preference] = usePreference(); const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); @@ -146,6 +152,16 @@ function Table({ lib.instance.updateProduct.bind(lib.instance), ); update.onSuccess = () => rowSelectionHandler(undefined); + update.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized.`; + case HttpStatusCode.NotFound: + return i18n.str`Product not found.`; + case HttpStatusCode.Conflict: + return i18n.str`This change was made without outdated info.`; + } + }; return ( <div class="table-container"> {onLoadMoreBefore && ( @@ -168,18 +184,14 @@ function Table({ <th> <i18n.Translate>Price per unit</i18n.Translate> </th> - {showDebugInfo ? ( - <Fragment> - <th> - <i18n.Translate>Taxes</i18n.Translate> - </th> - <th> - <i18n.Translate>Sales</i18n.Translate> - </th> - </Fragment> - ) : ( - <Fragment /> - )} + <FragmentPersonaFlag point={UIElement.option_inventoryTaxes}> + <th> + <i18n.Translate>Taxes</i18n.Translate> + </th> + <th> + <i18n.Translate>Sales</i18n.Translate> + </th> + </FragmentPersonaFlag> <th> <i18n.Translate>Stock</i18n.Translate> </th> @@ -262,28 +274,24 @@ function Table({ > {isFree ? i18n.str`Free` : `${i.price} / ${i.unit}`} </td> - {showDebugInfo ? ( - <Fragment> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - {sum(i.taxes)} - </td> - <td - onClick={() => - rowSelection !== i.id && rowSelectionHandler(i.id) - } - style={{ cursor: "pointer" }} - > - {difference(i.price, sum(i.taxes))} - </td> - </Fragment> - ) : ( - <Fragment /> - )} + <FragmentPersonaFlag point={UIElement.option_inventoryTaxes}> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {sum(i.taxes)} + </td> + <td + onClick={() => + rowSelection !== i.id && rowSelectionHandler(i.id) + } + style={{ cursor: "pointer" }} + > + {difference(i.price, sum(i.taxes))} + </td> + </FragmentPersonaFlag> <td onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id) 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 @@ -50,8 +50,12 @@ import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; import { TextField } from "../../../../components/form/TextField.js"; import { useSessionContext } from "../../../../context/session.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; -import { ComponentPersonaFlag } from "../../../../components/menu/SideBar.js"; +import { + ComponentPersonaFlag, + FragmentPersonaFlag, +} from "../../../../components/menu/SideBar.js"; import { UIElement } from "../../../../hooks/preference.js"; +import { useInstanceDetails } from "../../../../hooks/instance.js"; // type Entity = TalerMerchantApi.TemplateAddDetails & { type: Steps }; type Entity = { @@ -70,9 +74,10 @@ type Entity = { interface Props { onCreated: () => void; onBack?: () => void; + defaultSettingsDuration: Duration; } -export function CreatePage({ onCreated, onBack }: Props): VNode { +export function CreatePage({ defaultSettingsDuration, onCreated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const { config, state: session, lib } = useSessionContext(); const devices = useInstanceOtpDevices(); @@ -80,9 +85,9 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>({ minimum_age: 0, - pay_duration: { - d_ms: 1000 * 60 * 30, //30 min - }, + amount_editable: true, + summary_editable: true, + pay_duration: defaultSettingsDuration, }); function updateState(up: (s: Partial<Entity>) => Partial<Entity>) { @@ -104,11 +109,16 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { ? i18n.str`Invalid. Please enter letters and numbers only.` : undefined, description: !state.description ? i18n.str`Required` : undefined, + summary: !state.summary + ? state.summary_editable + ? undefined + : i18n.str`Required` + : undefined, // more summary validations? none amount: !state.amount ? state.amount_editable ? undefined : i18n.str`Required` - : !parsedPrice + : !parsedPrice // more summary validations? is valid amount... ? i18n.str`Invalid` : Amounts.isZero(parsedPrice) ? state.amount_editable @@ -214,15 +224,14 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { /> <Input<Entity> name="description" - label={i18n.str`Description`} - help="" + label={i18n.str`Template name`} tooltip={i18n.str`Describe what this template stands for`} /> <Input<Entity> name="summary" inputType="multiline" - label={i18n.str`Summary`} + label={i18n.str`Order summary`} tooltip={i18n.str`If specified here, this template will create orders with the same summary`} /> <InputToggle<Entity> @@ -256,18 +265,25 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { </TextField> </Fragment> )} - <InputNumber<Entity> - name="minimum_age" - label={i18n.str`Minimum age`} - help="" - tooltip={i18n.str`Is this contract restricted to some age?`} - /> - <InputDuration<Entity> - name="pay_duration" - label={i18n.str`Payment timeout`} - help="" - tooltip={i18n.str`How much time the customer has to complete the payment once the order was created.`} - /> + <FragmentPersonaFlag point={UIElement.option_ageRestriction}> + <InputNumber<Entity> + name="minimum_age" + label={i18n.str`Minimum age`} + help="" + tooltip={i18n.str`Is this contract restricted to some age?`} + /> + </FragmentPersonaFlag> + <FragmentPersonaFlag + point={UIElement.option_paymentTimeoutOnTemplate} + > + <InputDuration<Entity> + name="pay_duration" + label={i18n.str`Payment timeout`} + help="" + tooltip={i18n.str`How much time the customer has to complete the payment once the order was created.`} + /> + </FragmentPersonaFlag> + {!deviceList.length ? ( <ComponentPersonaFlag Comp={TextField} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx @@ -21,6 +21,17 @@ import { Fragment, VNode, h } from "preact"; import { CreatePage } from "./CreatePage.js"; +import { + assertUnreachable, + Duration, + HttpStatusCode, + TalerError, +} from "@gnu-taler/taler-util"; +import { useInstanceDetails } from "../../../../hooks/instance.js"; +import { Loading } from "@gnu-taler/web-util/browser"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; interface Props { onBack?: () => void; @@ -28,12 +39,34 @@ interface Props { } export default function CreateTemplate({ onConfirm, onBack }: Props): VNode { + const result = useInstanceDetails(); + + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage />; + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(result); + } + } + } return ( <> <CreatePage onBack={onBack} onCreated={onConfirm} + defaultSettingsDuration={Duration.fromTalerProtocolDuration( + result.body.default_pay_delay, + )} /> </> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx @@ -180,7 +180,7 @@ function Table({ data-tooltip={i18n.str`Use template to create new order`} onClick={() => onNewOrder(i)} > - <i18n.Translate>Use template</i18n.Translate> + <i18n.Translate>Test</i18n.Translate> </button> <button type="button" @@ -188,7 +188,7 @@ function Table({ data-tooltip={i18n.str`Generate a QR code for the template.`} onClick={() => onQR(i)} > - QR + <i18n.Translate>Show QR</i18n.Translate> </button> </div> </td> 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 @@ -52,7 +52,10 @@ import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; import { UIElement } from "../../../../hooks/preference.js"; -import { ComponentPersonaFlag } from "../../../../components/menu/SideBar.js"; +import { + ComponentPersonaFlag, + FragmentPersonaFlag, +} from "../../../../components/menu/SideBar.js"; type Entity = { description?: string; @@ -158,11 +161,16 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { const errors: FormErrors<Entity> = { description: !state.description ? i18n.str`Required` : undefined, + summary: !state.summary + ? state.summary_editable + ? undefined + : i18n.str`Required` + : undefined, // more summary validations? none amount: !state.amount ? state.amount_editable ? undefined : i18n.str`Required` - : !parsedPrice + : !parsedPrice // more summary validations? is valid amount... ? i18n.str`Invalid` : Amounts.isZero(parsedPrice) ? state.amount_editable @@ -274,14 +282,13 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { > <Input<Entity> name="description" - label={i18n.str`Description`} - help="" + label={i18n.str`Template name`} tooltip={i18n.str`Describe what this template stands for`} /> <Input<Entity> name="summary" inputType="multiline" - label={i18n.str`Summary`} + label={i18n.str`Order summary`} tooltip={i18n.str`If specified, this template will create order with the same summary`} /> <InputToggle<Entity> @@ -324,18 +331,24 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { /> </Fragment> )} - <InputNumber<Entity> - name="minimum_age" - label={i18n.str`Minimum age`} - help="" - tooltip={i18n.str`Is this contract restricted to some age?`} - /> - <InputDuration<Entity> - name="pay_duration" - label={i18n.str`Payment timeout`} - help="" - tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} - /> + <FragmentPersonaFlag point={UIElement.option_ageRestriction}> + <InputNumber<Entity> + name="minimum_age" + label={i18n.str`Minimum age`} + tooltip={i18n.str`Is this contract restricted to some age?`} + /> + </FragmentPersonaFlag> + <FragmentPersonaFlag + point={UIElement.option_paymentTimeoutOnTemplate} + > + <InputDuration<Entity> + name="pay_duration" + label={i18n.str`Payment timeout`} + help="" + tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} + /> + </FragmentPersonaFlag> + {!deviceList.length ? ( <ComponentPersonaFlag Comp={TextField} diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx @@ -28,6 +28,7 @@ import { InputSelector } from "../../components/form/InputSelector.js"; import { LangSelector } from "../../components/menu/LangSelector.js"; import { Preferences, usePreference } from "../../hooks/preference.js"; import { useSessionContext } from "../../context/session.js"; +import { NotificationCard } from "../../components/menu/index.js"; type FormType = Preferences; export function Settings({ onClose }: { onClose?: () => void }): VNode { @@ -39,8 +40,8 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { const formValue: typeof value = { ...value, - persona : value.persona ?? config.default_persona - } + persona: value.persona ?? config.default_persona, + }; function valueHandler(s: (d: Partial<FormType>) => Partial<FormType>): void { const next = s(formValue); @@ -51,6 +52,7 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { dateFormat: next.dateFormat ?? "ymd", persona: next.persona ?? config.default_persona, }; + const isDeveloper = next.persona === "developer"; if (isDeveloper !== showDebugInfo) { updateCommonPref("showDebugInfo", isDeveloper); @@ -145,6 +147,15 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { /> </FormProvider> </div> + {value.persona === "developer" ? ( + <NotificationCard + notification={{ + message: i18n.str`Developer mode`, + description: i18n.str`Only use developer mode if you know how the application works. Some features enabled in this mode are still under testing.`, + type: "WARN", + }} + /> + ) : undefined} </div> <div class="column" /> </div>