taler-typescript-core

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

commit 69b9d14ed36fb663ca597986f5b6927d88981973
parent e78656bd194367df94117f44406931506e4aa06a
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Thu, 23 Apr 2026 16:42:40 -0300

no need to save in local storage, use memory context

Diffstat:
Mpackages/merchant-backoffice-ui/src/Application.tsx | 5++++-
Mpackages/merchant-backoffice-ui/src/components/form/Input.tsx | 13+++++++++++++
Mpackages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx | 26++++++++++++++------------
Mpackages/merchant-backoffice-ui/src/components/menu/SideBar.tsx | 12++++++------
Apackages/merchant-backoffice-ui/src/context/currency.ts | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/merchant-backoffice-ui/src/context/session.ts | 32+-------------------------------
Mpackages/merchant-backoffice-ui/src/hooks/instance.ts | 7++++---
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 21+++++++++++++++------
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx | 13+++++--------
9 files changed, 163 insertions(+), 67 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx @@ -98,6 +98,7 @@ import { } from "./hooks/pots.js"; import { revalidateInstanceGroups } from "./hooks/groups.js"; import { revalidateInstanceScheduledReports } from "./hooks/reports.js"; +import { CurrenciesProvider } from "./context/currency.js"; const TALER_SCREEN_ID = 2; const WITH_LOCAL_STORAGE_CACHE = false; @@ -150,7 +151,9 @@ export function Application(): VNode { > <TalerWalletIntegrationBrowserProvider> <BrowserHashNavigationProvider> - <Routing /> + <CurrenciesProvider> + <Routing /> + </CurrenciesProvider> </BrowserHashNavigationProvider> </TalerWalletIntegrationBrowserProvider> </SWRConfig> diff --git a/packages/merchant-backoffice-ui/src/components/form/Input.tsx b/packages/merchant-backoffice-ui/src/components/form/Input.tsx @@ -103,6 +103,19 @@ export function InternalTextInputSwitch({ ref={focus ? doAutoFocus : undefined} class={hasError ? "textarea is-danger" : "textarea"} rows={3} + onKeyDown={e => { + console.log("asd") + if (e.ctrlKey && e.key === "Enter") { + // e.preventDefault() + const f = e.currentTarget.form + // FIXME: why this requestSubmit doesnt trigger form submission? + if (f) { + console.log(f.checkValidity()) + console.log(f.reportValidity()) + console.log(f.requestSubmit()) + } + } + }} /> ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx @@ -18,22 +18,22 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { ComponentChildren, h, VNode } from "preact"; -import { InputWithAddon } from "./InputWithAddon.js"; -import { InputProps, useField } from "./useField.js"; import { Amounts, - AmountString, - CurrencySpecification, + AmountString } from "@gnu-taler/taler-util"; +import { ComponentChildren, h, VNode } from "preact"; +import { useCurrenciesContext } from "../../context/currency.js"; import { useSessionContext } from "../../context/session.js"; +import { InputWithAddon } from "./InputWithAddon.js"; +import { InputProps, useField } from "./useField.js"; export interface Props<T> extends InputProps<T> { expand?: boolean; addonAfter?: ComponentChildren; children?: ComponentChildren; side?: ComponentChildren; + focus?: boolean; } export function InputCurrency<T>({ @@ -46,20 +46,23 @@ export function InputCurrency<T>({ expand, addonAfter, children, + focus, side, }: Props<keyof T>): VNode { - const { config, state } = useSessionContext(); + const { config } = useSessionContext(); + const { currency } = useCurrenciesContext(); const { value } = useField<T>(name); const parsedValue = !value ? undefined : Amounts.parse(value); + console.log("intpu currency", currency); // if the field already has a value, use the // currency of that value // fallback to the selected currency on the session const cSpec = parsedValue?.currency ? config.currencies[parsedValue.currency] - : state.currency; + : currency; - const cId = parsedValue?.currency ?? state.currency?.id; + const cId = parsedValue?.currency ?? currency?.id; const id = cSpec?.num_fractional_input_digits; const step = !id ? 0.1 : Math.pow(10, -1 * (id ?? 0)); @@ -73,14 +76,13 @@ export function InputCurrency<T>({ label={label} placeholder={placeholder} help={help} + focus={focus} tooltip={tooltip} addonAfter={addonAfter} inputType="decimal" expand={expand} toStr={(v?: AmountString) => v?.split(":")[1] || ""} - fromStr={(v: string) => - !v ? undefined : `${cId ?? ""}:${v}` - } + fromStr={(v: string) => (!v ? undefined : `${cId ?? ""}:${v}`)} inputExtra={{ min: 0, step }} > {children} diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -21,7 +21,7 @@ import { MerchantAccountKycStatusSimplified, - MerchantPersona + MerchantPersona, } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; @@ -29,10 +29,11 @@ import { useSessionContext } from "../../context/session.js"; import { useSettingsContext } from "../../context/settings.js"; import { useAvailableCurrencies, - useInstanceKYCSimplifiedWorstStatusLongPolling + useInstanceKYCSimplifiedWorstStatusLongPolling, } from "../../hooks/instance.js"; import { UIElement, usePreference } from "../../hooks/preference.js"; import { LangSelector } from "./LangSelector.js"; +import { useCurrenciesContext } from "../../context/currency.js"; const TALER_SCREEN_ID = 17; // const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; @@ -42,10 +43,9 @@ interface Props { mobile?: boolean; } - export function Sidebar({ mobile }: Props): VNode { const { i18n } = useTranslationContext(); - const { state, logOut, config, switchCurrency } = useSessionContext(); + const { state, logOut, config } = useSessionContext(); const worstKycStatus = useInstanceKYCSimplifiedWorstStatusLongPolling(); const [{ persona }] = usePreference(); @@ -58,7 +58,7 @@ export function Sidebar({ mobile }: Props): VNode { const { isTestingEnvironment } = useSettingsContext(); - const cList = useAvailableCurrencies() + const { list: cList, switchCurrency, currency } = useCurrenciesContext(); const hasMultiCurrency = cList.length > 1; return ( @@ -109,7 +109,7 @@ export function Sidebar({ mobile }: Props): VNode { }} > {cList.map((c) => ( - <option selected={c === state.currency?.id}>{c}</option> + <option selected={c === currency?.id}>{c}</option> ))} </select> </div> diff --git a/packages/merchant-backoffice-ui/src/context/currency.ts b/packages/merchant-backoffice-ui/src/context/currency.ts @@ -0,0 +1,101 @@ +/* + This file is part of GNU Taler + (C) 2021-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + CurrencySpecification, + MerchantAccountKycStatus, + TalerError, +} from "@gnu-taler/taler-util"; +import { ComponentChildren, createContext, h, VNode } from "preact"; +import { useContext } from "preact/hooks"; +import { useSessionContext } from "./session.js"; +import { useState } from "preact/hooks"; +import { + useAvailableCurrencies, + useInstanceKYCDetails, +} from "../hooks/instance.js"; + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +type CurrencyWithId = undefined | (CurrencySpecification & { id: string }); +export type Type = { + list: string[]; + currency: CurrencyWithId; + switchCurrency: (id: string) => void; +}; + +const initial: Type = { + list: [], + currency: undefined, + switchCurrency(id) {}, +}; +const Context = createContext<Type>(initial); + +export const useCurrenciesContext = (): Type => useContext(Context); + +export const CurrenciesProvider = ({ + children, +}: { + children: ComponentChildren; +}): VNode => { + const { config } = useSessionContext(); + const [currency, setCurrency] = useState< + undefined | (CurrencySpecification & { id: string }) + >(); + + const kyc = useInstanceKYCDetails(); + + const status = + kyc instanceof TalerError || !kyc || kyc.type === "fail" + ? undefined + : kyc.body; + + const readyToBeUsed = !status + ? [] + : status.kyc_data + .map((st) => + st.status === MerchantAccountKycStatus.READY + ? st.exchange_currency + : undefined, + ) + .filter((d): d is string => !!d); + + const firstCurrency = !readyToBeUsed.length ? undefined : readyToBeUsed[0]; + const fromDefault = !firstCurrency + ? undefined + : { + ...config.currencies[firstCurrency], + id: firstCurrency, + }; + + return h(Context.Provider, { + value: { + list: readyToBeUsed, + currency: currency ?? fromDefault, + switchCurrency(id: string) { + const def = config.currencies[id]; + if (!def) return; + setCurrency({ + ...def, + id: id, + }); + }, + }, + children, + }); +}; diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts @@ -23,8 +23,7 @@ import { buildCodecForObject, codecForString, codecForURL, - codecOptional, - codecOptionalDefault, + codecOptional } from "@gnu-taler/taler-util"; import { buildStorageKey, @@ -62,7 +61,6 @@ interface LoggedIn { //instance access token token: AccessToken | undefined; - currency: undefined | CurrencySpecification & { id: string }; } interface LoggedOut { @@ -71,23 +69,18 @@ interface LoggedOut { instance: string; isAdmin: boolean; token: AccessToken | undefined; - currency: undefined | CurrencySpecification & { id: string }; } interface SavedSession { backendUrl: URL; token: AccessToken | undefined; prevToken: AccessToken | undefined; - - //selected currency; - currency: string | undefined; } export const codecForSessionState = (): Codec<SavedSession> => buildCodecForObject<SavedSession>() .property("backendUrl", codecForURL()) .property("token", codecOptional(codecForString() as Codec<AccessToken>)) - .property("currency", codecOptional(codecForString())) .property( "prevToken", codecOptional(codecForString() as Codec<AccessToken>), @@ -109,7 +102,6 @@ export const defaultState = (url: URL): SavedSession => { backendUrl: url, token: undefined, prevToken: undefined, - currency: undefined, }; }; @@ -119,11 +111,6 @@ export interface SessionStateHandler { state: SessionState; /** - * change the UI to work with the selected - * supported currency - */ - switchCurrency(c: string): void; - /** * from every state to logout state */ logOut(): void; @@ -222,10 +209,6 @@ export const SessionContextProvider = ({ state: { backendUrl: state.backendUrl, token: state.token, - currency: state.currency ? { - ...rootConfig.currencies[state.currency], - id: state.currency, - } : undefined, impersonated: false, // doingImpersonation, FIXME: removing impersonation feature for 1.2 instance: currentInstance, isAdmin: currentInstance === DEFAULT_ADMIN_USERNAME, @@ -233,22 +216,12 @@ export const SessionContextProvider = ({ }, lib, config, - switchCurrency(newCurrency: string) { - if (!rootConfig.currencies[newCurrency]) return; - update({ - backendUrl: merchantUrl, - token: state.token, - prevToken: state.prevToken, - currency: newCurrency, - }); - }, logOut() { setStatus("loggedOut"); update({ backendUrl: merchantUrl, token: undefined, prevToken: undefined, - currency: state.currency, }); cleanAllCache(); }, @@ -258,7 +231,6 @@ export const SessionContextProvider = ({ backendUrl: merchantUrl, token: state.prevToken, prevToken: undefined, - currency: state.currency, }); setStatus("loggedIn"); }, @@ -270,7 +242,6 @@ export const SessionContextProvider = ({ backendUrl: baseUrl, token: undefined, prevToken: state.token, - currency: state.currency, }); setStatus("loggedIn"); cleanAllCache(); @@ -288,7 +259,6 @@ export const SessionContextProvider = ({ backendUrl, token, prevToken: state.prevToken, - currency: state.currency, }); }, getInstanceForUsername(username: string) { diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -16,6 +16,7 @@ import { AccessToken, + CurrencySpecification, getMerchantAccountKycStatusSimplified, MerchantAccountKycStatus, MerchantAccountKycStatusSimplified, @@ -30,6 +31,7 @@ import { useSessionContext } from "../context/session.js"; // Fix default import https://github.com/microsoft/TypeScript/issues/49189 import _useSWR, { mutate, SWRHook } from "swr"; import { useEffect } from "preact/hooks"; +import { useState } from "preact/hooks"; const useSWR = _useSWR as unknown as SWRHook; export function revalidateInstanceDetails() { @@ -160,7 +162,7 @@ export function useInstanceKYCSimplifiedBestStatusLongPolling() { return allKycData.reduce( (prev, cur) => { const st = getMerchantAccountKycStatusSimplified(cur.status); - if (!prev) return st; + if (prev === undefined) return st; if (st < prev) return st; return prev; }, @@ -175,9 +177,8 @@ export function useAvailableCurrencies(): string[] { kyc instanceof TalerError || !kyc || kyc.type === "fail" ? undefined : kyc.body; - if (!status) return []; - const cs = status.kyc_data.map((st) => + const cs = !status ? [] : status.kyc_data.map((st) => st.status === MerchantAccountKycStatus.READY ? st.exchange_currency : 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 @@ -59,18 +59,23 @@ import { InventoryProductForm } from "../../../../components/product/InventoryPr import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js"; import { ProductList } from "../../../../components/product/ProductList.js"; import { Tooltip } from "../../../../components/Tooltip.js"; +import { useCurrenciesContext } from "../../../../context/currency.js"; import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; import { UIElement } from "../../../../hooks/preference.js"; import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { LimitedKycActionWarning, MissingBankAccountsWarning } from "../../accounts/list/index.js"; +import { + LimitedKycActionWarning, + MissingBankAccountsWarning, +} from "../../accounts/list/index.js"; const TALER_SCREEN_ID = 42; export interface Props { onCreated: (id: string) => void; onBack?: () => void; + focus?: boolean; instanceConfig: InstanceConfig; instanceInventory: (TalerMerchantApi.ProductDetailResponse & WithId)[]; } @@ -158,6 +163,7 @@ export function CreatePage({ onCreated, onBack, instanceConfig, + focus, instanceInventory, }: Props): VNode { const { config, lib, state: session } = useSessionContext(); @@ -380,9 +386,9 @@ export function CreatePage({ TalerMerchantApi.ProductSold | undefined >(undefined); - const zero = !session.currency - ? undefined - : Amounts.zeroOfCurrency(session.currency.name); + const { currency } = useCurrenciesContext(); + + const zero = !currency ? undefined : Amounts.zeroOfCurrency(currency.name); const totalPriceInventory = inventoryList.reduce((prev, cur) => { const p = Amounts.parseOrThrow(cur.product.price); @@ -463,12 +469,13 @@ export function CreatePage({ <div class="column" /> <div class="column is-four-fifths"> {/* // FIXME: translating plural singular */} - <FragmentPersonaFlag point={UIElement.option_advanceOrderCreation} > + <FragmentPersonaFlag point={UIElement.option_advanceOrderCreation}> <InputGroup name="inventory_products" label={i18n.str`Manage products in order`} alternative={ - allProducts.length > 0 && totalPrice && ( + allProducts.length > 0 && + totalPrice && ( <p> <i18n.Translate> {allProducts.length} products with a total price of{" "} @@ -535,6 +542,7 @@ export function CreatePage({ <InputCurrency name="pricing.order_price" label={i18n.str`Order price`} + focus={true} addonAfter={ discountOrRiseRounded > 0 && (discountOrRiseRounded < 1 @@ -547,6 +555,7 @@ export function CreatePage({ ) : ( <InputCurrency name="pricing.order_price" + focus={true} label={i18n.str`Order price`} tooltip={i18n.str`Final order price`} /> 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 @@ -37,13 +37,14 @@ import { } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { Tooltip } from "../../../../components/Tooltip.js"; import { FormErrors, FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDurationSelector } from "../../../../components/form/InputDurationSelector.js"; +import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; @@ -53,12 +54,10 @@ import { ComponentPersonaFlag, FragmentPersonaFlag, } from "../../../../components/menu/SideBar.js"; +import { useCurrenciesContext } from "../../../../context/currency.js"; import { useSessionContext } from "../../../../context/session.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; import { UIElement } from "../../../../hooks/preference.js"; -import { Tooltip } from "../../../../components/Tooltip.js"; -import { InputDurationDropdown } from "../../../../components/form/InputDurationDropdown.js"; -import { useAvailableCurrencies } from "../../../../hooks/instance.js"; import { LimitedKycActionWarning, MissingBankAccountsWarning, @@ -151,15 +150,13 @@ export function CreatePage({ : undefined, }; - const cList = useAvailableCurrencies(); + const { list: cList, currency } = useCurrenciesContext(); const hasErrors = Object.keys(errors).some( (k) => (errors as Record<string, unknown>)[k] !== undefined, ); - const zero = !session.currency - ? undefined - : Amounts.zeroOfCurrency(session.currency.name); + const zero = !currency ? undefined : Amounts.zeroOfCurrency(currency.name); const contract_amount = state.amount_editable ? undefined