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:
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