taler-typescript-core

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

commit 2958a58fab2bc217090be5536b02665654039481
parent 063ee8244e4304de4f14697008848dea934c98dc
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed,  5 Nov 2025 08:39:31 -0300

fix backoffice method bound and save common preference on session

Diffstat:
Mpackages/aml-backoffice-ui/src/App.tsx | 95+++++++++++++++++++++++++++++++++++++------------------------------------------
Mpackages/bank-ui/src/app.tsx | 15+++++----------
Mpackages/bank-ui/src/pages/BankFrame.tsx | 11+++++++----
Mpackages/merchant-backoffice-ui/src/Application.tsx | 115++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mpackages/merchant-backoffice-ui/src/components/SolveMFA.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/components/menu/SideBar.tsx | 7+++----
Mpackages/merchant-backoffice-ui/src/components/modal/index.tsx | 119++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mpackages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx | 10++++++----
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx | 38--------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 12++++++------
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 210++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx | 27+++++++++++++++++++--------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx | 51+++++++++++++++++----------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx | 36++++++++++++++++++++++--------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 462+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx | 47+++++++++--------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/login/index.tsx | 78+++---------------------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/settings/index.tsx | 21++++++++++++++-------
Mpackages/web-util/src/components/Button.tsx | 3+--
Mpackages/web-util/src/components/ErrorLoading.tsx | 7+++----
Mpackages/web-util/src/components/ErrorLoadingMerchant.tsx | 5++---
Mpackages/web-util/src/components/NotificationBanner.tsx | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mpackages/web-util/src/context/common-preferences.ts | 70+++++++++++++++++++++++++++++++++-------------------------------------
37 files changed, 748 insertions(+), 803 deletions(-)

diff --git a/packages/aml-backoffice-ui/src/App.tsx b/packages/aml-backoffice-ui/src/App.tsx @@ -21,7 +21,6 @@ import { } from "@gnu-taler/taler-util"; import { BrowserHashNavigationProvider, - CommonPreferenceProvider, ExchangeApiProvider, Loading, TranslationProvider, @@ -56,60 +55,56 @@ export function App(): VNode { const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL); return ( <UiSettingsProvider value={settings}> - <CommonPreferenceProvider showDebug={false}> - <TranslationProvider - source={strings} - completeness={{ - es: strings["es"].completeness ?? 0, - de: strings["de"].completeness ?? 0, + <TranslationProvider + source={strings} + completeness={{ + es: strings["es"].completeness ?? 0, + de: strings["de"].completeness ?? 0, + }} + > + <ExchangeApiProvider + baseUrl={new URL("/", baseUrl)} + frameOnError={ExchangeAmlFrame} + evictors={{ + exchange: evictExchangeSwrCache, }} + preventCompression={preventCompression} > - <CommonPreferenceProvider showDebug={false}> - <ExchangeApiProvider - baseUrl={new URL("/", baseUrl)} - frameOnError={ExchangeAmlFrame} - evictors={{ - exchange: evictExchangeSwrCache, - }} - preventCompression={preventCompression} - > - <SWRConfig - value={{ - provider: WITH_LOCAL_STORAGE_CACHE - ? localStorageProvider - : undefined, - // normally, do not revalidate - revalidateOnFocus: false, - revalidateOnReconnect: false, - revalidateIfStale: false, - revalidateOnMount: undefined, - focusThrottleInterval: undefined, + <SWRConfig + value={{ + provider: WITH_LOCAL_STORAGE_CACHE + ? localStorageProvider + : undefined, + // normally, do not revalidate + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + revalidateOnMount: undefined, + focusThrottleInterval: undefined, - // normally, do not refresh - refreshInterval: undefined, - dedupingInterval: 2000, - refreshWhenHidden: false, - refreshWhenOffline: false, + // normally, do not refresh + refreshInterval: undefined, + dedupingInterval: 2000, + refreshWhenHidden: false, + refreshWhenOffline: false, - // ignore errors - shouldRetryOnError: false, - errorRetryCount: 0, - errorRetryInterval: undefined, + // ignore errors + shouldRetryOnError: false, + errorRetryCount: 0, + errorRetryInterval: undefined, - // do not go to loading again if already has data - keepPreviousData: true, - }} - > - <BrowserHashNavigationProvider> - <UiFormsProvider> - <Routing /> - </UiFormsProvider> - </BrowserHashNavigationProvider> - </SWRConfig> - </ExchangeApiProvider> - </CommonPreferenceProvider> - </TranslationProvider> - </CommonPreferenceProvider> + // do not go to loading again if already has data + keepPreviousData: true, + }} + > + <BrowserHashNavigationProvider> + <UiFormsProvider> + <Routing /> + </UiFormsProvider> + </BrowserHashNavigationProvider> + </SWRConfig> + </ExchangeApiProvider> + </TranslationProvider> </UiSettingsProvider> ); } diff --git a/packages/bank-ui/src/app.tsx b/packages/bank-ui/src/app.tsx @@ -26,21 +26,15 @@ import { import { BankApiProvider, BrowserHashNavigationProvider, - ErrorLoading, Loading, TalerWalletIntegrationBrowserProvider, TranslationProvider, - CommonPreferenceProvider, - useCommonPreference, } from "@gnu-taler/web-util/browser"; -import { h, VNode, Fragment } from "preact"; +import { Fragment, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { SWRConfig } from "swr"; import { Routing } from "./Routing.js"; import { SettingsProvider } from "./context/settings.js"; -import { strings } from "./i18n/strings.js"; -import { BankFrame } from "./pages/BankFrame.js"; -import { UiSettings, fetchSettings } from "./settings.js"; import { revalidateAccountDetails, revalidatePublicAccounts, @@ -53,6 +47,9 @@ import { revalidateConversionRateClassDetails, revalidateConversionRateClasses, } from "./hooks/regional.js"; +import { strings } from "./i18n/strings.js"; +import { BankFrame } from "./pages/BankFrame.js"; +import { UiSettings, fetchSettings } from "./settings.js"; const WITH_LOCAL_STORAGE_CACHE = false; export function App() { const [settings, setSettings] = useState<UiSettings>(); @@ -77,9 +74,7 @@ export function App() { de: strings["de"].completeness, }} > - <CommonPreferenceProvider showDebug={false}> - <SubApp baseUrl={baseUrl} /> - </CommonPreferenceProvider> + <SubApp baseUrl={baseUrl} /> </TranslationProvider> </SettingsProvider> ); diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx @@ -32,7 +32,7 @@ import { notifyError, notifyException, useBankCoreApiContext, - useCommonPreference, + useCommonPreferences, useTranslationContext, } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, VNode, h } from "preact"; @@ -68,7 +68,7 @@ export function BankFrame({ const { i18n } = useTranslationContext(); const session = useSessionState(); const settings = useSettingsContext(); - const { showDebugInfo } = useCommonPreference(); + const [{ showDebugInfo }] = useCommonPreferences(); const [preferences, updatePreferences] = usePreferences(); const [, , resetBankState] = useBankState(); const d = useBankCoreApiContext(); @@ -80,7 +80,10 @@ export function BankFrame({ if (error) { logBugForDevelopers(error); if (error instanceof Error) { - notifyException(i18n.str`Internal error, please report. There should be more information in the console.`, error); + notifyException( + i18n.str`Internal error, please report. There should be more information in the console.`, + error, + ); } else { notifyError( i18n.str`Internal error, please report.`, @@ -251,7 +254,7 @@ function AppActivity(): VNode { const d = useBankCoreApiContext(); const onBackendActivity = !d ? undefined : d.onActivity; const cancelRequest = !d ? undefined : d.cancelRequest; - const { showDebugInfo } = useCommonPreference(); + const [{ showDebugInfo }] = useCommonPreferences(); useEffect(() => { // console.log("ASDASDS", onBackendActivity) if (!showDebugInfo) return; diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx @@ -29,7 +29,6 @@ import { } from "@gnu-taler/taler-util"; import { BrowserHashNavigationProvider, - CommonPreferenceProvider, ConfigResultFail, MerchantApiProvider, TalerWalletIntegrationBrowserProvider, @@ -44,10 +43,12 @@ import { Loading } from "./components/exception/loading.js"; import { NotificationCard } from "./components/menu/index.js"; import { SessionContextProvider } from "./context/session.js"; import { SettingsProvider } from "./context/settings.js"; +import { revalidateInstanceAccessTokens } from "./hooks/access-tokens.js"; import { revalidateBankAccountDetails, revalidateInstanceBankAccounts, } from "./hooks/bank.js"; +import { revalidateInstanceCategories } from "./hooks/category.js"; import { revalidateBackendInstances, revalidateInstanceDetails, @@ -84,8 +85,6 @@ import { buildDefaultBackendBaseURL, fetchSettings, } from "./settings.js"; -import { revalidateInstanceCategories } from "./hooks/category.js"; -import { revalidateInstanceAccessTokens } from "./hooks/access-tokens.js"; const WITH_LOCAL_STORAGE_CACHE = false; export function Application(): VNode { @@ -98,65 +97,63 @@ export function Application(): VNode { const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL); return ( <SettingsProvider value={settings}> - <CommonPreferenceProvider showDebug={false}> - <TranslationProvider - source={strings} - completeness={{ - uk: strings["uk"].completeness, - tr: strings["tr"].completeness, - // ru: strings["ru"].completeness, - sv: strings["sv"].completeness, - it: strings["it"].completeness, - fr: strings["fr"].completeness, - es: strings["es"].completeness, - de: strings["de"].completeness, + <TranslationProvider + source={strings} + completeness={{ + uk: strings["uk"].completeness, + tr: strings["tr"].completeness, + // ru: strings["ru"].completeness, + sv: strings["sv"].completeness, + it: strings["it"].completeness, + fr: strings["fr"].completeness, + es: strings["es"].completeness, + de: strings["de"].completeness, + }} + > + <MerchantApiProvider + baseUrl={new URL("./", baseUrl)} + frameOnError={OnConfigError} + evictors={{ + management: swrCacheEvictor, }} > - <MerchantApiProvider - baseUrl={new URL("./", baseUrl)} - frameOnError={OnConfigError} - evictors={{ - management: swrCacheEvictor, - }} - > - <SessionContextProvider> - <SWRConfig - value={{ - provider: WITH_LOCAL_STORAGE_CACHE - ? localStorageProvider - : undefined, - // normally, do not revalidate - revalidateOnFocus: false, - revalidateOnReconnect: false, - revalidateIfStale: false, - revalidateOnMount: undefined, - focusThrottleInterval: undefined, + <SessionContextProvider> + <SWRConfig + value={{ + provider: WITH_LOCAL_STORAGE_CACHE + ? localStorageProvider + : undefined, + // normally, do not revalidate + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + revalidateOnMount: undefined, + focusThrottleInterval: undefined, - // normally, do not refresh - refreshInterval: undefined, - dedupingInterval: 2000, - refreshWhenHidden: false, - refreshWhenOffline: false, + // normally, do not refresh + refreshInterval: undefined, + dedupingInterval: 2000, + refreshWhenHidden: false, + refreshWhenOffline: false, - // ignore errors - shouldRetryOnError: false, - errorRetryCount: 0, - errorRetryInterval: undefined, + // ignore errors + shouldRetryOnError: false, + errorRetryCount: 0, + errorRetryInterval: undefined, - // do not go to loading again if already has data - keepPreviousData: true, - }} - > - <TalerWalletIntegrationBrowserProvider> - <BrowserHashNavigationProvider> - <Routing /> - </BrowserHashNavigationProvider> - </TalerWalletIntegrationBrowserProvider> - </SWRConfig> - </SessionContextProvider> - </MerchantApiProvider> - </TranslationProvider> - </CommonPreferenceProvider> + // do not go to loading again if already has data + keepPreviousData: true, + }} + > + <TalerWalletIntegrationBrowserProvider> + <BrowserHashNavigationProvider> + <Routing /> + </BrowserHashNavigationProvider> + </TalerWalletIntegrationBrowserProvider> + </SWRConfig> + </SessionContextProvider> + </MerchantApiProvider> + </TranslationProvider> </SettingsProvider> ); } @@ -208,9 +205,7 @@ function localStorageProvider(): Map<unknown, unknown> { function OnConfigError({ state, }: { - state: - | ConfigResultFail<TalerMerchantApi.MerchantVersionResponse> - | undefined; + state: ConfigResultFail<TalerMerchantApi.MerchantVersionResponse> | undefined; }): VNode { const { i18n } = useTranslationContext(); if (!state) { diff --git a/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx b/packages/merchant-backoffice-ui/src/components/SolveMFA.tsx @@ -86,7 +86,7 @@ function SolveChallenge({ } const data = !value.code || !!errors ? undefined : { tan: value.code } const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const verify = safeFunctionHandler(lib.instance.confirmChallenge, !data ? undefined : [challenge.challenge_id, data]) + const verify = safeFunctionHandler(lib.instance.confirmChallenge.bind(lib.instance), !data ? undefined : [challenge.challenge_id, data]) verify.onSuccess = onSolved verify.onFail = (fail) => { switch (fail.case) { diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -24,11 +24,10 @@ import { MerchantAccountKycStatusSimplified, TalerError, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { useCommonPreferences, useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useSessionContext } from "../../context/session.js"; import { useInstanceKYCDetailsLongPolling } from "../../hooks/instance.js"; -import { usePreference } from "../../hooks/preference.js"; import { LangSelector } from "./LangSelector.js"; // const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; @@ -42,7 +41,7 @@ export function Sidebar({ mobile }: Props): VNode { const { i18n } = useTranslationContext(); const { state, logOut, config } = useSessionContext(); const kycStatus = useInstanceKYCDetailsLongPolling(); - const [pref] = usePreference(); + const [{ showDebugInfo }] = useCommonPreferences(); const allKycData = kycStatus !== undefined && @@ -143,7 +142,7 @@ export function Sidebar({ mobile }: Props): VNode { </span> </a> </li> - {pref.developerMode ? ( + {showDebugInfo ? ( <li> <a href={"#/tokenfamilies"} class="has-icon"> <span class="icon"> diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -27,7 +27,11 @@ import { stringifyPaytoUri, TranslatedString, } from "@gnu-taler/taler-util"; -import { ButtonBetterBulma, SafeHandlerTemplate, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + ButtonBetterBulma, + SafeHandlerTemplate, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js"; @@ -38,7 +42,7 @@ interface Props { active?: boolean; description?: string; onCancel?: () => void; - confirm?: SafeHandlerTemplate<any,any>; + confirm?: SafeHandlerTemplate<any, any>; label?: string; children?: ComponentChildren; danger?: boolean; @@ -58,7 +62,7 @@ export function ConfirmModal({ children, danger, label = "Confirm", - noCancelButton + noCancelButton, }: Props): VNode { const { i18n } = useTranslationContext(); return ( @@ -71,11 +75,7 @@ export function ConfirmModal({ <b>{description}</b> </p> )} - <button - class="delete " - aria-label="close" - onClick={onCancel} - /> + <button class="delete " aria-label="close" onClick={onCancel} /> </header> <section class="modal-card-body">{children}</section> <footer class="modal-card-foot"> @@ -95,23 +95,21 @@ export function ConfirmModal({ <i18n.Translate>{label}</i18n.Translate> </ButtonBetterBulma> </Fragment> - ) : ( - (noCancelButton ? undefined : - <button class="button " onClick={onCancel}> - <i18n.Translate>Close</i18n.Translate> - </button> - ) + ) : noCancelButton ? undefined : ( + <button class="button " onClick={onCancel}> + <i18n.Translate>Close</i18n.Translate> + </button> )} </div> </footer> </div> - {noCancelButton ? undefined : + {noCancelButton ? undefined : ( <button class="modal-close is-large " aria-label="close" onClick={onCancel} /> - } + )} </div> ); } @@ -135,10 +133,7 @@ export function ContinueModal({ <section class="modal-card-body">{children}</section> <footer class="modal-card-foot"> <div class="buttons is-right" style={{ width: "100%" }}> - <ButtonBetterBulma - class="button is-success " - onClick={confirm} - > + <ButtonBetterBulma class="button is-success " onClick={confirm}> <i18n.Translate>Continue</i18n.Translate> </ButtonBetterBulma> </div> @@ -206,10 +201,7 @@ export function ClearConfirmModal({ <button class="button " onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> </button> - <ButtonBetterBulma - class="button is-info" - onClick={confirm} - > + <ButtonBetterBulma class="button is-info" onClick={confirm}> <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> </div> @@ -226,7 +218,7 @@ export function ClearConfirmModal({ interface CompareAccountsModalProps { onCancel: () => void; - confirm: SafeHandlerTemplate<any,any>; + confirm: SafeHandlerTemplate<any, any>; formPayto: Paytos.URI | undefined; testPayto: Paytos.URI; } @@ -270,7 +262,8 @@ export function CompareAccountsModal({ > <p> <i18n.Translate> - The connection to the account info URL was successful, but the reported account information does not match the details in the form. + The connection to the account info URL was successful, but the + reported account information does not match the details in the form. </i18n.Translate> </p> <div class="table-container"> @@ -344,7 +337,9 @@ export function CompareAccountsModal({ {!!testPayto.params["receiver-postal-code"] && ( <tr> <td> - <i18n.Translate>The postal code of the account owner's address</i18n.Translate> + <i18n.Translate> + The postal code of the account owner's address + </i18n.Translate> </td> <td>{formPayto?.params["receiver-postal-code"] ?? "--"}</td> <td>{testPayto.params["receiver-postal-code"]}</td> @@ -380,31 +375,31 @@ export function ValidBankAccount({ const payto = targets[0]; const subject = payto.params["message"]; - const accountPart = payto.targetType === PaytoType.TalerBank ? ( - <Fragment> - <Row name={i18n.str`Bank host`} value={payto.host} /> - <Row name={i18n.str`Bank account`} value={payto.account} /> - </Fragment> - ) : payto.targetType === PaytoType.IBAN ? ( - <Fragment> - {payto.bic !== undefined ? ( - <Row name={i18n.str`BIC`} value={payto.bic} /> - ) : undefined} - <Row name={i18n.str`IBAN`} value={payto.iban} /> - </Fragment> - ) : undefined; + const accountPart = + payto.targetType === PaytoType.TalerBank ? ( + <Fragment> + <Row name={i18n.str`Bank host`} value={payto.host} /> + <Row name={i18n.str`Bank account`} value={payto.account} /> + </Fragment> + ) : payto.targetType === PaytoType.IBAN ? ( + <Fragment> + {payto.bic !== undefined ? ( + <Row name={i18n.str`BIC`} value={payto.bic} /> + ) : undefined} + <Row name={i18n.str`IBAN`} value={payto.iban} /> + </Fragment> + ) : undefined; const receiverName = payto.params["receiver-name"] || payto.params["receiver"] || undefined; - const receiverPostalCode = - payto.params["receiver-postal-code"] || undefined; - const receiverTown = - payto.params["receiver-town"] || undefined; + const receiverPostalCode = payto.params["receiver-postal-code"] || undefined; + const receiverTown = payto.params["receiver-town"] || undefined; - const from = origin.targetType === PaytoType.IBAN + const from = + origin.targetType === PaytoType.IBAN ? origin.iban : origin.targetType === PaytoType.TalerReserve || - origin.targetType === PaytoType.TalerReserveHttp + origin.targetType === PaytoType.TalerReserveHttp ? origin.reservePub : origin.targetType === PaytoType.Bitcoin ? `${origin.address.substring(0, 8)}...` @@ -418,11 +413,13 @@ export function ValidBankAccount({ description={i18n.str`Validate bank account: ${from}`} active onCancel={onCancel} - // onConfirm={onConfirm} + // onConfirm={onConfirm} > <p style={{ paddingTop: 0 }}> <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. + 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. </i18n.Translate> </p> <div class="table-container"> @@ -433,7 +430,8 @@ 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 this string and paste it into the 'Subject' or 'Purpose' + field in your preferred banking app or online banking website. </i18n.Translate> </td> </tr> @@ -444,7 +442,9 @@ export function ValidBankAccount({ <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 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> @@ -453,7 +453,10 @@ export function ValidBankAccount({ <Row name={i18n.str`Receiver name`} value={receiverName} /> ) : undefined} {receiverPostalCode ? ( - <Row name={i18n.str`Receiver postal code`} value={receiverPostalCode} /> + <Row + name={i18n.str`Receiver postal code`} + value={receiverPostalCode} + /> ) : undefined} {receiverTown ? ( <Row name={i18n.str`Receiver town`} value={receiverTown} /> @@ -464,7 +467,8 @@ export function ValidBankAccount({ <i18n.Translate>Step 3:</i18n.Translate> &nbsp; <i18n.Translate> - Select the smallest possible amount for a wire transfer in your preferred banking app or online banking website. + Select the smallest possible amount for a wire transfer in + your preferred banking app or online banking website. </i18n.Translate> </td> </tr> @@ -483,8 +487,11 @@ export function ValidBankAccount({ {/* <WarningBox style={{ margin: 0 }}> */} <b> <i18n.Translate> - Make sure ALL data is correct, especially the subject, and that you are choosing the bank account from which you definitely want to make the wire transfer. - You can use the copy buttons (<CopyIcon />) to avoid typos or make use of the "payto://" URI below to copy just one value. + Make sure ALL data is correct, especially the subject, and + that you are choosing the bank account from which you + definitely want to make the wire transfer. You can use the + copy buttons (<CopyIcon />) to avoid typos or make use of + the "payto://" URI below to copy just one value. </i18n.Translate> </b> {/* </WarningBox> */} @@ -494,7 +501,8 @@ export function ValidBankAccount({ <tr> <td colSpan={2} width="100%" style={{ wordBreak: "break-all" }}> <i18n.Translate> - As an alternative, in case that your bank already supports the 'PayTo URI' standard, you can use this{" "} + As an alternative, in case that your bank already supports the + 'PayTo URI' standard, you can use this{" "} <a target="_blank" rel="noreferrer" @@ -615,7 +623,6 @@ export const CopiedIcon = (): VNode => ( </svg> ); - export function LoadingModal({ onCancel }: { onCancel: () => void }): VNode { const { i18n } = useTranslationContext(); return ( diff --git a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx @@ -14,7 +14,10 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { AmountString, Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useCommonPreferences, + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks"; import { useListener } from "../../hooks/listener.js"; @@ -25,7 +28,6 @@ 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 { usePreference } from "../../hooks/preference.js"; type Entity = TalerMerchantApi.Product; @@ -142,7 +144,7 @@ interface NonInventoryProduct { export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { const { i18n } = useTranslationContext(); - const [preference] = usePreference(); + const [{ showDebugInfo }] = useCommonPreferences(); const [value, valueHandler] = useState<Partial<NonInventoryProduct>>({ taxes: [], ...initial, @@ -211,7 +213,7 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { tooltip={i18n.str`How many products will be added.`} /> - {preference.developerMode ? ( + {showDebugInfo ? ( <InputTaxes<NonInventoryProduct> name="taxes" label={i18n.str`Taxes`} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/create/index.tsx @@ -46,44 +46,6 @@ export default function AccessTokenCreatePage({ const [ok, setOk] = useState<{ token: string; expiration: AbsoluteTime }>(); - // const [doCreate, repeatCreate] = mfa.withMfaHandler( - // ({ challengeIds, onChallengeRequired }) => - // async function doCreateImpl(pwd:string, request: Entity) { - // try { - // const resp = await lib.instance.createAccessToken( - // state.instance, - // pwd, - // request, - // { challengeIds }, - // ); - // if (resp.type === "fail") { - // if (resp.case === HttpStatusCode.Accepted) { - // onChallengeRequired(resp.body); - // return; - // } - // setNotif({ - // message: i18n.str`Could not create access token`, - // type: "ERROR", - // description: resp.detail?.hint, - // }); - // return; - // } - // setOk({ - // expiration: AbsoluteTime.fromProtocolTimestamp( - // resp.body.expiration, - // ), - // token: resp.body.access_token, - // }); - // } catch (error) { - // setNotif({ - // message: i18n.str`Could not create access token`, - // type: "ERROR", - // description: error instanceof Error ? error.message : String(error), - // }); - // } - // }, - // ); - return ( <Fragment> {!ok ? undefined : ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accessTokens/list/index.tsx @@ -74,7 +74,7 @@ export default function AccessTokenListPage({ onCreate }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const deleteToken = safeFunctionHandler( - lib.instance.deleteAccessToken, + lib.instance.deleteAccessToken.bind(lib.instance), !session.token || !deleting ? undefined : [session.token, deleting.serial], ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -22,17 +22,18 @@ import { AccessToken, HttpStatusCode, - opEmptySuccess, PaytoParseError, Paytos, TalerMerchantApi, + opEmptySuccess, } from "@gnu-taler/taler-util"; import { ButtonBetterBulma, LocalNotificationBannerBulma, useChallengeHandler, + useCommonPreferences, useLocalNotificationBetter, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -48,7 +49,6 @@ import { InputToggle } from "../../../../components/form/InputToggle.js"; import { CompareAccountsModal } from "../../../../components/modal/index.js"; import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; import { useSessionContext } from "../../../../context/session.js"; -import { usePreference } from "../../../../hooks/preference.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { safeConvertURL } from "../update/UpdatePage.js"; import { TestRevenueErrorType, testRevenueAPI } from "./index.js"; @@ -65,8 +65,8 @@ interface Props { export function CreatePage({ onCreated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const [{ developerMode }] = usePreference(); - const accountAuthType = developerMode + const [{ showDebugInfo }] = useCommonPreferences(); + const accountAuthType = showDebugInfo ? ["none", "basic", "bearer"] : ["none", "basic"]; @@ -238,7 +238,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { name="payto_uri" label={i18n.str`Account details`} /> - {!developerMode ? undefined : ( + {!showDebugInfo ? undefined : ( <Fragment> <div class="message-body" style={{ marginBottom: 10 }}> <p> 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 @@ -21,21 +21,22 @@ import { AccessToken, - BankAccountDetail, HttpStatusCode, PaytoParseError, PaytoString, - PaytoUri, Paytos, - TalerError, TalerMerchantApi, - TranslatedString, - assertUnreachable, opEmptySuccess, - parsePaytoUri, - succeedOrThrow, + succeedOrThrow } from "@gnu-taler/taler-util"; -import { ButtonBetterBulma, LocalNotificationBanner, useChallengeHandler, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + ButtonBetterBulma, + LocalNotificationBanner, + useChallengeHandler, + useCommonPreferences, + useLocalNotificationBetter, + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { @@ -47,12 +48,11 @@ import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputToggle } from "../../../../components/form/InputToggle.js"; import { CompareAccountsModal } from "../../../../components/modal/index.js"; +import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; +import { useSessionContext } from "../../../../context/session.js"; import { WithId } from "../../../../declaration.js"; -import { usePreference } from "../../../../hooks/preference.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { TestRevenueErrorType, testRevenueAPI } from "../create/index.js"; -import { useSessionContext } from "../../../../context/session.js"; -import { SolveMFAChallenges } from "../../../../components/SolveMFA.js"; type Entity = TalerMerchantApi.BankAccountDetail & WithId; type FormType = TalerMerchantApi.AccountPatchDetails & { @@ -65,15 +65,11 @@ interface Props { account: Entity; } -export function UpdatePage({ - account, - onUpdated, - onBack, -}: Props): VNode { +export function UpdatePage({ account, onUpdated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const [{ developerMode }] = usePreference(); - const accountAuthType = developerMode + const [{ showDebugInfo }] = useCommonPreferences(); + const accountAuthType = showDebugInfo ? ["unedit", "none", "basic", "bearer"] : ["unedit", "none", "basic"]; @@ -117,33 +113,33 @@ export function UpdatePage({ !state.credit_facade_credentials || !state.credit_facade_url ? undefined : (undefinedIfEmpty({ - type: - replacingAccountId && + type: + replacingAccountId && // @ts-expect-error unedit is not in facade creds state.credit_facade_credentials?.type === "unedit" - ? i18n.str`Required` - : undefined, - username: - state.credit_facade_credentials?.type !== "basic" - ? undefined - : !state.credit_facade_credentials.username ? i18n.str`Required` : undefined, + username: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !state.credit_facade_credentials.username + ? i18n.str`Required` + : undefined, - token: - state.credit_facade_credentials?.type !== "bearer" - ? undefined - : !state.credit_facade_credentials.token - ? i18n.str`Required` - : undefined, + token: + state.credit_facade_credentials?.type !== "bearer" + ? undefined + : !state.credit_facade_credentials.token + ? i18n.str`Required` + : undefined, - password: - state.credit_facade_credentials?.type !== "basic" - ? undefined - : !state.credit_facade_credentials.password - ? i18n.str`Required` - : undefined, - }) as any), + password: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !state.credit_facade_credentials.password + ? i18n.str`Required` + : undefined, + }) as any), }); const hasErrors = errors !== undefined; @@ -156,72 +152,101 @@ export function UpdatePage({ | TalerMerchantApi.FacadeCredentials | undefined = credit_facade_url == undefined || - state.credit_facade_credentials === undefined + state.credit_facade_credentials === undefined ? undefined : // @ts-expect-error unedit is not in facade creds - state.credit_facade_credentials.type === "unedit" + state.credit_facade_credentials.type === "unedit" ? undefined : state.credit_facade_credentials.type === "basic" ? { - type: "basic", - password: state.credit_facade_credentials.password, - username: state.credit_facade_credentials.username, - } + type: "basic", + password: state.credit_facade_credentials.password, + username: state.credit_facade_credentials.username, + } : state.credit_facade_credentials.type === "bearer" ? { - type: "bearer", - token: state.credit_facade_credentials.token, - } + type: "bearer", + token: state.credit_facade_credentials.token, + } : { - type: "none", - }; + type: "none", + }; const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - - async function changeBankAccount(token: AccessToken, newPayto: string | undefined, id: string, data: TalerMerchantApi.AccountPatchDetails, challengeIds: string[]) { + async function changeBankAccount( + token: AccessToken, + newPayto: string | undefined, + id: string, + data: TalerMerchantApi.AccountPatchDetails, + challengeIds: string[], + ) { if (newPayto) { const details: TalerMerchantApi.AccountAddDetails = { ...data, - payto_uri: newPayto - } - const created = await lib.instance.addBankAccount(token, details, { challengeIds }) - if (created.type === "fail") return created - const deleted = await lib.instance.deleteBankAccount(token, id) - if (deleted.type === "fail") return deleted + payto_uri: newPayto, + }; + const created = await lib.instance.addBankAccount(token, details, { + challengeIds, + }); + if (created.type === "fail") return created; + const deleted = await lib.instance.deleteBankAccount(token, id); + if (deleted.type === "fail") return deleted; } else { - const resp = await lib.instance.updateBankAccount(token, id, data) - if (resp.type === "fail") return resp + const resp = await lib.instance.updateBankAccount(token, id, data); + if (resp.type === "fail") return resp; } - return opEmptySuccess() + return opEmptySuccess(); } const mfa = useChallengeHandler(); - const update = safeFunctionHandler(changeBankAccount, - !session.token ? undefined : [session.token, replacingAccountId ? state.payto_uri! : undefined, account.h_wire, { - credit_facade_credentials, credit_facade_url - }, []]) - update.onSuccess = onUpdated + const update = safeFunctionHandler( + changeBankAccount, + !session.token + ? undefined + : [ + session.token, + replacingAccountId ? state.payto_uri! : undefined, + account.h_wire, + { + credit_facade_credentials, + credit_facade_url, + }, + [], + ], + ); + update.onSuccess = onUpdated; update.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Accepted: - mfa.onChallengeRequired(fail.body) - return i18n.str`Second factor authentication required.` + mfa.onChallengeRequired(fail.body); + return i18n.str`Second factor authentication required.`; case HttpStatusCode.Unauthorized: - return i18n.str`Unauthorized` + return i18n.str`Unauthorized`; case HttpStatusCode.NotFound: - return i18n.str`Not found` + return i18n.str`Not found`; case HttpStatusCode.Conflict: - return i18n.str`Conflict` + return i18n.str`Conflict`; } - } - const repeat = update.lambda((ids: string[]) => [update.args![0], update.args![1], update.args![2], update.args![3], ids]) + }; + const repeat = update.lambda((ids: string[]) => [ + update.args![0], + update.args![1], + update.args![2], + update.args![3], + ids, + ]); const revenueAPI = !state.credit_facade_url ? undefined : new URL("./", state.credit_facade_url); - const test = safeFunctionHandler(testRevenueAPI, !revenueAPI || !state.credit_facade_url ? undefined : [revenueAPI, state.credit_facade_credentials]) + const test = safeFunctionHandler( + testRevenueAPI, + !revenueAPI || !state.credit_facade_url + ? undefined + : [revenueAPI, state.credit_facade_credentials], + ); test.onSuccess = (success) => { const match = state.payto_uri === Paytos.toFullString(success); setState({ @@ -231,20 +256,25 @@ export function UpdatePage({ if (!match) { setRevenuePayto(success); } - } + }; test.onFail = (fail) => { switch (fail.case) { - case HttpStatusCode.BadRequest: return i18n.str`Server replied with "bad request".` - case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized, check credentials.` - case HttpStatusCode.NotFound: return i18n.str`The endpoint does not seem to be a Taler Revenue API.` - case TestRevenueErrorType.CANT_VALIDATE: return i18n.str`The request was made correctly, but the bank's server did not respond with the appropriate value for 'credit_account', so we cannot confirm that it is the same bank account.` + case HttpStatusCode.BadRequest: + return i18n.str`Server replied with "bad request".`; + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized, check credentials.`; + case HttpStatusCode.NotFound: + return i18n.str`The endpoint does not seem to be a Taler Revenue API.`; + case TestRevenueErrorType.CANT_VALIDATE: + return i18n.str`The request was made correctly, but the bank's server did not respond with the appropriate value for 'credit_account', so we cannot confirm that it is the same bank account.`; case PaytoParseError.UNSUPPORTED: case PaytoParseError.COMPONENTS_LENGTH: case PaytoParseError.INVALID_TARGET_PATH: case PaytoParseError.WRONG_PREFIX: - case PaytoParseError.INCOMPLETE: return i18n.str`Unsupported type of account` + case PaytoParseError.INCOMPLETE: + return i18n.str`Unsupported type of account`; } - } + }; if (mfa.pendingChallenge) { return ( @@ -268,7 +298,10 @@ export function UpdatePage({ <span class="is-size-4"> <i18n.Translate>Account:</i18n.Translate>{" "} <b> - {succeedOrThrow(Paytos.fromString(account.payto_uri)).displayName} + { + succeedOrThrow(Paytos.fromString(account.payto_uri)) + .displayName + } </b> </span> </div> @@ -290,14 +323,14 @@ export function UpdatePage({ name="payto_uri" label={i18n.str`Account`} /> - {!developerMode ? undefined : ( + {!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. + 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> @@ -314,7 +347,8 @@ export function UpdatePage({ 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 === "none") + return i18n.str`Without authentication`; if (str === "basic") return i18n.str`With username and password`; if (str === "bearer") return i18n.str`With token`; @@ -398,7 +432,7 @@ export function UpdatePage({ payto_uri: Paytos.toFullString(revenuePayto), }); setRevenuePayto(undefined); - return opEmptySuccess() + return opEmptySuccess(); }, [])} formPayto={safeParsed} testPayto={revenuePayto} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/create/CreatePage.tsx @@ -20,7 +20,12 @@ */ import { HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { @@ -56,16 +61,22 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const data = !!errors ? undefined : state as TalerMerchantApi.CategoryCreateRequest - const create = safeFunctionHandler(lib.instance - .addCategory, !session.token || !data ? undefined : [session.token, data]) - create.onSuccess = onCreated + const data = !!errors + ? undefined + : (state as TalerMerchantApi.CategoryCreateRequest); + const create = safeFunctionHandler( + lib.instance.addCategory.bind(lib.instance), + !session.token || !data ? undefined : [session.token, data], + ); + create.onSuccess = onCreated; create.onFail = (fail) => { switch (fail.case) { - case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized` - case HttpStatusCode.NotFound: return i18n.str`Not found` + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; } - } + }; return ( <div> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/list/Table.tsx @@ -20,7 +20,13 @@ */ import { HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { ButtonBetterBulma, LocalNotificationBannerBulma, SafeHandlerTemplate, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + SafeHandlerTemplate, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { useSessionContext } from "../../../../context/session.js"; @@ -48,40 +54,18 @@ export function CardTable({ const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const remove = safeFunctionHandler(lib.instance.deleteCategory).lambda((id: string) => !session.token ? undefined! : [session.token, id]) - remove.onSuccess = () => i18n.str`Category deleted` + const remove = safeFunctionHandler( + lib.instance.deleteCategory.bind(lib.instance), + ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); + remove.onSuccess = () => i18n.str`Category deleted`; remove.onFail = (fail) => { switch (fail.case) { - case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized` - case HttpStatusCode.NotFound: return i18n.str`Not found` + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; } - - } - // async (e: TalerMerchantApi.CategoryListEntry) => { - // return lib.instance - // .deleteCategory(state.token, String(e.category_id)) - // .then((resp) => { - // if (resp.type === "ok") { - // setNotif({ - // message: i18n.str`Category delete successfully`, - // type: "SUCCESS", - // }); - // } else { - // setNotif({ - // message: i18n.str`Could not delete the category`, - // type: "ERROR", - // description: resp.detail?.hint, - // }); - // } - // }) - // .catch((error) => - // setNotif({ - // message: i18n.str`Could not delete the category`, - // type: "ERROR", - // description: error instanceof Error ? error.message : String(error), - // }), - // ); - // } + }; return ( <Fragment> <LocalNotificationBannerBulma notification={notification} /> @@ -128,13 +112,12 @@ export function CardTable({ </div> </div> </Fragment> - ); } interface TableProps { rowSelection: string[]; instances: Entity[]; - onDelete: SafeHandlerTemplate<[id: string],unknown>; + onDelete: SafeHandlerTemplate<[id: string], unknown>; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx @@ -25,7 +25,13 @@ import { TalerError, TalerMerchantApi, } from "@gnu-taler/taler-util"; -import { ButtonBetterBulma, Loading, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + ButtonBetterBulma, + Loading, + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import emptyImage from "../../../../assets/empty.png"; @@ -79,9 +85,9 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { return res.type === "fail" ? undefined : { - id: String(prod.product_id), - description: res.body.description, - }; + id: String(prod.product_id), + description: res.body.description, + }; }); }); Promise.all(ps).then((all) => { @@ -91,15 +97,19 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { }, []); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const data = state as TalerMerchantApi.CategoryCreateRequest; - const update = safeFunctionHandler(lib.instance - .updateCategory, !token ? undefined : [token, category.id, data]) - update.onSuccess = onUpdated + const update = safeFunctionHandler( + lib.instance.updateCategory.bind(lib.instance), + !token ? undefined : [token, category.id, data], + ); + update.onSuccess = onUpdated; update.onFail = (fail) => { - switch(fail.case) { - case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized` - case HttpStatusCode.NotFound: return i18n.str`Not found` + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; } - } + }; return ( <div> @@ -134,7 +144,6 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { /> </FormProvider> - <div class="buttons is-right mt-5"> {onBack && ( <button class="button" onClick={onBack}> @@ -148,8 +157,7 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { <i18n.Translate>Confirm</i18n.Translate> </ButtonBetterBulma> </div> - <ProductListSmall onSelect={() => { }} list={category.products} /> - + <ProductListSmall onSelect={() => {}} list={category.products} /> </div> </div> </section> 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 @@ -28,7 +28,12 @@ import { TalerMerchantApi, TalerProtocolDuration, } from "@gnu-taler/taler-util"; -import { ButtonBetterBulma, LocalNotificationBannerBulma, useLocalNotificationBetter, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { format, isFuture } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; @@ -178,25 +183,25 @@ export function CreatePage({ refund_deadline: !value.payments?.refund_deadline ? undefined : value.payments.pay_deadline && - Duration.cmp( - value.payments.refund_deadline, - value.payments.pay_deadline, - ) === -1 - ? i18n.str`The refund deadline cannot be earlier than the payment deadline.` - : value.payments.wire_transfer_deadline && Duration.cmp( - value.payments.wire_transfer_deadline, value.payments.refund_deadline, + value.payments.pay_deadline, ) === -1 + ? i18n.str`The refund deadline cannot be earlier than the payment deadline.` + : value.payments.wire_transfer_deadline && + Duration.cmp( + value.payments.wire_transfer_deadline, + value.payments.refund_deadline, + ) === -1 ? i18n.str`Wire transfer deadline can't be before refund deadline` : undefined, pay_deadline: !value.payments?.pay_deadline ? i18n.str`Required` : value.payments.wire_transfer_deadline && - Duration.cmp( - value.payments.wire_transfer_deadline, - value.payments.pay_deadline, - ) === -1 + Duration.cmp( + value.payments.wire_transfer_deadline, + value.payments.pay_deadline, + ) === -1 ? i18n.str`Wire transfer deadline can't be before pay deadline` : undefined, wire_transfer_deadline: !value.payments?.wire_transfer_deadline @@ -207,9 +212,9 @@ export function CreatePage({ : !value.payments?.refund_deadline ? i18n.str`Must have a refund deadline` : Duration.cmp( - value.payments.refund_deadline, - value.payments.auto_refund_deadline, - ) == -1 + value.payments.refund_deadline, + value.payments.auto_refund_deadline, + ) == -1 ? i18n.str`Auto refund can't be after refund deadline` : undefined, }), @@ -221,15 +226,15 @@ export function CreatePage({ : undefined, fulfillment_message: !!value.shipping?.fulfillment_message && - !!value.shipping?.fulfillment_url + !!value.shipping?.fulfillment_url ? i18n.str`Either fulfillment url or fulfillment message must be specified.` : undefined, fulfillment_url: !!value.shipping?.fulfillment_message && - !!value.shipping?.fulfillment_url + !!value.shipping?.fulfillment_url ? i18n.str`Either fulfillment url or fulfillment message must be specified.` : !!value.shipping?.fulfillment_url && - isInvalidUrl(value.shipping.fulfillment_url) + isInvalidUrl(value.shipping.fulfillment_url) ? i18n.str`is not a valid URL` : undefined, }), @@ -240,65 +245,76 @@ export function CreatePage({ const price = order.pricing?.order_price as AmountString | undefined; const summary = order.pricing?.summary; - const request: undefined | TalerMerchantApi.PostOrderRequest = !value.payments || !value.shipping || !price || !summary ? undefined : { - order: { - amount: price, - summary: summary, - products: productList, - extra: undefinedIfEmpty(value.extra), - pay_deadline: AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - value.payments.pay_deadline!, - ), - ), - wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - value.payments.wire_transfer_deadline!, - ), - ), - refund_deadline: AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - value.payments.refund_deadline!, - ), - ), - auto_refund: value.payments.auto_refund_deadline - ? Duration.toTalerProtocolDuration( - value.payments.auto_refund_deadline, - ) - : undefined, - max_fee: value.payments.max_fee as AmountString, - delivery_date: value.shipping.delivery_date - ? { t_s: value.shipping.delivery_date.getTime() / 1000 } - : undefined, - delivery_location: value.shipping.delivery_location, - fulfillment_url: value.shipping.fulfillment_url, - fulfillment_message: value.shipping.fulfillment_message, - minimum_age: value.payments.minimum_age, - }, - inventory_products: inventoryList.map((p) => ({ - product_id: p.product.id, - quantity: p.quantity, - })), - create_token: value.payments.createToken, - }; + const request: undefined | TalerMerchantApi.PostOrderRequest = + !value.payments || !value.shipping || !price || !summary + ? undefined + : { + order: { + amount: price, + summary: summary, + products: productList, + extra: undefinedIfEmpty(value.extra), + pay_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.pay_deadline!, + ), + ), + wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.wire_transfer_deadline!, + ), + ), + refund_deadline: AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + value.payments.refund_deadline!, + ), + ), + auto_refund: value.payments.auto_refund_deadline + ? Duration.toTalerProtocolDuration( + value.payments.auto_refund_deadline, + ) + : undefined, + max_fee: value.payments.max_fee as AmountString, + delivery_date: value.shipping.delivery_date + ? { t_s: value.shipping.delivery_date.getTime() / 1000 } + : undefined, + delivery_location: value.shipping.delivery_location, + fulfillment_url: value.shipping.fulfillment_url, + fulfillment_message: value.shipping.fulfillment_message, + minimum_age: value.payments.minimum_age, + }, + inventory_products: inventoryList.map((p) => ({ + product_id: p.product.id, + quantity: p.quantity, + })), + create_token: value.payments.createToken, + }; const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const create = safeFunctionHandler(lib.instance.createOrder, !session.token || !request ? undefined : [session.token, request]) + const create = safeFunctionHandler( + lib.instance.createOrder.bind(lib.instance), + !session.token || !request ? undefined : [session.token, request], + ); create.onSuccess = (resp) => { - onCreated(resp.order_id) - } + onCreated(resp.order_id); + }; create.onFail = (fail) => { switch (fail.case) { - case HttpStatusCode.Unauthorized: return i18n.str`Unauthorized` - case HttpStatusCode.NotFound: return i18n.str`Not found` - case HttpStatusCode.Conflict: return i18n.str`Conflict` - case HttpStatusCode.Gone: return i18n.str`Product with ID "${fail.body.product_id}" is out of stock.` - case HttpStatusCode.UnavailableForLegalReasons: return i18n.str`No exchange would accept a payment because of KYC requirements.` + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; + case HttpStatusCode.Conflict: + return i18n.str`Conflict`; + case HttpStatusCode.Gone: + return i18n.str`Product with ID "${fail.body.product_id}" is out of stock.`; + case HttpStatusCode.UnavailableForLegalReasons: + return i18n.str`No exchange would accept a payment because of KYC requirements.`; } - } + }; const addProductToTheInventoryList = ( product: TalerMerchantApi.ProductDetail & WithId, quantity: number, @@ -556,161 +572,161 @@ export function CreatePage({ {(pref.advanceOrderMode || requiresSomeTalerOptions || errors?.payments) && ( - <InputGroup - name="payments" - label={i18n.str`Taler payment options`} - tooltip={i18n.str`Override default Taler payment settings for this order`} - > - {(pref.advanceOrderMode || - noDefault_payDeadline || - errors?.payments?.pay_deadline !== undefined) && ( - <InputDuration - name="payments.pay_deadline" - label={i18n.str`Payment time`} - help={ - <DeadlineHelp duration={value.payments?.pay_deadline} /> - } - withForever - withoutClear - tooltip={i18n.str`Time for the customer to pay before the offer expires. Inventory products will be reserved until this deadline. The time starts after the order is created.`} - side={ - <span> - <button - class="button" - onClick={() => { - const c = { - ...value, - payments: { - ...(value.payments ?? {}), - pay_deadline: - instance_default.payments?.pay_deadline, - }, - }; - valueHandler(c); - }} - > - <i18n.Translate>Default</i18n.Translate> - </button> - </span> - } - /> - )} - {(pref.advanceOrderMode || - errors?.payments?.refund_deadline !== undefined) && ( - <InputDuration - name="payments.refund_deadline" - label={i18n.str`Refund time`} - help={ - <DeadlineHelp - duration={value.payments?.refund_deadline} - /> - } - withForever - withoutClear - tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`} - side={ - <span> - <button - class="button" - onClick={() => { - valueHandler({ - ...value, - payments: { - ...(value.payments ?? {}), - refund_deadline: - instance_default.payments?.refund_deadline, - }, - }); - }} - > - <i18n.Translate>Default</i18n.Translate> - </button> - </span> - } - /> - )} - {(pref.advanceOrderMode || - noDefault_wireDeadline || - errors?.payments?.wire_transfer_deadline !== undefined) && ( - <InputDuration - name="payments.wire_transfer_deadline" - label={i18n.str`Wire transfer time`} - help={ - <DeadlineHelp - duration={value.payments?.wire_transfer_deadline} - /> - } - withoutClear - withForever - tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`} - side={ - <span> - <button - class="button" - onClick={() => { - valueHandler({ - ...value, - payments: { - ...(value.payments ?? {}), - wire_transfer_deadline: - instance_default.payments - ?.wire_transfer_deadline, - }, - }); - }} - > - <i18n.Translate>Default</i18n.Translate> - </button> - </span> - } - /> - )} - {(pref.advanceOrderMode || - errors?.payments?.auto_refund_deadline !== undefined) && ( - <InputDuration - name="payments.auto_refund_deadline" - label={i18n.str`Auto-refund time`} - help={ - <DeadlineHelp - duration={value.payments?.auto_refund_deadline} - /> - } - tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} - withForever - /> - )} - - {(pref.advanceOrderMode || - errors?.payments?.max_fee !== undefined) && ( - <InputCurrency - name="payments.max_fee" - label={i18n.str`Maximum fee`} - tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} + <InputGroup + name="payments" + label={i18n.str`Taler payment options`} + tooltip={i18n.str`Override default Taler payment settings for this order`} + > + {(pref.advanceOrderMode || + noDefault_payDeadline || + errors?.payments?.pay_deadline !== undefined) && ( + <InputDuration + name="payments.pay_deadline" + label={i18n.str`Payment time`} + help={ + <DeadlineHelp duration={value.payments?.pay_deadline} /> + } + withForever + withoutClear + tooltip={i18n.str`Time for the customer to pay before the offer expires. Inventory products will be reserved until this deadline. The time starts after the order is created.`} + side={ + <span> + <button + class="button" + onClick={() => { + const c = { + ...value, + payments: { + ...(value.payments ?? {}), + pay_deadline: + instance_default.payments?.pay_deadline, + }, + }; + valueHandler(c); + }} + > + <i18n.Translate>Default</i18n.Translate> + </button> + </span> + } + /> + )} + {(pref.advanceOrderMode || + errors?.payments?.refund_deadline !== undefined) && ( + <InputDuration + name="payments.refund_deadline" + label={i18n.str`Refund time`} + help={ + <DeadlineHelp + duration={value.payments?.refund_deadline} /> - )} - {(pref.advanceOrderMode || - errors?.payments?.createToken !== undefined) && ( - <InputToggle - name="payments.createToken" - label={i18n.str`Create token`} - tooltip={i18n.str`If the order ID is easy to guess, the token will prevent other users from claiming the order.`} + } + withForever + withoutClear + tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`} + side={ + <span> + <button + class="button" + onClick={() => { + valueHandler({ + ...value, + payments: { + ...(value.payments ?? {}), + refund_deadline: + instance_default.payments?.refund_deadline, + }, + }); + }} + > + <i18n.Translate>Default</i18n.Translate> + </button> + </span> + } + /> + )} + {(pref.advanceOrderMode || + noDefault_wireDeadline || + errors?.payments?.wire_transfer_deadline !== undefined) && ( + <InputDuration + name="payments.wire_transfer_deadline" + label={i18n.str`Wire transfer time`} + help={ + <DeadlineHelp + duration={value.payments?.wire_transfer_deadline} /> - )} - {(pref.advanceOrderMode || - errors?.payments?.minimum_age !== undefined) && ( - <InputNumber - name="payments.minimum_age" - label={i18n.str`Minimum age required`} - tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} - help={ - minAgeByProducts > 0 - ? i18n.str`Minimum age defined by the products is ${minAgeByProducts}` - : i18n.str`No product with age restriction in this order` - } + } + withoutClear + withForever + tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`} + side={ + <span> + <button + class="button" + onClick={() => { + valueHandler({ + ...value, + payments: { + ...(value.payments ?? {}), + wire_transfer_deadline: + instance_default.payments + ?.wire_transfer_deadline, + }, + }); + }} + > + <i18n.Translate>Default</i18n.Translate> + </button> + </span> + } + /> + )} + {(pref.advanceOrderMode || + errors?.payments?.auto_refund_deadline !== undefined) && ( + <InputDuration + name="payments.auto_refund_deadline" + label={i18n.str`Auto-refund time`} + help={ + <DeadlineHelp + duration={value.payments?.auto_refund_deadline} /> - )} - </InputGroup> - )} + } + tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} + withForever + /> + )} + + {(pref.advanceOrderMode || + errors?.payments?.max_fee !== undefined) && ( + <InputCurrency + name="payments.max_fee" + label={i18n.str`Maximum fee`} + tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} + /> + )} + {(pref.advanceOrderMode || + errors?.payments?.createToken !== undefined) && ( + <InputToggle + name="payments.createToken" + label={i18n.str`Create token`} + tooltip={i18n.str`If the order ID is easy to guess, the token will prevent other users from claiming the order.`} + /> + )} + {(pref.advanceOrderMode || + errors?.payments?.minimum_age !== undefined) && ( + <InputNumber + name="payments.minimum_age" + label={i18n.str`Minimum age required`} + tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} + help={ + minAgeByProducts > 0 + ? i18n.str`Minimum age defined by the products is ${minAgeByProducts}` + : i18n.str`No product with age restriction in this order` + } + /> + )} + </InputGroup> + )} {(pref.advanceOrderMode || errors?.extra !== undefined) && ( <InputGroup 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,14 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AmountString, Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; import { - AmountString, - Amounts, - TalerMerchantApi -} from "@gnu-taler/taler-util"; -import { + useCommonPreferences, useLocalNotificationBetter, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { ComponentChildren, Fragment, VNode, h } from "preact"; @@ -136,40 +133,14 @@ function Table({ onLoadMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); + const [{ showDebugInfo }] = useCommonPreferences(); const [preference] = usePreference(); - const { state: session, lib } = useSessionContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const update = safeFunctionHandler(lib.instance.updateProduct); + const update = safeFunctionHandler( + lib.instance.updateProduct.bind(lib.instance), + ); update.onSuccess = () => rowSelectionHandler(undefined); - // async (id, prod) => { - // try { - // const resp = await lib.instance.updateProduct( - // state.token, - // id, - // prod, - // ); - // if (resp.type === "ok") { - // setNotif({ - // message: i18n.str`Product updated successfully`, - // type: "SUCCESS", - // }); - // } else { - // setNotif({ - // message: i18n.str`Could not update the product`, - // type: "ERROR", - // description: resp.detail?.hint, - // }); - // } - // } catch (error) { - // setNotif({ - // message: i18n.str`Could not update the product`, - // type: "ERROR", - // description: error instanceof Error ? error.message : undefined, - // }); - // } - // return; - // } return ( <div class="table-container"> {onLoadMoreBefore && ( @@ -192,7 +163,7 @@ function Table({ <th> <i18n.Translate>Price per unit</i18n.Translate> </th> - {preference.developerMode ? ( + {showDebugInfo ? ( <Fragment> <th> <i18n.Translate>Taxes</i18n.Translate> @@ -286,7 +257,7 @@ function Table({ > {isFree ? i18n.str`Free` : `${i.price} / ${i.unit}`} </td> - {preference.developerMode ? ( + {showDebugInfo ? ( <Fragment> <td onClick={() => diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx @@ -45,7 +45,7 @@ export function UpdatePage({ product, onBack, onConfirm }: Props): VNode { const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const update = safeFunctionHandler( - lib.instance.updateProduct, + lib.instance.updateProduct.bind(lib.instance), !session.token || !form ? undefined : [session.token, product.product_id, form], 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 @@ -158,7 +158,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { otp_id: state.otpId!, } - const create = safeFunctionHandler(lib.instance.addTemplate, !session.token ? undefined : [session.token, data]) + const create = safeFunctionHandler(lib.instance.addTemplate.bind(lib.instance), !session.token ? undefined : [session.token, data]) create.onSuccess = onCreated create.onFail = (fail) => { switch (fail.case) { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -77,7 +77,7 @@ export default function ListTemplates({ } } - const remove = safeFunctionHandler(lib.instance.deleteTemplate, !session.token || !deleting ? undefined : [session.token, deleting.template_id]) + const remove = safeFunctionHandler(lib.instance.deleteTemplate.bind(lib.instance), !session.token || !deleting ? undefined : [session.token, deleting.template_id]) remove.onSuccess = () => { setDeleting(null) } 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 @@ -207,7 +207,7 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { }, otp_id: state.otpId!, } - const update = safeFunctionHandler(lib.instance.updateTemplate, !session.token || !!errors ? undefined : [session.token, template.id, data]) + const update = safeFunctionHandler(lib.instance.updateTemplate.bind(lib.instance), !session.token || !!errors ? undefined : [session.token, template.id, data]) update.onSuccess = onUpdated update.onFail = (fail) => { switch (fail.case) { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx @@ -77,7 +77,7 @@ export function UsePage({ id, template, onBack, onOrderCreated }: Props): VNode } */ - const useTemplate = safeFunctionHandler(lib.instance.useTemplateCreateOrder, !!errors ? undefined : [id, state as UsingTemplateDetails]) + const useTemplate = safeFunctionHandler(lib.instance.useTemplateCreateOrder.bind(lib.instance), !!errors ? undefined : [id, state as UsingTemplateDetails]) useTemplate.onSuccess = (success) => { onOrderCreated(success.order_id) } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/create/CreatePage.tsx @@ -67,7 +67,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { }; - const create = safeFunctionHandler(lib.instance.createTokenFamily, !session.token || !!errors ? undefined : [session.token, value as Entity]); + const create = safeFunctionHandler(lib.instance.createTokenFamily.bind(lib.instance), !session.token || !!errors ? undefined : [session.token, value as Entity]); create.onSuccess = onCreated create.onFail = (fail) => { switch (fail.case) { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/list/index.tsx @@ -72,7 +72,7 @@ export default function TokenFamilyList({ onCreate, onSelect }: Props): VNode { } - const remove = safeFunctionHandler(lib.instance.deleteTokenFamily, !session.token || !deleting ? undefined : [session.token, deleting.slug]); + const remove = safeFunctionHandler(lib.instance.deleteTokenFamily.bind(lib.instance), !session.token || !deleting ? undefined : [session.token, deleting.slug]); remove.onSuccess = () => { setDeleting(null); return i18n.str`Token family has been deleted.` diff --git a/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/tokenfamilies/update/UpdatePage.tsx @@ -53,7 +53,7 @@ export function UpdatePage({ onUpdated, onBack, tokenFamily }: Props) { const hasErrors = errors !== undefined; - const update = safeFunctionHandler(lib.instance.updateTokenFamily, !session.token || !!errors ? undefined : [session.token, tokenFamily.slug, value as Entity]); + const update = safeFunctionHandler(lib.instance.updateTokenFamily.bind(lib.instance), !session.token || !!errors ? undefined : [session.token, tokenFamily.slug, value as Entity]); update.onSuccess = onUpdated update.onFail = (fail) => { switch (fail.case) { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx @@ -87,7 +87,7 @@ export function CreatePage({ accounts, onCreated, onBack }: Props): VNode { const data = state as TransferInformation; const create = safeFunctionHandler( - lib.instance.informWireTransfer, + lib.instance.informWireTransfer.bind(lib.instance), !session.token || !!errors ? undefined : [session.token, data], ); create.onSuccess = onCreated; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -83,7 +83,7 @@ export default function ListTransfer({ onCreate }: Props): VNode { ); // <LocalNotificationBannerBulma notification={notification} /> const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const remove = safeFunctionHandler(lib.instance.deleteWireTransfer) + const remove = safeFunctionHandler(lib.instance.deleteWireTransfer.bind(lib.instance)) if (!result) return <Loading />; if (result instanceof TalerError) { return <ErrorLoadingMerchant error={result} />; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx @@ -89,7 +89,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { const data = state as TalerMerchantApi.WebhookAddDetails; const create = safeFunctionHandler( - lib.instance.addWebhook, + lib.instance.addWebhook.bind(lib.instance), !session.token || hasErrors ? undefined : [session.token, data], ); create.onSuccess = onCreate diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -60,7 +60,7 @@ export function CardTable({ const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const deleteWebhook = safeFunctionHandler(lib.instance.deleteWebhook); + const deleteWebhook = safeFunctionHandler(lib.instance.deleteWebhook.bind(lib.instance)); deleteWebhook.onSuccess = () => i18n.str`Webhook deleted successfully`; deleteWebhook.onFail = (fail) => { switch (fail.case) { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx @@ -93,7 +93,7 @@ export function UpdatePage({ webhook, onConfirm, onBack }: Props): VNode { const data = state as Entity; const update = safeFunctionHandler( - lib.instance.updateWebhook, + lib.instance.updateWebhook.bind(lib.instance), !session.token || !!errors ? undefined : [session.token, webhook.id, data], ); update.onSuccess = (success) => { diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -28,11 +28,10 @@ import { } from "@gnu-taler/taler-util"; import { ButtonBetterBulma, - Notification, + LocalNotificationBannerBulma, useChallengeHandler, - useCommonPreference, useLocalNotificationBetter, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -115,31 +114,7 @@ export function LoginPage({ showCreateAccount }: Props): VNode { return ( <Fragment> - <LocalNotificationBannerBulma - notification={{ - acknowledge() {}, - message: { - title: "The operation failed." as TranslatedString, - type: "error", - description: "Unauthorized" as TranslatedString, - debug: { - detail: { - code: 2015, - hint: "The merchant refused the request due to lack of authorization.", - detail: "Check 'Authorization' header", - }, - case: 401, - when: { - t_ms: 1762199783634, - }, - args: "admin, w, []", - }, - when: { - t_ms: 1762199783634, - } as any, - }, - }} - /> + <LocalNotificationBannerBulma notification={notification} /> <div class="columns is-centered" style={{ margin: "auto" }}> <div class="column is-two-thirds "> <div class="modal-card" style={{ width: "100%", margin: 0 }}> @@ -267,50 +242,3 @@ export function LoginPage({ showCreateAccount }: Props): VNode { ); } -function LocalNotificationBannerBulma({ - notification, -}: { - notification?: Notification; -}): VNode { - const { showDebugInfo, toggle } = useCommonPreference(); - if (!notification) return <Fragment />; - const msg = notification.message; - switch (msg.type) { - case "error": - return ( - <div class="notification"> - <div class="columns is-vcentered"> - <div class="column is-12"> - <article class="message is-danger"> - <div class="message-header"> - <p>{msg.title}</p> - </div> - {msg.description && ( - <div class="message-body"> - <div>{msg.description}</div> - {!showDebugInfo ? undefined : msg.debug && ( - <pre> {JSON.stringify(msg.debug, undefined, 2)}</pre> - )} - </div> - )} - </article> - </div> - </div> - </div> - ); - case "info": - return ( - <div class="notification"> - <div class="columns is-vcentered"> - <div class="column is-12"> - <article class="message is-info"> - <div class="message-header"> - <p>{msg.title}</p> - </div> - </article> - </div> - </div> - </div> - ); - } -} diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx @@ -15,7 +15,10 @@ */ import { AbsoluteTime } from "@gnu-taler/taler-util"; -import { useCommonPreference, useLang, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useCommonPreferences, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; import { FormErrors, @@ -26,12 +29,12 @@ import { InputToggle } from "../../components/form/InputToggle.js"; import { LangSelector } from "../../components/menu/LangSelector.js"; import { Preferences, usePreference } from "../../hooks/preference.js"; -type FormType = Preferences & {developerMode: boolean} +type FormType = Preferences & { developerMode: boolean }; export function Settings({ onClose }: { onClose?: () => void }): VNode { const { i18n } = useTranslationContext(); const [value, , updateValue] = usePreference(); - const {showDebugInfo, toggle} = useCommonPreference(); + const [{ showDebugInfo }, updateCommonPref] = useCommonPreferences(); const errors: FormErrors<FormType> = {}; function valueHandler(s: (d: Partial<FormType>) => Partial<FormType>): void { @@ -39,12 +42,16 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { const v: Preferences = { advanceOrderMode: next.advanceOrderMode ?? false, advanceInstanceMode: next.advanceInstanceMode ?? false, - hideMissingAccountUntil: next.hideMissingAccountUntil ?? AbsoluteTime.never(), + hideMissingAccountUntil: + next.hideMissingAccountUntil ?? AbsoluteTime.never(), hideKycUntil: next.hideKycUntil ?? AbsoluteTime.never(), dateFormat: next.dateFormat ?? "ymd", }; - if (next.developerMode !== undefined && next.developerMode !== showDebugInfo) { - toggle() + if ( + next.developerMode !== undefined && + next.developerMode !== showDebugInfo + ) { + updateCommonPref("showDebugInfo", next.developerMode); } updateValue(v); @@ -62,7 +69,7 @@ export function Settings({ onClose }: { onClose?: () => void }): VNode { errors={errors} object={{ ...value, - developerMode: showDebugInfo + developerMode: showDebugInfo, }} valueHandler={valueHandler} > diff --git a/packages/web-util/src/components/Button.tsx b/packages/web-util/src/components/Button.tsx @@ -115,9 +115,9 @@ export function ButtonBetterBulma({ }: PropsBetter & {"data-tooltip"?: string,}): VNode { const [running, setRunning] = useState(false); return ( - <span {...rest as any}> <button class="button is-success" + {...rest as any} disabled={running || !onClick || !onClick.args} onClick={(e) => { e.preventDefault(); @@ -132,7 +132,6 @@ export function ButtonBetterBulma({ > {running ? <Wait /> : children} </button> - </span> ); } diff --git a/packages/web-util/src/components/ErrorLoading.tsx b/packages/web-util/src/components/ErrorLoading.tsx @@ -21,16 +21,15 @@ import { assertUnreachable, } from "@gnu-taler/taler-util"; import { Fragment, VNode, h } from "preact"; -import { useCommonPreference } from "../context/common-preferences.js"; -import { useTranslationContext } from "../index.browser.js"; +import { useCommonPreferences, useTranslationContext } from "../index.browser.js"; import { Attention } from "./Attention.js"; export function DebugInfo({ error }: { error: any }): VNode { const { i18n } = useTranslationContext(); - const { showDebugInfo, toggle } = useCommonPreference(); + const [{ showDebugInfo }, update] = useCommonPreferences(); return ( <div class="text-[grey]"> - <button onClick={toggle}> + <button onClick={() => update("showDebugInfo", !showDebugInfo)}> {!showDebugInfo ? ( <i18n.Translate>Show more information</i18n.Translate> ) : ( diff --git a/packages/web-util/src/components/ErrorLoadingMerchant.tsx b/packages/web-util/src/components/ErrorLoadingMerchant.tsx @@ -22,11 +22,10 @@ import { } from "@gnu-taler/taler-util"; import { Fragment, VNode, h } from "preact"; import { Attention } from "./Attention.js"; -import { useTranslationContext } from "../index.browser.js"; -import { useCommonPreference } from "../context/common-preferences.js"; +import { useCommonPreferences, useTranslationContext } from "../index.browser.js"; export function ErrorLoading({ error }: { error: TalerError }): VNode { - const { showDebugInfo, toggle } = useCommonPreference(); + const [{ showDebugInfo }, ] = useCommonPreferences(); const { i18n } = useTranslationContext(); switch (error.errorDetail.code) { ////////////////// diff --git a/packages/web-util/src/components/NotificationBanner.tsx b/packages/web-util/src/components/NotificationBanner.tsx @@ -1,5 +1,9 @@ import { Fragment, h, VNode } from "preact"; -import { DebugInfo, Notification } from "../index.browser.js"; +import { + DebugInfo, + Notification, + useCommonPreferences, +} from "../index.browser.js"; import { Attention } from "./Attention.js"; export function LocalNotificationBanner({ @@ -54,39 +58,71 @@ export function LocalNotificationBannerBulma({ }): VNode { if (!notification) return <Fragment />; const msg = notification.message; + const [{ showDebugInfo }] = useCommonPreferences(); switch (msg.type) { case "error": return ( - <div class="notification"> - <div class="columns is-vcentered"> - <div class="column is-12"> - <article class="message is-danger"> - <div class="message-header"> - <p>{msg.title}</p> - </div> - {msg.description && ( - <div class="message-body"> - <div>{msg.description}</div> - {msg.debug && ( - <pre> {JSON.stringify(msg.debug, undefined, 2)}</pre> + <div style={{ position: "relative" }}> + <div + style={{ + position: "fixed", + zIndex: 99, + top: 0, + left: 0, + right: 0, + width: "100%", + }} + > + <div class="notification"> + <div class="columns is-vcentered"> + <div class="column is-12"> + <article class="message is-danger"> + <div class="message-header"> + <p>{msg.title}</p> + <button + class="delete " + aria-label="close" + onClick={() => notification.acknowledge()} + /> + </div> + {msg.description && ( + <div class="message-body"> + <div>{msg.description}</div> + {showDebugInfo && msg.debug && ( + <pre> {JSON.stringify(msg.debug, undefined, 2)}</pre> + )} + </div> )} - </div> - )} - </article> + </article> + </div> + </div> </div> </div> </div> ); case "info": return ( - <div class="notification"> - <div class="columns is-vcentered"> - <div class="column is-12"> - <article class="message is-info"> - <div class="message-header"> - <p>{msg.title}</p> + <div style={{ position: "relative" }}> + <div + style={{ + position: "fixed", + zIndex: 99, + top: 0, + left: 0, + right: 0, + width: "100%", + }} + > + <div class="notification"> + <div class="columns is-vcentered"> + <div class="column is-12"> + <article class="message is-info"> + <div class="message-header"> + <p>{msg.title}</p> + </div> + </article> </div> - </article> + </div> </div> </div> </div> diff --git a/packages/web-util/src/context/common-preferences.ts b/packages/web-util/src/context/common-preferences.ts @@ -14,53 +14,49 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { + buildCodecForObject, + Codec, + codecForBoolean, +} from "@gnu-taler/taler-util"; import { ComponentChildren, createContext, h, VNode } from "preact"; import { useContext, useState } from "preact/hooks"; +import { buildStorageKey, useLocalStorage } from "../hooks/useLocalStorage.js"; const TALER_SCREEN_ID = 102; -interface Type { +interface Preferences { showDebugInfo: boolean; - toggle(): void; } +interface Type extends Preferences { + toggleShowDebugInfo(): void; +} + +const codecForPreferences = (): Codec<Preferences> => + buildCodecForObject<Preferences>() + .allowExtra() + .property("showDebugInfo", codecForBoolean()) + .build("CommonPreferences"); + +const COMMON_PREFERENCES_KEY = buildStorageKey( + "common-preferences", + codecForPreferences(), +); const initial: Type = { showDebugInfo: true, - toggle() {}, + toggleShowDebugInfo() {}, }; -// @ts-expect-error asd -const Context = createContext<Type>(undefined); - -export const CommonPreferenceProvider = ({ - showDebug, - children, -}: { - showDebug?: boolean; - children: ComponentChildren; -}): VNode => { - const [value, update] = useState<boolean>(showDebug ?? false); - return h(Context.Provider, { - value: { - showDebugInfo: !!value, - toggle() { - update(!value); - }, - }, - children, - }); +export function useCommonPreferences(): [ + Readonly<Preferences>, + <T extends keyof Preferences>(key: T, value: Preferences[T]) => void, +] { + const { value, update } = useLocalStorage(COMMON_PREFERENCES_KEY, initial); + + function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) { + const newValue = { ...value, [k]: v }; + update(newValue); + } + return [value, updateField]; }; - -export const useCommonPreference = (): Type => useContext(Context); - -// /** -// * User common preferences. -// * -// * @returns tuple of [state, update()] -// */ -// export function useCommonPreference(): [boolean, () => void] { -// const {showDebugInfo, toggle} = useContext(Context); - -// return [showDebugInfo, toggle]; -// } -