diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src')
236 files changed, 6028 insertions, 9749 deletions
diff --git a/packages/merchant-backoffice-ui/src/AdminRoutes.tsx b/packages/merchant-backoffice-ui/src/AdminRoutes.tsx index 91dec09b0..b186f1408 100644 --- a/packages/merchant-backoffice-ui/src/AdminRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/AdminRoutes.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -17,6 +17,7 @@ import { h, VNode } from "preact"; import { Router, route, Route } from "preact-router"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/index.js"; +import { InstancePaths } from "./Routing.js"; export enum AdminPaths { list_instances = "/instances", @@ -42,11 +43,9 @@ export function AdminRoutes(): VNode { component={InstanceCreatePage} onBack={() => route(AdminPaths.list_instances)} onConfirm={() => { - // route(AdminPaths.list_instances); + route(InstancePaths.bank_list); }} - // onError={(error: any) => { - // }} /> </Router> ); diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index e832d3107..097e98567 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,147 +19,346 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util"; import { - ErrorType, + CacheEvictor, + TalerMerchantApi, + TalerMerchantInstanceCacheEviction, + TalerMerchantManagementCacheEviction, + assertUnreachable, + canonicalizeBaseUrl, +} from "@gnu-taler/taler-util"; +import { + BrowserHashNavigationProvider, + ConfigResultFail, + MerchantApiProvider, + TalerWalletIntegrationBrowserProvider, TranslationProvider, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { useMemo } from "preact/hooks"; -import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; +import { VNode, h } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import { SWRConfig } from "swr"; +import { Routing } from "./Routing.js"; import { Loading } from "./components/exception/loading.js"; +import { NotificationCard } from "./components/menu/index.js"; +import { SettingsProvider } from "./context/settings.js"; +import { + revalidateBankAccountDetails, + revalidateInstanceBankAccounts, +} from "./hooks/bank.js"; +import { + revalidateBackendInstances, + revalidateInstanceDetails, + revalidateManagedInstanceDetails, +} from "./hooks/instance.js"; import { - NotConnectedAppMenu, - NotificationCard -} from "./components/menu/index.js"; + revalidateInstanceOtpDevices, + revalidateOtpDeviceDetails, +} from "./hooks/otp.js"; import { - BackendContextProvider -} from "./context/backend.js"; -import { ConfigContextProvider } from "./context/config.js"; -import { useBackendConfig } from "./hooks/backend.js"; + revalidateInstanceProducts, + revalidateProductDetails, +} from "./hooks/product.js"; +import { + revalidateInstanceTemplates, + revalidateTemplateDetails, +} from "./hooks/templates.js"; +import { revalidateInstanceTransfers } from "./hooks/transfer.js"; +import { + revalidateInstanceWebhooks, + revalidateWebhookDetails, +} from "./hooks/webhooks.js"; import { strings } from "./i18n/strings.js"; +import { + MerchantUiSettings, + buildDefaultBackendBaseURL, + fetchSettings, +} from "./settings.js"; +import { + revalidateInstanceOrders, + revalidateOrderDetails, +} from "./hooks/order.js"; +import { SessionContextProvider } from "./context/session.js"; +const WITH_LOCAL_STORAGE_CACHE = false; export function Application(): VNode { + const [settings, setSettings] = useState<MerchantUiSettings>(); + useEffect(() => { + fetchSettings(setSettings); + }, []); + if (!settings) return <Loading />; + + const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL); return ( - <BackendContextProvider> - <TranslationProvider source={strings}> - <ApplicationStatusRoutes /> + <SettingsProvider value={settings}> + <TranslationProvider + source={strings} + completeness={{ + es: strings["es"].completeness, + de: strings["de"].completeness, + }} + > + <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, + + // normally, do not refresh + refreshInterval: undefined, + dedupingInterval: 2000, + refreshWhenHidden: false, + refreshWhenOffline: false, + + // 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> - </BackendContextProvider> + </SettingsProvider> ); } -/** - * Check connection testing against /config - * - * @returns - */ -function ApplicationStatusRoutes(): VNode { - const result = useBackendConfig(); - const { i18n } = useTranslationContext(); +function getInitialBackendBaseURL( + backendFromSettings: string | undefined, +): string { + /** + * For testing purpose + */ + const overrideUrl = + typeof localStorage !== "undefined" + ? localStorage.getItem("merchant-base-url") + : undefined; + let result: string; - const { currency, version } = result.ok && result.data - ? result.data - : { currency: "unknown", version: "unknown" }; - const ctx = useMemo(() => ({ currency, version }), [currency, version]); - - if (!result.ok) { - if (result.loading) return <Loading />; - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) { - return ( - <Fragment> - <NotConnectedAppMenu title="Login" /> - <NotificationCard - notification={{ - message: i18n.str`Checking the /config endpoint got authorization error`, - type: "ERROR", - description: `The /config endpoint of the backend server should be accesible`, - }} - /> - </Fragment> + if (overrideUrl) { + // testing/development path + result = overrideUrl; + } else { + // normal path + if (!backendFromSettings) { + console.error( + "ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'", ); + result = buildDefaultBackendBaseURL(); + } else { + result = backendFromSettings; } - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) { + } + try { + return canonicalizeBaseUrl(result); + } catch (e) { + // fall back + return canonicalizeBaseUrl(window.origin); + } +} + +function localStorageProvider(): Map<unknown, unknown> { + const map = new Map(JSON.parse(localStorage.getItem("app-cache") || "[]")); + + window.addEventListener("beforeunload", () => { + const appCache = JSON.stringify(Array.from(map.entries())); + localStorage.setItem("app-cache", appCache); + }); + return map; +} + +function OnConfigError({ + state, +}: { + state: ConfigResultFail<TalerMerchantApi.VersionResponse> | undefined; +}): VNode { + const { i18n } = useTranslationContext(); + if (!state) { + return ( + <i18n.Translate>checking compatibility with server...</i18n.Translate> + ); + } + switch (state.type) { + case "error": { return ( - <Fragment> - <NotConnectedAppMenu title="Error" /> - <NotificationCard - notification={{ - message: i18n.str`Could not find /config enpoint on this URL`, - type: "ERROR", - description: `Check the URL or contact the system administrator.`, - }} - /> - </Fragment> - ); - } - if (result.type === ErrorType.SERVER) { - <Fragment> - <NotConnectedAppMenu title="Error" /> <NotificationCard notification={{ - message: i18n.str`Server response with an error code`, + message: i18n.str`Contacting the server failed`, + description: state.error.message, + details: JSON.stringify(state.error.errorDetail, undefined, 2), type: "ERROR", - description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, }} /> - </Fragment>; + ); } - if (result.type === ErrorType.UNREADABLE) { - <Fragment> - <NotConnectedAppMenu title="Error" /> + case "incompatible": { + return ( <NotificationCard notification={{ - message: i18n.str`Response from server is unreadable, http status: ${result.status}`, - type: "ERROR", - description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, + message: i18n.str`The server version is not supported`, + description: i18n.str`Supported version "${state.supported}", server version "${state.result.version}".`, + type: "WARN", }} /> - </Fragment>; + ); } - return ( - <Fragment> - <NotConnectedAppMenu title="Error" /> - <NotificationCard - notification={{ - message: i18n.str`Unexpected Error`, - type: "ERROR", - description: i18n.str`Got message "${result.message}" from ${result.info?.url}`, - }} - /> - </Fragment> - ); + default: + assertUnreachable(state); } +} - const SUPPORTED_VERSION = "5:0:1" - if (result.data && !LibtoolVersion.compare( - SUPPORTED_VERSION, - result.data.version, - )?.compatible) { - return <Fragment> - <NotConnectedAppMenu title="Error" /> - <NotificationCard - notification={{ - message: i18n.str`Incompatible version`, - type: "ERROR", - description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`, - }} - /> - </Fragment> +const swrCacheEvictor = new (class + implements + CacheEvictor< + TalerMerchantManagementCacheEviction | TalerMerchantInstanceCacheEviction + > +{ + async notifySuccess( + op: + | TalerMerchantManagementCacheEviction + | TalerMerchantInstanceCacheEviction, + ) { + switch (op) { + case TalerMerchantManagementCacheEviction.CREATE_INSTANCE: { + await Promise.all([revalidateBackendInstances()]); + return; + } + case TalerMerchantManagementCacheEviction.UPDATE_INSTANCE: { + await Promise.all([revalidateManagedInstanceDetails()]); + return; + } + case TalerMerchantManagementCacheEviction.DELETE_INSTANCE: { + await Promise.all([revalidateBackendInstances()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_CURRENT_INSTANCE: { + await Promise.all([revalidateInstanceDetails()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_CURRENT_INSTANCE: { + await Promise.all([revalidateInstanceDetails()]); + return; + } + case TalerMerchantInstanceCacheEviction.CREATE_BANK_ACCOUNT: { + await Promise.all([revalidateInstanceBankAccounts()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_BANK_ACCOUNT: { + await Promise.all([revalidateBankAccountDetails()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_BANK_ACCOUNT: { + await Promise.all([revalidateInstanceBankAccounts()]); + return; + } + case TalerMerchantInstanceCacheEviction.CREATE_PRODUCT: { + await Promise.all([revalidateInstanceProducts()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT: { + await Promise.all([revalidateProductDetails()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_PRODUCT: { + await Promise.all([revalidateInstanceProducts()]); + return; + } + case TalerMerchantInstanceCacheEviction.CREATE_TRANSFER: { + await Promise.all([revalidateInstanceTransfers()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_TRANSFER: { + await Promise.all([revalidateInstanceTransfers()]); + return; + } + case TalerMerchantInstanceCacheEviction.CREATE_DEVICE: { + await Promise.all([revalidateInstanceOtpDevices()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_DEVICE: { + await Promise.all([revalidateOtpDeviceDetails()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_DEVICE: { + await Promise.all([revalidateInstanceOtpDevices()]); + return; + } + case TalerMerchantInstanceCacheEviction.CREATE_TEMPLATE: { + await Promise.all([revalidateInstanceTemplates()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_TEMPLATE: { + await Promise.all([revalidateTemplateDetails()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_TEMPLATE: { + await Promise.all([revalidateInstanceTemplates()]); + return; + } + case TalerMerchantInstanceCacheEviction.CREATE_WEBHOOK: { + await Promise.all([revalidateInstanceWebhooks()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_WEBHOOK: { + await Promise.all([revalidateWebhookDetails()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_WEBHOOK: { + await Promise.all([revalidateInstanceWebhooks()]); + return; + } + case TalerMerchantInstanceCacheEviction.CREATE_ORDER: { + await Promise.all([revalidateInstanceOrders()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_ORDER: { + await Promise.all([revalidateOrderDetails()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_ORDER: { + await Promise.all([revalidateInstanceOrders()]); + return; + } + case TalerMerchantInstanceCacheEviction.LAST: + // case TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY:{ + // await Promise.all([ + // reva + // ]) + // return + // } + // case TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY:{ + // await Promise.all([ + // ]) + // return + // } + // case TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY:{ + // await Promise.all([ + // ]) + // return + // } + } } - - return ( - <div class="has-navbar-fixed-top"> - <ConfigContextProvider value={ctx}> - <ApplicationReadyRoutes /> - </ConfigContextProvider> - </div> - ); -} +})(); diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx deleted file mode 100644 index 47177e97e..000000000 --- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx +++ /dev/null @@ -1,175 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { createHashHistory } from "history"; -import { Fragment, VNode, h } from "preact"; -import { Route, Router, route } from "preact-router"; -import { useState } from "preact/hooks"; -import { InstanceRoutes } from "./InstanceRoutes.js"; -import { - NotConnectedAppMenu, - NotYetReadyAppMenu, - NotificationCard, -} from "./components/menu/index.js"; -import { useBackendContext } from "./context/backend.js"; -import { LoginToken } from "./declaration.js"; -import { useBackendInstancesTestForAdmin } from "./hooks/backend.js"; -import { LoginPage } from "./paths/login/index.js"; -import { Settings } from "./paths/settings/index.js"; -import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; - -/** - * Check if admin against /management/instances - * @returns - */ -export function ApplicationReadyRoutes(): VNode { - const { i18n } = useTranslationContext(); - const [unauthorized, setUnauthorized] = useState(false) - const { - url: backendURL, - updateToken, - alreadyTriedLogin, - } = useBackendContext(); - - function updateLoginStatus(token: LoginToken | undefined) { - updateToken(token) - setUnauthorized(false) - } - - const result = useBackendInstancesTestForAdmin(); - - const clearTokenAndGoToRoot = () => { - route("/"); - }; - const [showSettings, setShowSettings] = useState(false) - const unauthorizedAdmin = !result.loading - && !result.ok - && result.type === ErrorType.CLIENT - && result.status === HttpStatusCode.Unauthorized; - - if (!alreadyTriedLogin && !result.ok) { - return ( - <Fragment> - <NotConnectedAppMenu title="Welcome!" /> - <LoginPage onConfirm={updateToken} /> - </Fragment> - ); - } - - if (showSettings) { - return <Fragment> - <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> - <Settings onClose={() => setShowSettings(false)} /> - </Fragment> - } - - if (result.loading) { - return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." isPasswordOk={false} />; - } - - let admin = result.ok || unauthorizedAdmin; - let instanceNameByBackendURL: string | undefined; - - if (!admin) { - // * the testing against admin endpoint failed and it's not - // an authorization problem - // * merchant backend will return this SPA under the main - // endpoint or /instance/<id> endpoint - // => trying to infer the instance id - const path = new URL(backendURL).pathname; - const match = INSTANCE_ID_LOOKUP.exec(path); - if (!match || !match[1]) { - // this should be rare because - // query to /config is ok but the URL - // does not match our pattern - return ( - <Fragment> - <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> - <NotificationCard - notification={{ - message: i18n.str`Couldn't access the server.`, - description: i18n.str`Could not infer instance id from url ${backendURL}`, - type: "ERROR", - }} - /> - {/* <ConnectionPage onConfirm={changeBackend} /> */} - </Fragment> - ); - } - - instanceNameByBackendURL = match[1]; - } - - if (unauthorized || unauthorizedAdmin) { - return <Fragment> - <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> - <NotificationCard - notification={{ - message: i18n.str`Access denied`, - description: i18n.str`Check your token is valid`, - type: "ERROR", - }} - /> - <LoginPage onConfirm={updateLoginStatus} /> - </Fragment> - } - - const history = createHashHistory(); - return ( - <Router history={history}> - <Route - default - component={DefaultMainRoute} - admin={admin} - onUnauthorized={() => setUnauthorized(true)} - onLoginPass={() => { - setUnauthorized(false) - }} - instanceNameByBackendURL={instanceNameByBackendURL} - /> - </Router> - ); -} - -function DefaultMainRoute({ - instance, - admin, - onUnauthorized, - onLoginPass, - instanceNameByBackendURL, - url, //from preact-router -}: any): VNode { - const [instanceName, setInstanceName] = useState( - instanceNameByBackendURL || instance || "default", - ); - - return ( - <InstanceRoutes - admin={admin} - path={url} - onUnauthorized={onUnauthorized} - onLoginPass={onLoginPass} - id={instanceName} - setInstanceName={setInstanceName} - /> - ); -} diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx index a82e96c14..665137415 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -20,67 +20,59 @@ */ import { - useTranslationContext, - HttpError, - ErrorType, -} from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; -import { Fragment, FunctionComponent, h, VNode } from "preact"; -import { Route, route, Router } from "preact-router"; -import { useEffect, useMemo, useState } from "preact/hooks"; + AbsoluteTime, + TalerError, + TranslatedString, +} from "@gnu-taler/taler-util"; +import { urlPattern, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { createHashHistory } from "history"; +import { Fragment, VNode, h } from "preact"; +import { Route, Router, route } from "preact-router"; +import { useEffect, useErrorBoundary, useState } from "preact/hooks"; import { Loading } from "./components/exception/loading.js"; -import { Menu, NotificationCard } from "./components/menu/index.js"; -import { InstanceContextProvider } from "./context/instance.js"; import { - useBackendDefaultToken, - useBackendInstanceToken, - useSimpleLocalStorage, -} from "./hooks/index.js"; + Menu, + NotConnectedAppMenu, + NotificationCard, +} from "./components/menu/index.js"; +import { useSessionContext } from "./context/session.js"; +import { useInstanceBankAccounts } from "./hooks/bank.js"; import { useInstanceKYCDetails } from "./hooks/instance.js"; +import { usePreference } from "./hooks/preference.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/index.js"; -import TokenPage from "./paths/instance/token/index.js"; +import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; +import BankAccountListPage from "./paths/instance/accounts/list/index.js"; +import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; import ListKYCPage from "./paths/instance/kyc/list/index.js"; import OrderCreatePage from "./paths/instance/orders/create/index.js"; import OrderDetailsPage from "./paths/instance/orders/details/index.js"; import OrderListPage from "./paths/instance/orders/list/index.js"; +import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js"; +import ValidatorListPage from "./paths/instance/otp_devices/list/index.js"; +import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js"; import ProductCreatePage from "./paths/instance/products/create/index.js"; import ProductListPage from "./paths/instance/products/list/index.js"; import ProductUpdatePage from "./paths/instance/products/update/index.js"; -import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; -import BankAccountListPage from "./paths/instance/accounts/list/index.js"; -import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; -import ReservesCreatePage from "./paths/instance/reserves/create/index.js"; -import ReservesDetailsPage from "./paths/instance/reserves/details/index.js"; -import ReservesListPage from "./paths/instance/reserves/list/index.js"; import TemplateCreatePage from "./paths/instance/templates/create/index.js"; -import TemplateUsePage from "./paths/instance/templates/use/index.js"; -import TemplateQrPage from "./paths/instance/templates/qr/index.js"; import TemplateListPage from "./paths/instance/templates/list/index.js"; +import TemplateQrPage from "./paths/instance/templates/qr/index.js"; import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; -import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; -import WebhookListPage from "./paths/instance/webhooks/list/index.js"; -import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; -import ValidatorCreatePage from "./paths/instance/otp_devices/create/index.js"; -import ValidatorListPage from "./paths/instance/otp_devices/list/index.js"; -import ValidatorUpdatePage from "./paths/instance/otp_devices/update/index.js"; - -import TokenFamilyCreatePage from "./paths/instance/tokenfamilies/create/index.js"; -import TokenFamilyListPage from "./paths/instance/tokenfamilies/list/index.js"; -import TokenFamilyUpdatePage from "./paths/instance/tokenfamilies/update/index.js"; - +import TemplateUsePage from "./paths/instance/templates/use/index.js"; +import TokenPage from "./paths/instance/token/index.js"; import TransferCreatePage from "./paths/instance/transfers/create/index.js"; import TransferListPage from "./paths/instance/transfers/list/index.js"; import InstanceUpdatePage, { AdminUpdate as InstanceAdminUpdatePage, Props as InstanceUpdatePageProps, } from "./paths/instance/update/index.js"; +import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; +import WebhookListPage from "./paths/instance/webhooks/list/index.js"; +import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; import { LoginPage } from "./paths/login/index.js"; -import NotFoundPage from "./paths/notfound/index.js"; -import { Notification } from "./utils/types.js"; -import { LoginToken, MerchantBackend } from "./declaration.js"; +import { NotFoundPage } from "./paths/notfound/index.js"; import { Settings } from "./paths/settings/index.js"; -import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js"; +import { Notification } from "./utils/types.js"; export enum InstancePaths { error = "/error", @@ -122,15 +114,11 @@ export enum InstancePaths { otp_devices_update = "/otp-devices/:vid/update", otp_devices_new = "/otp-devices/new", - token_family_list = "/tokenfamilies", - token_family_update = "/tokenfamilies/:slug/update", - token_family_new = "/tokenfamilies/new", - interface = "/interface", } // eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => { }; +// const noop = () => { }; export enum AdminPaths { list_instances = "/instances", @@ -138,142 +126,166 @@ export enum AdminPaths { update_instance = "/instance/:id/update", } -export interface Props { - id: string; - admin?: boolean; - path: string; - onUnauthorized: () => void; - onLoginPass: () => void; - setInstanceName: (s: string) => void; -} - -export function InstanceRoutes({ - id, - admin, - path, - // onUnauthorized, - onLoginPass, - setInstanceName, -}: Props): VNode { - const [defaultToken, updateDefaultToken] = useBackendDefaultToken(); - const [token, updateToken] = useBackendInstanceToken(id); - const { i18n } = useTranslationContext(); - - type GlobalNotifState = (Notification & { to: string }) | undefined; +export interface Props {} + +export const privatePages = { + home: urlPattern(/\/home/, () => "#/home"), + go: urlPattern(/\/home/, () => "#/home"), +}; +export const publicPages = { + home: urlPattern(/\/home/, () => "#/home"), + go: urlPattern(/\/home/, () => "#/home"), +}; + +const history = createHashHistory(); +export function Routing(_p: Props): VNode { + // const { i18n } = useTranslationContext(); + const { state } = useSessionContext(); + + type GlobalNotifState = + | (Notification & { to: string | undefined }) + | undefined; const [globalNotification, setGlobalNotification] = useState<GlobalNotifState>(undefined); - const changeToken = (token?: LoginToken) => { - if (admin) { - updateToken(token); - } else { - updateDefaultToken(token); - } - onLoginPass() - }; - // const updateLoginStatus = (url: string, token?: string) => { - // changeToken(token); - // }; - - const value = useMemo( - () => ({ id, token, admin, changeToken }), - [id, token, admin], - ); - - function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) { - return function ServerErrorRedirectToImpl( - error: HttpError<MerchantBackend.ErrorDetail>, - ) { - if (error.type === ErrorType.TIMEOUT) { - setGlobalNotification({ - message: i18n.str`The request to the backend take too long and was cancelled`, - description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`, - type: "ERROR", - to, - }); - } else { - setGlobalNotification({ - message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - details: - error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER - ? error.payload.detail - : undefined, - type: "ERROR", - to, - }); - } - return <Redirect to={to} />; - }; - } + const [error] = useErrorBoundary(); + const [preference] = usePreference(); + + const now = AbsoluteTime.now(); + + const instance = useInstanceBankAccounts(); + const accounts = + !instance || instance instanceof TalerError || instance.type === "fail" + ? undefined + : instance.body; + const shouldWarnAboutMissingBankAccounts = + !state.isAdmin && + accounts !== undefined && + accounts.accounts.length < 1 && + (AbsoluteTime.isNever(preference.hideMissingAccountUntil) || + AbsoluteTime.cmp(now, preference.hideMissingAccountUntil) > 1); + + const shouldLogin = state.status === "loggedOut"; + + // function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) { + // return function ServerErrorRedirectToImpl( + // error: HttpError<TalerErrorDetail>, + // ) { + // if (error.type === ErrorType.TIMEOUT) { + // setGlobalNotification({ + // message: i18n.str`The request to the backend take too long and was cancelled`, + // description: i18n.str`Diagnostic from ${error.info.url} is "${error.message}"`, + // type: "ERROR", + // to, + // }); + // } else { + // setGlobalNotification({ + // message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, + // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + // details: + // error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER + // ? error.payload.hint + // : undefined, + // type: "ERROR", + // to, + // }); + // } + // return <Redirect to={to} />; + // }; + // } // const LoginPageAccessDeniend = onUnauthorized - const LoginPageAccessDenied = () => { - return <Fragment> - <NotificationCard - notification={{ - message: i18n.str`Access denied`, - description: i18n.str`Session expired or password changed.`, - type: "ERROR", - }} - /> - <LoginPage onConfirm={changeToken} /> - </Fragment> + // const LoginPageAccessDenied = () => { + // return ( + // <Fragment> + // <NotificationCard + // notification={{ + // message: i18n.str`Access denied`, + // description: i18n.str`Session expired or password changed.`, + // type: "ERROR", + // }} + // /> + // <LoginPage /> + // </Fragment> + // ); + // }; + // function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<unknown>) { + // return function IfAdminCreateDefaultOrImpl(props?: T) { + // if (state.isAdmin && state.instance === DEFAULT_ADMIN_USERNAME) { + // return ( + // <Fragment> + // <NotificationCard + // notification={{ + // message: i18n.str`No 'default' instance configured yet.`, + // description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`, + // type: "INFO", + // }} + // /> + // <InstanceCreatePage + // forceId={DEFAULT_ADMIN_USERNAME} + // onConfirm={() => { + // route(InstancePaths.bank_list); + // }} + // /> + // </Fragment> + // ); + // } + // if (props) { + // return <Next {...props} />; + // } + // return <Next />; + // }; + // } + + if (shouldLogin) { + return ( + <Fragment> + <NotConnectedAppMenu title="Welcome!" /> + <LoginPage /> + </Fragment> + ); } - function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) { - return function IfAdminCreateDefaultOrImpl(props?: T) { - if (admin && id === "default") { - return ( - <Fragment> - <NotificationCard - notification={{ - message: i18n.str`No 'default' instance configured yet.`, - description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`, - type: "INFO", - }} - /> - <InstanceCreatePage - forceId="default" - onConfirm={() => { - route(InstancePaths.order_list); - }} - /> - </Fragment> - ); - } - if (props) { - return <Next {...props} />; - } - return <Next />; - }; + if (shouldWarnAboutMissingBankAccounts) { + return ( + <Fragment> + <Menu /> + <BankAccountBanner /> + <BankAccountCreatePage + onConfirm={() => { + route(InstancePaths.bank_list); + }} + /> + </Fragment> + ); } - const clearTokenAndGoToRoot = () => { - route("/"); - // clear all tokens - updateToken(undefined) - updateDefaultToken(undefined) - }; - return ( - <InstanceContextProvider value={value}> - <Menu - instance={id} - admin={admin} - onShowSettings={() => { - route(InstancePaths.interface) - }} - path={path} - onLogout={clearTokenAndGoToRoot} - setInstanceName={setInstanceName} - isPasswordOk={defaultToken !== undefined} - /> + <Fragment> + <Menu /> <KycBanner /> <NotificationCard notification={globalNotification} /> + {error && ( + <NotificationCard + notification={{ + message: "Internal error, please repot", + type: "ERROR", + description: ( + <pre> + { + (error instanceof Error + ? error.stack + : String(error)) as TranslatedString + } + </pre> + ), + }} + /> + )} <Router + history={history} onChange={(e) => { const movingOutFromNotification = globalNotification && e.url !== globalNotification.to; @@ -286,7 +298,7 @@ export function InstanceRoutes({ {/** * Admin pages */} - {admin && ( + {state.isAdmin && ( <Route path={AdminPaths.list_instances} component={InstanceListPage} @@ -296,22 +308,19 @@ export function InstanceRoutes({ onUpdate={(id: string): void => { route(`/instance/${id}/update`); }} - setInstanceName={setInstanceName} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> )} - {admin && ( + {state.isAdmin && ( <Route path={AdminPaths.new_instance} component={InstanceCreatePage} onBack={() => route(AdminPaths.list_instances)} onConfirm={() => { - route(InstancePaths.order_list); + route(AdminPaths.list_instances); }} /> )} - {admin && ( + {state.isAdmin && ( <Route path={AdminPaths.update_instance} component={AdminInstanceUpdatePage} @@ -319,9 +328,6 @@ export function InstanceRoutes({ onConfirm={() => { route(AdminPaths.list_instances); }} - onUpdateError={ServerErrorRedirectTo(AdminPaths.list_instances)} - onLoadError={ServerErrorRedirectTo(AdminPaths.list_instances)} - onNotFound={NotFoundPage} /> )} {/** @@ -336,10 +342,6 @@ export function InstanceRoutes({ onConfirm={() => { route(`/`); }} - onUpdateError={noop} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> {/** * Update instance page @@ -351,11 +353,8 @@ export function InstanceRoutes({ route(`/`); }} onCancel={() => { - route(InstancePaths.order_list) + route(InstancePaths.order_list); }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> {/** * Inventory pages @@ -363,28 +362,22 @@ export function InstanceRoutes({ <Route path={InstancePaths.inventory_list} component={ProductListPage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.inventory_new); }} onSelect={(id: string) => { route(InstancePaths.inventory_update.replace(":pid", id)); }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> <Route path={InstancePaths.inventory_update} component={ProductUpdatePage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)} onConfirm={() => { route(InstancePaths.inventory_list); }} onBack={() => { route(InstancePaths.inventory_list); }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> <Route path={InstancePaths.inventory_new} @@ -402,28 +395,22 @@ export function InstanceRoutes({ <Route path={InstancePaths.bank_list} component={BankAccountListPage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.bank_new); }} onSelect={(id: string) => { route(InstancePaths.bank_update.replace(":bid", id)); }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> <Route path={InstancePaths.bank_update} component={BankAccountUpdatePage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.inventory_list)} onConfirm={() => { route(InstancePaths.bank_list); }} onBack={() => { route(InstancePaths.bank_list); }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> <Route path={InstancePaths.bank_new} @@ -447,16 +434,10 @@ export function InstanceRoutes({ onSelect={(id: string) => { route(InstancePaths.order_details.replace(":oid", id)); }} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> <Route path={InstancePaths.order_details} component={OrderDetailsPage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.order_list)} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.order_list); }} @@ -477,9 +458,6 @@ export function InstanceRoutes({ <Route path={InstancePaths.transfers_list} component={TransferListPage} - onUnauthorized={LoginPageAccessDenied} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.transfers_new); }} @@ -494,54 +472,12 @@ export function InstanceRoutes({ route(InstancePaths.transfers_list); }} /> - {/* * - * Token family pages - */} - <Route - path={InstancePaths.token_family_list} - component={TokenFamilyListPage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} - onCreate={() => { - route(InstancePaths.token_family_new); - }} - onSelect={(slug: string) => { - route(InstancePaths.token_family_update.replace(":slug", slug)); - }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - /> - <Route - path={InstancePaths.token_family_update} - component={TokenFamilyUpdatePage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.token_family_list)} - onConfirm={() => { - route(InstancePaths.token_family_list); - }} - onBack={() => { - route(InstancePaths.token_family_list); - }} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - /> - <Route - path={InstancePaths.token_family_new} - component={TokenFamilyCreatePage} - onConfirm={() => { - route(InstancePaths.token_family_list); - }} - onBack={() => { - route(InstancePaths.token_family_list); - }} - /> {/** * Webhooks pages */} <Route path={InstancePaths.webhooks_list} component={WebhookListPage} - onUnauthorized={LoginPageAccessDenied} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.webhooks_new); }} @@ -555,9 +491,6 @@ export function InstanceRoutes({ onConfirm={() => { route(InstancePaths.webhooks_list); }} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.webhooks_list)} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.webhooks_list); }} @@ -578,9 +511,6 @@ export function InstanceRoutes({ <Route path={InstancePaths.otp_devices_list} component={ValidatorListPage} - onUnauthorized={LoginPageAccessDenied} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.otp_devices_new); }} @@ -594,9 +524,6 @@ export function InstanceRoutes({ onConfirm={() => { route(InstancePaths.otp_devices_list); }} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.otp_devices_list)} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.otp_devices_list); }} @@ -617,9 +544,6 @@ export function InstanceRoutes({ <Route path={InstancePaths.templates_list} component={TemplateListPage} - onUnauthorized={LoginPageAccessDenied} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} onCreate={() => { route(InstancePaths.templates_new); }} @@ -639,9 +563,6 @@ export function InstanceRoutes({ onConfirm={() => { route(InstancePaths.templates_list); }} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.templates_list); }} @@ -662,9 +583,6 @@ export function InstanceRoutes({ onOrderCreated={(id: string) => { route(InstancePaths.order_details.replace(":oid", id)); }} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.templates_list); }} @@ -672,50 +590,11 @@ export function InstanceRoutes({ <Route path={InstancePaths.templates_qr} component={TemplateQrPage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.templates_list)} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} onBack={() => { route(InstancePaths.templates_list); }} /> - {/** - * reserves pages - */} - <Route - path={InstancePaths.reserves_list} - component={ReservesListPage} - onUnauthorized={LoginPageAccessDenied} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.settings)} - onSelect={(id: string) => { - route(InstancePaths.reserves_details.replace(":rid", id)); - }} - onCreate={() => { - route(InstancePaths.reserves_new); - }} - /> - <Route - path={InstancePaths.reserves_details} - component={ReservesDetailsPage} - onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.reserves_list)} - onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onBack={() => { - route(InstancePaths.reserves_list); - }} - /> - <Route - path={InstancePaths.reserves_new} - component={ReservesCreatePage} - onConfirm={() => { - route(InstancePaths.reserves_list); - }} - onBack={() => { - route(InstancePaths.reserves_list); - }} - /> <Route path={InstancePaths.kyc} component={ListKYCPage} /> <Route path={InstancePaths.interface} component={Settings} /> {/** @@ -724,7 +603,7 @@ export function InstanceRoutes({ <Route path="/loading" component={Loading} /> <Route default component={NotFoundPage} /> </Router> - </InstanceContextProvider> + </Fragment> ); } @@ -739,74 +618,113 @@ function AdminInstanceUpdatePage({ id, ...rest }: { id: string } & InstanceUpdatePageProps): VNode { - const [token, changeToken] = useBackendInstanceToken(id); - const updateLoginStatus = (token?: LoginToken): void => { - changeToken(token); - }; - const value = useMemo( - () => ({ id, token, admin: true, changeToken }), - [id, token], - ); - const { i18n } = useTranslationContext(); + // const { i18n } = useTranslationContext(); return ( - <InstanceContextProvider value={value}> + <Fragment> <InstanceAdminUpdatePage {...rest} instanceId={id} - onLoadError={(error: HttpError<MerchantBackend.ErrorDetail>) => { - const notif = - error.type === ErrorType.TIMEOUT - ? { - message: i18n.str`The request to the backend take too long and was cancelled`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - type: "ERROR" as const, - } - : { - message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - details: - error.type === ErrorType.CLIENT || - error.type === ErrorType.SERVER - ? error.payload.detail - : undefined, - type: "ERROR" as const, - }; - return ( - <Fragment> - <NotificationCard notification={notif} /> - <LoginPage onConfirm={updateLoginStatus} /> - </Fragment> - ); - }} - onUnauthorized={() => { - return ( - <Fragment> - <NotificationCard - notification={{ - message: i18n.str`Access denied`, - description: i18n.str`The access token provided is invalid`, - type: "ERROR", - }} - /> - <LoginPage onConfirm={updateLoginStatus} /> - </Fragment> - ); - }} + // onLoadError={(error: HttpError<TalerErrorDetail>) => { + // const notif = + // error.type === ErrorType.TIMEOUT + // ? { + // message: i18n.str`The request to the backend take too long and was cancelled`, + // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + // type: "ERROR" as const, + // } + // : { + // message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, + // description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + // details: + // error.type === ErrorType.CLIENT || + // error.type === ErrorType.SERVER + // ? error.payload.hint + // : undefined, + // type: "ERROR" as const, + // }; + // return ( + // <Fragment> + // <NotificationCard notification={notif} /> + // <LoginPage /> + // </Fragment> + // ); + // }} + // onUnauthorized={() => { + // return ( + // <Fragment> + // <NotificationCard + // notification={{ + // message: i18n.str`Access denied`, + // description: i18n.str`The access token provided is invalid`, + // type: "ERROR", + // }} + // /> + // <LoginPage /> + // </Fragment> + // ); + // }} /> - </InstanceContextProvider> + </Fragment> + ); +} + +function BankAccountBanner(): VNode { + const { i18n } = useTranslationContext(); + + const [, updatePref] = usePreference(); + const now = AbsoluteTime.now(); + const oneDay = { d_ms: 1000 * 60 * 60 * 24 }; + const tomorrow = AbsoluteTime.addDuration(now, oneDay); + + return ( + <NotificationCard + notification={{ + type: "INFO", + message: i18n.str`You need to associate a bank account to receive revenue.`, + description: ( + <div> + <p> + <i18n.Translate> + Without this the merchant backend will refuse to create new + orders. + </i18n.Translate> + </p> + <div class="buttons is-right"> + <button + class="button" + onClick={() => updatePref("hideMissingAccountUntil", tomorrow)} + > + <i18n.Translate>Hide for today</i18n.Translate> + </button> + </div> + </div> + ), + }} + /> ); } function KycBanner(): VNode { const kycStatus = useInstanceKYCDetails(); const { i18n } = useTranslationContext(); - const [settings] = useSettings(); - const today = format(new Date(), dateFormatForSettings(settings)); - const [lastHide, setLastHide] = useSimpleLocalStorage("kyc-last-hide"); - const hasBeenHidden = today === lastHide; - const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect"; - if (hasBeenHidden || !needsToBeShown) return <Fragment />; + // const today = format(new Date(), dateFormatForSettings(settings)); + const [prefs, updatePref] = usePreference(); + + const now = AbsoluteTime.now(); + + const needsToBeShown = + kycStatus !== undefined && + !(kycStatus instanceof TalerError) && + kycStatus.type === "ok" && + !!kycStatus.body; + + const hidden = AbsoluteTime.cmp(now, prefs.hideKycUntil) < 1; + if (hidden || !needsToBeShown) return <Fragment />; + + const oneDay = { d_ms: 1000 * 60 * 60 * 24 }; + const tomorrow = AbsoluteTime.addDuration(now, oneDay); + return ( <NotificationCard notification={{ @@ -815,11 +733,16 @@ function KycBanner(): VNode { description: ( <div> <p> - Some transfer are on hold until a KYC process is completed. Go to - the KYC section in the left panel for more information + <i18n.Translate> + Some transfer are on hold until a KYC process is completed. Go + to the KYC section in the left panel for more information + </i18n.Translate> </p> <div class="buttons is-right"> - <button class="button" onClick={() => setLastHide(today)}> + <button + class="button" + onClick={() => updatePref("hideKycUntil", tomorrow)} + > <i18n.Translate>Hide for today</i18n.Translate> </button> </div> diff --git a/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx new file mode 100644 index 000000000..b1d1cac66 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/components/ErrorLoadingMerchant.tsx @@ -0,0 +1,146 @@ +/* +/* + This file is part of GNU Taler + (C) 2022 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 { TalerError, TalerErrorCode, assertUnreachable } from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; +import { NotificationCard } from "./menu/index.js"; + +/** + * equivalent to ErrorLoading for merchant-backoffice which uses notification-card + * @param param0 + * @returns + */ +export function ErrorLoadingMerchant({ error, showDetail }: { error: TalerError, showDetail?: boolean }): VNode { + const { i18n } = useTranslationContext() + switch (error.errorDetail.code) { + ////////////////// + // Every error that can be produce in a Http Request + ////////////////// + case TalerErrorCode.GENERIC_TIMEOUT: { + if (error.hasErrorCode(TalerErrorCode.GENERIC_TIMEOUT)) { + const { requestMethod, requestUrl, timeoutMs } = error.errorDetail + return <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`The request reached a timeout, check your connection.`, + description: error.message, + details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2) + }} /> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR: { + if (error.hasErrorCode(TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR)) { + const { requestMethod, requestUrl, timeoutMs } = error.errorDetail + return <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`The request was cancelled.`, + description: error.message, + details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2) + }} /> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT: { + if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT)) { + const { requestMethod, requestUrl, timeoutMs } = error.errorDetail + return <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`The request reached a timeout, check your connection.`, + description: error.message, + details: JSON.stringify({ requestMethod, requestUrl, timeoutMs }, undefined, 2) + }} /> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED: { + if (error.hasErrorCode(TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED)) { + const { requestMethod, requestUrl, throttleStats } = error.errorDetail + return <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`A lot of request were made to the same server and this action was throttled.`, + description: error.message, + details: JSON.stringify({ requestMethod, requestUrl, throttleStats }, undefined, 2) + }} /> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE: { + if (error.hasErrorCode(TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE)) { + const { requestMethod, requestUrl, httpStatusCode, validationError } = error.errorDetail + return <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`The response of the request is malformed.`, + description: error.message, + details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, validationError }, undefined, 2) + }} /> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_NETWORK_ERROR: { + if (error.hasErrorCode(TalerErrorCode.WALLET_NETWORK_ERROR)) { + const { requestMethod, requestUrl } = error.errorDetail + return <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`Could not complete the request due to a network problem.`, + description: error.message, + details: JSON.stringify({ requestMethod, requestUrl }, undefined, 2) + }} /> + } + assertUnreachable(1 as never) + } + case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { + if (error.hasErrorCode(TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR)) { + const { requestMethod, requestUrl, httpStatusCode, errorResponse } = error.errorDetail + return <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`Unexpected request error.`, + description: error.message, + details: JSON.stringify({ requestMethod, requestUrl, httpStatusCode, errorResponse }, undefined, 2) + }} /> + } + assertUnreachable(1 as never) + } + ////////////////// + // Every other error + ////////////////// + // case TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR: { + // return <Attention type="danger" title={i18n.str``}> + // </Attention> + // } + ////////////////// + // Default message for unhandled case + ////////////////// + default: { + return <NotificationCard + notification={{ + type: "ERROR", + message: i18n.str`Unexpected error.`, + description: error.message, + details: JSON.stringify(error.errorDetail, undefined, 2) + }} /> + } + } +} + diff --git a/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx b/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx index b1fc33877..58c10e7d7 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/AsyncButton.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/exception/QR.tsx b/packages/merchant-backoffice-ui/src/components/exception/QR.tsx index c9340ea76..246ce0229 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/QR.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/QR.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/exception/loading.tsx b/packages/merchant-backoffice-ui/src/components/exception/loading.tsx index a043b81eb..5c249f79d 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/loading.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/loading.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx index 0d53c4d08..a5f3c1d2f 100644 --- a/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/FormProvider.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/Input.tsx b/packages/merchant-backoffice-ui/src/components/form/Input.tsx index c1ddcb064..899061c35 100644 --- a/packages/merchant-backoffice-ui/src/components/form/Input.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/Input.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx index 4ed4c4b28..b0b9eaefc 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputArray.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx b/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx index f79e16c07..bdb2feb6b 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputBoolean.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx index b02354d7c..11396b88e 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -18,11 +18,12 @@ * * @author Sebastian Javier Marchano (sebasjm) */ +import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, h, VNode } from "preact"; -import { useConfigContext } from "../../context/config.js"; -import { Amount } from "../../declaration.js"; import { InputWithAddon } from "./InputWithAddon.js"; import { InputProps } from "./useField.js"; +import { AmountString } from "@gnu-taler/taler-util"; +import { useSessionContext } from "../../context/session.js"; export interface Props<T> extends InputProps<T> { expand?: boolean; @@ -43,7 +44,7 @@ export function InputCurrency<T>({ children, side, }: Props<keyof T>): VNode { - const config = useConfigContext(); + const { config } = useSessionContext(); return ( <InputWithAddon<T> name={name} @@ -57,7 +58,7 @@ export function InputCurrency<T>({ addonAfter={addonAfter} inputType="number" expand={expand} - toStr={(v?: Amount) => v?.split(":")[1] || ""} + toStr={(v?: AmountString) => v?.split(":")[1] || ""} fromStr={(v: string) => (!v ? undefined : `${config.currency}:${v}`)} inputExtra={{ min: 0 }} > diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx index a398629dc..812505f6a 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -24,7 +24,7 @@ import { ComponentChildren, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { DatePicker } from "../picker/DatePicker.js"; import { InputProps, useField } from "./useField.js"; -import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js"; +import { dateFormatForSettings, usePreference } from "../../hooks/preference.js"; export interface Props<T> extends InputProps<T> { readonly?: boolean; @@ -47,7 +47,7 @@ export function InputDate<T>({ }: Props<keyof T>): VNode { const [opened, setOpened] = useState(false); const { i18n } = useTranslationContext(); - const [settings] = useSettings() + const [settings] = usePreference() const { error, required, value, onChange } = useField<T>(name); diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx index ef4df1df4..ad3cb0e32 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -20,16 +20,19 @@ */ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { formatDuration, intervalToDuration } from "date-fns"; -import { h, VNode } from "preact"; +import { ComponentChildren, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { SimpleModal } from "../modal/index.js"; import { DurationPicker } from "../picker/DurationPicker.js"; import { InputProps, useField } from "./useField.js"; +import { Duration } from "@gnu-taler/taler-util"; export interface Props<T> extends InputProps<T> { expand?: boolean; readonly?: boolean; withForever?: boolean; + side?: ComponentChildren; + withoutClear?: boolean; } export function InputDuration<T>({ @@ -41,19 +44,25 @@ export function InputDuration<T>({ help, readonly, withForever, + withoutClear, + side, }: Props<keyof T>): VNode { const [opened, setOpened] = useState(false); const { i18n } = useTranslationContext(); - const { error, required, value, onChange } = useField<T>(name); + const { error, required, value: anyValue, onChange } = useField<T>(name); let strValue = ""; + const value: Duration = anyValue if (!value) { strValue = ""; - } else if (value.d_us === "forever") { + } else if (value.d_ms === "forever") { strValue = i18n.str`forever`; } else { + if (value.d_ms === undefined) { + throw Error(`assertion error: duration should have a d_ms but got '${JSON.stringify(value)}'`) + } strValue = formatDuration( - intervalToDuration({ start: 0, end: value.d_us / 1000 }), + intervalToDuration({ start: 0, end: value.d_ms }), { locale: { formatDistance: (name, value) => { @@ -87,7 +96,7 @@ export function InputDuration<T>({ return ( <div class="field is-horizontal"> - <div class="field-label is-normal"> + <div class="field-label is-normal is-flex-grow-3"> <label class="label"> {label} {tooltip && ( @@ -97,72 +106,80 @@ export function InputDuration<T>({ )} </label> </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <div class="field has-addons"> - <p class={expand ? "control is-expanded " : "control "}> - <input - class="input" - type="text" - readonly - value={strValue} - placeholder={placeholder} + + <div class="is-flex-grow-3"> + <div class="field-body "> + <div class="field"> + <div class="field has-addons"> + <p class={expand ? "control is-expanded " : "control "}> + <input + class="input" + type="text" + readonly + value={strValue} + placeholder={placeholder} + onClick={() => { + if (!readonly) setOpened(true); + }} + /> + {required && ( + <span class="icon has-text-danger is-right"> + <i class="mdi mdi-alert" /> + </span> + )} + </p> + <div + class="control" onClick={() => { if (!readonly) setOpened(true); }} - /> - {required && ( - <span class="icon has-text-danger is-right"> - <i class="mdi mdi-alert" /> - </span> - )} - {help} - </p> - <div - class="control" - onClick={() => { - if (!readonly) setOpened(true); - }} - > - <a class="button is-static"> - <span class="icon"> - <i class="mdi mdi-clock" /> - </span> - </a> + > + <a class="button is-static"> + <span class="icon"> + <i class="mdi mdi-clock" /> + </span> + </a> + </div> </div> + {error && <p class="help is-danger">{error}</p>} </div> - {error && <p class="help is-danger">{error}</p>} + {withForever && ( + <span data-tooltip={i18n.str`change value to never`}> + <button + class="button is-info mr-3" + onClick={() => onChange({ d_ms: "forever" } as any)} + > + <i18n.Translate>forever</i18n.Translate> + </button> + </span> + )} + {!readonly && !withoutClear && ( + <span data-tooltip={i18n.str`change value to empty`}> + <button + class="button is-info " + onClick={() => onChange(undefined as any)} + > + <i18n.Translate>clear</i18n.Translate> + </button> + </span> + )} + {side} </div> - {withForever && ( - <span data-tooltip={i18n.str`change value to never`}> - <button - class="button is-info mr-3" - onClick={() => onChange({ d_us: "forever" } as any)} - > - <i18n.Translate>forever</i18n.Translate> - </button> - </span> - )} - {!readonly && ( - <span data-tooltip={i18n.str`change value to empty`}> - <button - class="button is-info " - onClick={() => onChange(undefined as any)} - > - <i18n.Translate>clear</i18n.Translate> - </button> - </span> - )} + <span> + {help} + </span> </div> + + {opened && ( <SimpleModal onCancel={() => setOpened(false)}> <DurationPicker days hours minutes - value={!value || value.d_us === "forever" ? 0 : value.d_us} + value={!value || value.d_ms === "forever" ? 0 : value.d_ms} onChange={(v) => { - onChange({ d_us: v } as any); + onChange({ d_ms: v } as any); }} /> </SimpleModal> diff --git a/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx b/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx index b5e0bd52b..92b9e8b16 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputGroup.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx index b024e2c6b..d284b476f 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputImage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx b/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx index a2fc8113e..d4b13d555 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputLocation.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx index 3b5df1474..38444b85d 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputNumber.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -53,8 +53,9 @@ export function InputNumber<T>({ help={help} tooltip={tooltip} inputExtra={{ min: 0 }} - children={children} side={side} - /> + > + {children} + </InputWithAddon> ); } diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx index 6e88e8f2c..fcecd8932 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputPayto.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx index 282e52278..cc5326bbe 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx index 32545c89a..a0c15c77c 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -18,7 +18,11 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util"; +import { + parsePaytoUri, + PaytoUriGeneric, + stringifyPaytoUri, +} from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { COUNTRY_TABLE } from "../../utils/constants.js"; @@ -71,7 +75,7 @@ function checkAddressChecksum(address: string) { return true; } -function validateBitcoin( +function validateBitcoin_path1( addr: string, i18n: ReturnType<typeof useTranslationContext>["i18n"], ): string | undefined { @@ -84,7 +88,7 @@ function validateBitcoin( return i18n.str`This is not a valid bitcoin address.`; } -function validateEthereum( +function validateEthereum_path1( addr: string, i18n: ReturnType<typeof useTranslationContext>["i18n"], ): string | undefined { @@ -98,6 +102,29 @@ function validateEthereum( } /** + * validates + * bank.com/ + * bank.com + * bank.com/path + * bank.com/path/subpath/ + */ +const DOMAIN_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+(\/[a-zA-Z0-9-.]+)*\/?$/ + +function validateTalerBank_path1( + addr: string, + i18n: ReturnType<typeof useTranslationContext>["i18n"], +): string | undefined { + console.log(addr, DOMAIN_REGEX.test(addr)) + try { + const valid = DOMAIN_REGEX.test(addr); + if (valid) return undefined; + } catch (e) { + console.log(e); + } + return i18n.str`This is not a valid host.`; +} + +/** * An IBAN is validated by converting it into an integer and performing a * basic mod-97 operation (as described in ISO 7064) on it. * If the IBAN is valid, the remainder equals 1. @@ -111,7 +138,7 @@ function validateEthereum( * If the remainder is 1, the check digit test is passed and the IBAN might be valid. * */ -function validateIBAN( +function validateIBAN_path1( iban: string, i18n: ReturnType<typeof useTranslationContext>["i18n"], ): string | undefined { @@ -178,34 +205,36 @@ export function InputPaytoForm<T>({ }: Props<keyof T>): VNode { const { value: initialValueStr, onChange } = useField<T>(name); - const initialPayto = parsePaytoUri(initialValueStr ?? "") - const paths = !initialPayto ? [] : initialPayto.targetPath.split("/") + const initialPayto = parsePaytoUri(initialValueStr ?? ""); + const paths = !initialPayto ? [] : initialPayto.targetPath.split("/"); const initialPath1 = paths.length >= 1 ? paths[0] : undefined; const initialPath2 = paths.length >= 2 ? paths[1] : undefined; - const initial: Entity = initialPayto === undefined ? defaultTarget : { - target: initialPayto.targetType, - params: initialPayto.params, - path1: initialPath1, - path2: initialPath2, - } - const [value, setValue] = useState<Partial<Entity>>(initial) + const initial: Entity = + initialPayto === undefined + ? defaultTarget + : { + target: initialPayto.targetType, + params: initialPayto.params, + path1: initialPath1, + path2: initialPath2, + }; + const [value, setValue] = useState<Partial<Entity>>(initial); const { i18n } = useTranslationContext(); const errors: FormErrors<Entity> = { - target: - value.target === noTargetValue - ? i18n.str`required` - : undefined, + target: value.target === noTargetValue ? i18n.str`required` : undefined, path1: !value.path1 ? i18n.str`required` : value.target === "iban" - ? validateIBAN(value.path1, i18n) + ? validateIBAN_path1(value.path1, i18n) : value.target === "bitcoin" - ? validateBitcoin(value.path1, i18n) + ? validateBitcoin_path1(value.path1, i18n) : value.target === "ethereum" - ? validateEthereum(value.path1, i18n) - : undefined, + ? validateEthereum_path1(value.path1, i18n) + : value.target === "x-taler-bank" + ? validateTalerBank_path1(value.path1, i18n) + : undefined, path2: value.target === "x-taler-bank" ? !value.path2 @@ -222,22 +251,29 @@ export function InputPaytoForm<T>({ const hasErrors = Object.keys(errors).some( (k) => (errors as any)[k] !== undefined, ); - const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({ - targetType: value.target, - targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""), - params: value.params ?? {} as any, - isKnown: false, - }) + + const path1WithSlash = value.path1 && !value.path1.endsWith("/") ? value.path1 + "/" : value.path1 + const str = + hasErrors || !value.target + ? undefined + : stringifyPaytoUri({ + targetType: value.target, + targetPath: value.path2 + ? `${path1WithSlash}${value.path2}` + : value.path1 ?? "", + params: value.params ?? ({} as any), + isKnown: false, + }); useEffect(() => { - onChange(str as any) - }, [str]) + onChange(str as any); + }, [str]); // const submit = useCallback((): void => { - // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos; + // // const accounts: TalerMerchantApi.AccountAddDetails[] = paytos; // // const alreadyExists = // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; // // if (!alreadyExists) { - // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = { + // const newValue: TalerMerchantApi.AccountAddDetails = { // payto_uri: paytoURL, // }; // if (value.auth) { @@ -365,7 +401,23 @@ export function InputPaytoForm<T>({ name="path1" readonly={readonly} label={i18n.str`Host`} + fromStr={(v) => { + if (v.startsWith("http")) { + try { + const url = new URL(v); + return url.host + url.pathname; + } catch { + return v; + } + } + return v; + }} tooltip={i18n.str`Bank host.`} + help={<Fragment> + <div><i18n.Translate>Without scheme and may include subpath:</i18n.Translate></div> + <div>bank.com/</div> + <div>bank.com/path/subpath/</div> + </Fragment>} /> <Input<Entity> name="path2" @@ -389,9 +441,7 @@ export function InputPaytoForm<T>({ /> </Fragment> )} - </FormProvider> </InputGroup> ); } - diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx index be5800d14..9956a6427 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx index 12ce6c6aa..4de84d984 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSecured.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx index 9d1a3ab8e..4a35ad96c 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSecured.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx index a8dad5d89..f567f7247 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSelector.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx index 668c65ea7..d7cf04553 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx index 1d18685c5..8104d1f9f 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputStock.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -18,10 +18,10 @@ * * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h } from "preact"; import { useLayoutEffect, useState } from "preact/hooks"; -import { MerchantBackend, Timestamp } from "../../declaration.js"; import { FormErrors, FormProvider } from "./FormProvider.js"; import { InputDate } from "./InputDate.js"; import { InputGroup } from "./InputGroup.js"; @@ -39,8 +39,8 @@ export interface Stock { current: number; lost: number; sold: number; - address?: MerchantBackend.Location; - nextRestock?: Timestamp; + address?: TalerMerchantApi.Location; + nextRestock?: TalerProtocolTimestamp; } interface StockDelta { @@ -133,8 +133,7 @@ export function InputStock<T>({ const stockAddedErrors: FormErrors<typeof addedStock> = { lost: currentStock + addedStock.incoming < addedStock.lost - ? i18n.str`lost cannot be greater than current and incoming (max ${ - currentStock + addedStock.incoming + ? i18n.str`lost cannot be greater than current and incoming (max ${currentStock + addedStock.incoming })` : undefined, }; diff --git a/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx index 2701768aa..1cd88d31a 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputTab.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx index b5722e4ec..4392c7659 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputTaxes.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -22,18 +22,18 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useCallback, useState } from "preact/hooks"; import * as yup from "yup"; -import { MerchantBackend } from "../../declaration.js"; import { TaxSchema as schema } from "../../schemas/index.js"; import { FormErrors, FormProvider } from "./FormProvider.js"; import { Input } from "./Input.js"; import { InputGroup } from "./InputGroup.js"; import { InputProps, useField } from "./useField.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; export interface Props<T> extends InputProps<T> { isValid?: (e: any) => boolean; } -type Entity = MerchantBackend.Tax; +type Entity = TalerMerchantApi.Tax; export function InputTaxes<T>({ name, readonly, diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx index f95dfcd05..8c935f33b 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -79,7 +79,7 @@ export function InputToggle<T>({ disabled={readonly} onChange={onCheckboxClick} /> - <div class="toggle-switch"></div> + <div class={`toggle-switch ${readonly ? "disabled" : ""}`} style={{ cursor: readonly ? "default" : undefined }}></div> </label> {help} </p> diff --git a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx index e9fd88770..b8cd4c2d2 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputWithAddon.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx index 2ff23dfd3..f5f9d5b4f 100644 --- a/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/JumpToElementById.tsx @@ -3,7 +3,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -export function JumpToElementById({ testIfExist, onSelect, palceholder, description }: { palceholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<any>, onSelect: (id: string) => void }): VNode { +export function JumpToElementById({ testIfExist, onSelect, placeholder, description }: { placeholder: TranslatedString, description: TranslatedString, testIfExist: (id: string) => Promise<boolean>, onSelect: (id: string) => void }): VNode { const { i18n } = useTranslationContext() const [error, setError] = useState<string | undefined>( @@ -17,9 +17,13 @@ export function JumpToElementById({ testIfExist, onSelect, palceholder, descript return; } try { - await testIfExist(currentId); - onSelect(currentId); - setError(undefined); + const exi = await testIfExist(currentId); + if (exi) { + onSelect(currentId); + setError(undefined); + } else { + setError(i18n.str`not found`); + } } catch { setError(i18n.str`not found`); } @@ -35,7 +39,7 @@ export function JumpToElementById({ testIfExist, onSelect, palceholder, descript type="text" value={id ?? ""} onChange={(e) => setId(e.currentTarget.value)} - placeholder={palceholder} + placeholder={placeholder} /> {error && <p class="help is-danger">{error}</p>} </div> diff --git a/packages/merchant-backoffice-ui/src/components/form/TextField.tsx b/packages/merchant-backoffice-ui/src/components/form/TextField.tsx index 03f36dcbb..8f897c2d8 100644 --- a/packages/merchant-backoffice-ui/src/components/form/TextField.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/TextField.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/useField.tsx b/packages/merchant-backoffice-ui/src/components/form/useField.tsx index c7559faae..49bba4984 100644 --- a/packages/merchant-backoffice-ui/src/components/form/useField.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/useField.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx b/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx index 9a445eb32..4fbfc4a75 100644 --- a/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/useGroupField.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/index.stories.ts b/packages/merchant-backoffice-ui/src/components/index.stories.ts index c57ddab14..f96defc09 100644 --- a/packages/merchant-backoffice-ui/src/components/index.stories.ts +++ b/packages/merchant-backoffice-ui/src/components/index.stories.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx index 6f5881fc0..864d09f48 100644 --- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx +++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -20,8 +20,8 @@ */ import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useBackendContext } from "../../context/backend.js"; +import { Fragment, VNode, h } from "preact"; +import { useSessionContext } from "../../context/session.js"; import { Entity } from "../../paths/admin/create/CreatePage.js"; import { Input } from "../form/Input.js"; import { InputDuration } from "../form/InputDuration.js"; @@ -31,6 +31,7 @@ import { InputLocation } from "../form/InputLocation.js"; import { InputSelector } from "../form/InputSelector.js"; import { InputToggle } from "../form/InputToggle.js"; import { InputWithAddon } from "../form/InputWithAddon.js"; +import { TextField } from "../form/TextField.js"; export function DefaultInstanceFormFields({ readonlyId, @@ -40,13 +41,13 @@ export function DefaultInstanceFormFields({ showId: boolean; }): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() + const { state } = useSessionContext(); return ( <Fragment> {showId && ( <InputWithAddon<Entity> name="id" - addonBefore={`${backendURL}/instances/`} + addonBefore={new URL("instances/", state.backendUrl.href).href} readonly={readonlyId} label={i18n.str`Identifier`} tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`} @@ -59,11 +60,20 @@ export function DefaultInstanceFormFields({ tooltip={i18n.str`Legal name of the business represented by this instance.`} /> + <TextField name="asdasd" label=""> + <i18n.Translate> + Choose individual if you don't have or are not required to have legal business permission. + </i18n.Translate> + </TextField> + <InputSelector<Entity> name="user_type" - label={i18n.str`Type`} + label={i18n.str`Selling as`} tooltip={i18n.str`Different type of account can have different rules and requirements.`} values={["business", "individual"]} + toStr={(d: string) => { + return d.toUpperCase(); + }} /> <Input<Entity> @@ -84,12 +94,6 @@ export function DefaultInstanceFormFields({ tooltip={i18n.str`Logo image.`} /> - <InputToggle<Entity> - name="use_stefan" - label={i18n.str`Pay transaction fee`} - tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`} - /> - <InputGroup name="address" label={i18n.str`Address`} @@ -106,6 +110,12 @@ export function DefaultInstanceFormFields({ <InputLocation name="jurisdiction" /> </InputGroup> + <InputToggle<Entity> + name="use_stefan" + label={i18n.str`Pay transaction fee`} + tooltip={i18n.str`Assume the cost of the transaction of let the user pay for it.`} + /> + <InputDuration<Entity> name="default_pay_delay" label={i18n.str`Default payment delay`} diff --git a/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx b/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx index 41fe1374a..a6cd8014d 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/LangSelector.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx index 9f1b33893..d81410bdf 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx index d7b3a2fa4..dbe21e0e9 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,43 +19,38 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerError } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useBackendContext } from "../../context/backend.js"; -import { useConfigContext } from "../../context/config.js"; +import { Fragment, VNode, h } from "preact"; +import { useSessionContext } from "../../context/session.js"; import { useInstanceKYCDetails } from "../../hooks/instance.js"; import { LangSelector } from "./LangSelector.js"; -const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; +// const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined; const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined; interface Props { - onLogout: () => void; - onShowSettings: () => void; mobile?: boolean; - instance: string; - admin?: boolean; - mimic?: boolean; - isPasswordOk: boolean; } -export function Sidebar({ - mobile, - instance, - onShowSettings, - onLogout, - admin, - mimic, - isPasswordOk -}: Props): VNode { - const config = useConfigContext(); - const { url: backendURL } = useBackendContext() +export function Sidebar({ mobile }: Props): VNode { const { i18n } = useTranslationContext(); + const { state, logOut, config } = useSessionContext(); const kycStatus = useInstanceKYCDetails(); - const needKYC = kycStatus.ok && kycStatus.data.type === "redirect"; + const needKYC = + kycStatus !== undefined && + !(kycStatus instanceof TalerError) && + kycStatus.type === "ok" && + !!kycStatus.body; + const isLoggedIn = state.status === "loggedIn"; + const hasToken = isLoggedIn && state.token !== undefined; + return ( - <aside class="aside is-placed-left is-expanded" style={{ overflowY: "scroll" }}> + <aside + class="aside is-placed-left is-expanded" + style={{ overflowY: "scroll" }} + > {mobile && ( <div class="footer" @@ -80,7 +75,7 @@ export function Sidebar({ </div> </div> <div class="menu is-menu-main"> - {instance ? ( + {isLoggedIn ? ( <Fragment> <ul class="menu-list"> <li> @@ -169,14 +164,6 @@ export function Sidebar({ </a> </li> <li> - <a href={"/reserves"} class="has-icon"> - <span class="icon"> - <i class="mdi mdi-cash" /> - </span> - <span class="menu-item-label">Reserves</span> - </a> - </li> - <li> <a href={"/webhooks"} class="has-icon"> <span class="icon"> <i class="mdi mdi-newspaper" /> @@ -214,9 +201,7 @@ export function Sidebar({ </p> <ul class="menu-list"> <li> - <a class="has-icon is-state-info is-hoverable" - onClick={(): void => onShowSettings()} - > + <a class="has-icon is-state-info is-hoverable" href="/interface"> <span class="icon"> <i class="mdi mdi-newspaper" /> </span> @@ -230,9 +215,7 @@ export function Sidebar({ <span style={{ width: "3rem" }} class="icon"> <i class="mdi mdi-web" /> </span> - <span class="menu-item-label"> - {new URL(backendURL).hostname} - </span> + <span class="menu-item-label">{state.backendUrl.hostname}</span> </div> </li> <li> @@ -240,12 +223,10 @@ export function Sidebar({ <span style={{ width: "3rem" }} class="icon"> ID </span> - <span class="menu-item-label"> - {!instance ? "default" : instance} - </span> + <span class="menu-item-label">{state.instance}</span> </div> </li> - {admin && !mimic && ( + {state.isAdmin && ( <Fragment> <p class="menu-label"> <i18n.Translate>Instances</i18n.Translate> @@ -272,11 +253,14 @@ export function Sidebar({ </li> </Fragment> )} - {isPasswordOk ? + {hasToken ? ( <li> <a class="has-icon is-state-info is-hoverable" - onClick={(): void => onLogout()} + onClick={(e): void => { + logOut(); + e.preventDefault(); + }} > <span class="icon"> <i class="mdi mdi-logout default" /> @@ -285,8 +269,8 @@ export function Sidebar({ <i18n.Translate>Log out</i18n.Translate> </span> </a> - </li> : undefined - } + </li> + ) : undefined} </ul> </div> </aside> diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx index a918cb818..123271f8d 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -17,10 +17,12 @@ import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useEffect, useState } from "preact/hooks"; import { AdminPaths } from "../../AdminRoutes.js"; -import { InstancePaths } from "../../InstanceRoutes.js"; +import { InstancePaths } from "../../Routing.js"; import { Notification } from "../../utils/types.js"; import { NavigationBar } from "./NavigationBar.js"; import { Sidebar } from "./SideBar.js"; +import { useSessionContext } from "../../context/session.js"; +import { useNavigationContext } from "@gnu-taler/web-util/browser"; function getInstanceTitle(path: string, id: string): string { switch (path) { @@ -83,16 +85,7 @@ function getAdminTitle(path: string, instance: string) { return getInstanceTitle(path, instance); } -interface MenuProps { - title?: string; - path: string; - instance: string; - admin?: boolean; - onLogout?: () => void; - onShowSettings: () => void; - setInstanceName: (s: string) => void; - isPasswordOk: boolean; -} +interface MenuProps {} function WithTitle({ title, @@ -107,25 +100,18 @@ function WithTitle({ return <Fragment>{children}</Fragment>; } -export function Menu({ - onLogout, - onShowSettings, - title, - instance, - path, - admin, - setInstanceName, - isPasswordOk -}: MenuProps): VNode { +export function Menu(_p: MenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); - const titleWithSubtitle = title - ? title - : !admin - ? getInstanceTitle(path, instance) - : getAdminTitle(path, instance); - const adminInstance = instance === "default"; - const mimic = admin && !adminInstance; + const { state, deImpersonate } = useSessionContext(); + const { path } = useNavigationContext(); + + const titleWithSubtitle = !state.isAdmin + ? getInstanceTitle(path, state.instance) + : getAdminTitle(path, state.instance); + + const isLoggedIn = state.status === "loggedIn"; + return ( <WithTitle title={titleWithSubtitle}> <div @@ -137,32 +123,26 @@ export function Menu({ title={titleWithSubtitle} /> - {onLogout && ( - <Sidebar - onShowSettings={onShowSettings} - onLogout={onLogout} - admin={admin} - mimic={mimic} - instance={instance} - mobile={mobileOpen} - isPasswordOk={isPasswordOk} - /> - )} - - {mimic && ( - <nav class="level" style={{ - zIndex: 100, - position: "fixed", - width: "50%", - marginLeft: "20%" - }}> + {isLoggedIn && <Sidebar mobile={mobileOpen} />} + + {state.status !== "loggedOut" && state.impersonated && ( + <nav + class="level" + style={{ + zIndex: 100, + position: "fixed", + width: "50%", + marginLeft: "20%", + }} + > <div class="level-item has-text-centered has-background-warning"> <p class="is-size-5"> - You are viewing the instance <b>"{instance}"</b>.{" "} + You are viewing the instance <b>"{state.instance}"</b> + .{" "} <a href="#/instances" - onClick={(e) => { - setInstanceName("default"); + onClick={() => { + deImpersonate(); }} > go back @@ -244,18 +224,16 @@ export function NotConnectedAppMenu({ ); } -export function NotYetReadyAppMenu({ - onLogout, - onShowSettings, - title, - isPasswordOk -}: NotYetReadyAppMenuProps): VNode { +export function NotYetReadyAppMenu({ title }: NotYetReadyAppMenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); + const { state } = useSessionContext(); useEffect(() => { document.title = `Taler Backoffice: ${title}`; }, [title]); + const isLoggedIn = state.status === "loggedIn"; + return ( <div class={mobileOpen ? "has-aside-mobile-expanded" : ""} @@ -265,9 +243,7 @@ export function NotYetReadyAppMenu({ onMobileMenu={() => setMobileOpen(!mobileOpen)} title={title} /> - {onLogout && ( - <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} /> - )} + {isLoggedIn && <Sidebar mobile={mobileOpen} />} </div> ); } diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx index 8372c84cc..1335d0f77 100644 --- a/packages/merchant-backoffice-ui/src/components/modal/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/modal/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -22,11 +22,11 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { ComponentChildren, Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { useInstanceContext } from "../../context/instance.js"; import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js"; import { Spinner } from "../exception/loading.js"; import { FormProvider } from "../form/FormProvider.js"; import { Input } from "../form/Input.js"; +import { useSessionContext } from "../../context/session.js"; interface Props { active?: boolean; @@ -298,8 +298,8 @@ export function UpdateTokenModal({ new_token: !form.new_token ? i18n.str`cannot be empty` : form.new_token === form.old_token - ? i18n.str`cannot be the same as the old token` - : undefined, + ? i18n.str`cannot be the same as the old token` + : undefined, repeat_token: form.new_token !== form.repeat_token ? i18n.str`is not the same` @@ -310,9 +310,9 @@ export function UpdateTokenModal({ (k) => (errors as any)[k] !== undefined, ); - const instance = useInstanceContext(); + const { state } = useSessionContext(); - const text = i18n.str`You are updating the access token from instance with id ${instance.id}`; + const text = i18n.str`You are updating the access token from instance with id ${state.instance}`; return ( <ClearConfirmModal @@ -374,8 +374,8 @@ export function SetTokenNewInstanceModal({ new_token: !form.new_token ? i18n.str`cannot be empty` : form.new_token === form.old_token - ? i18n.str`cannot be the same as the old access token` - : undefined, + ? i18n.str`cannot be the same as the old access token` + : undefined, repeat_token: form.new_token !== form.repeat_token ? i18n.str`is not the same` diff --git a/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx index 073382fb1..5cd8a237b 100644 --- a/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/components/notifications/CreatedSuccessfully.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx b/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx index af594de0f..d75c5ced2 100644 --- a/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/notifications/Notifications.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/notifications/index.tsx b/packages/merchant-backoffice-ui/src/components/notifications/index.tsx index 235c75577..0c4e0d761 100644 --- a/packages/merchant-backoffice-ui/src/components/notifications/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/notifications/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx b/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx index 0bc629d46..6dc1fadd6 100644 --- a/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx +++ b/packages/merchant-backoffice-ui/src/components/picker/DatePicker.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, Component } from "preact"; +import { Component, h } from "preact"; interface Props { closeFunction?: () => void; @@ -64,7 +64,7 @@ export class DatePicker extends Component<Props, State> { getDaysByMonth(month: number, year: number) { const calendar = []; - const date = new Date(year, month, 1); // month to display + // const date = new Date(year, month, 1); // month to display const firstDay = new Date(year, month, 1).getDay(); // first weekday of month const lastDate = new Date(year, month + 1, 0).getDate(); // last date of month diff --git a/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx index 8f74d55ac..b95ab054c 100644 --- a/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.tsx b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.tsx index 0968b0a17..7c1c172ac 100644 --- a/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.tsx +++ b/packages/merchant-backoffice-ui/src/components/picker/DurationPicker.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -42,7 +42,7 @@ export function DurationPicker({ onChange, value, }: Props): VNode { - const ss = 1000 * 1000; + const ss = 1000; const ms = ss * 60; const hs = ms * 60; const ds = hs * 24; diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx index 2d5a54cde..fcc97f96a 100644 --- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx index 377d9c1ba..52ac2a1fe 100644 --- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -16,24 +16,24 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { MerchantBackend, WithId } from "../../declaration.js"; import { ProductMap } from "../../paths/instance/orders/create/CreatePage.js"; import { FormErrors, FormProvider } from "../form/FormProvider.js"; import { InputNumber } from "../form/InputNumber.js"; import { InputSearchOnList } from "../form/InputSearchOnList.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; type Form = { - product: MerchantBackend.Products.ProductDetail & WithId; + product: TalerMerchantApi.ProductDetail & WithId; quantity: number; }; interface Props { currentProducts: ProductMap; onAddProduct: ( - product: MerchantBackend.Products.ProductDetail & WithId, + product: TalerMerchantApi.ProductDetail & WithId, quantity: number, ) => void; - inventory: (MerchantBackend.Products.ProductDetail & WithId)[]; + inventory: (TalerMerchantApi.ProductDetail & WithId)[]; } export function InventoryProductForm({ diff --git a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx index c6d280f94..a127999fc 100644 --- a/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/NonInventoryProductForm.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,11 +13,11 @@ 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 { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks"; import * as yup from "yup"; -import { MerchantBackend } from "../../declaration.js"; import { useListener } from "../../hooks/listener.js"; import { NonInventoryProductSchema as schema } from "../../schemas/index.js"; import { FormErrors, FormProvider } from "../form/FormProvider.js"; @@ -27,7 +27,7 @@ import { InputImage } from "../form/InputImage.js"; import { InputNumber } from "../form/InputNumber.js"; import { InputTaxes } from "../form/InputTaxes.js"; -type Entity = MerchantBackend.Product; +type Entity = TalerMerchantApi.Product; interface Props { onAddProduct: (p: Entity) => Promise<void>; @@ -46,7 +46,7 @@ export function NonInventoryProductFrom({ }, [isEditing]); const [submitForm, addFormSubmitter] = useListener< - Partial<MerchantBackend.Product> | undefined + Partial<TalerMerchantApi.Product> | undefined >((result) => { if (result) { setShowCreateProduct(false); @@ -55,7 +55,7 @@ export function NonInventoryProductFrom({ taxes: result.taxes || [], description: result.description || "", image: result.image || "", - price: result.price || "", + price: (result.price || "") as AmountString, unit: result.unit || "", }); } @@ -136,7 +136,7 @@ interface NonInventoryProduct { unit: string; price: string; image: string; - taxes: MerchantBackend.Tax[]; + taxes: TalerMerchantApi.Tax[]; } export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { @@ -144,7 +144,7 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { taxes: [], ...initial, }); - let errors: FormErrors<Entity> = {}; + let errors: FormErrors<NonInventoryProduct> = {}; try { schema.validateSync(value, { abortEarly: false }); } catch (err) { @@ -159,7 +159,7 @@ export function ProductForm({ onSubscribe, initial }: ProductProps): VNode { } const submit = useCallback((): Entity | undefined => { - return value as MerchantBackend.Product; + return value as TalerMerchantApi.Product; }, [value]); const hasErrors = Object.keys(errors).some( diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx index e91e8c876..c6d687b85 100644 --- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,12 +19,12 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h } from "preact"; import { useCallback, useEffect, useState } from "preact/hooks"; import * as yup from "yup"; -import { useBackendContext } from "../../context/backend.js"; -import { MerchantBackend } from "../../declaration.js"; +import { useSessionContext } from "../../context/session.js"; import { ProductCreateSchema as createSchema, ProductUpdateSchema as updateSchema, @@ -38,7 +38,7 @@ import { InputStock, Stock } from "../form/InputStock.js"; import { InputTaxes } from "../form/InputTaxes.js"; import { InputWithAddon } from "../form/InputWithAddon.js"; -type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }; +type Entity = TalerMerchantApi.ProductDetail & { product_id: string }; interface Props { onSubscribe: (c?: () => Entity | undefined) => void; @@ -52,18 +52,18 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { description_i18n: {}, taxes: [], next_restock: { t_s: "never" }, - price: ":0", + price: ":0" as AmountString, ...initial, stock: !initial || initial.total_stock === -1 ? undefined : { - current: initial.total_stock || 0, - lost: initial.total_lost || 0, - sold: initial.total_sold || 0, - address: initial.address, - nextRestock: initial.next_restock, - }, + current: initial.total_stock || 0, + lost: initial.total_lost || 0, + sold: initial.total_sold || 0, + address: initial.address, + nextRestock: initial.next_restock, + }, }); let errors: FormErrors<Entity> = {}; @@ -82,11 +82,11 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { } } const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); const submit = useCallback((): Entity | undefined => { - const stock: Stock = (value as any).stock; + const stock = value.stock; if (!stock) { value.total_stock = -1; @@ -99,13 +99,13 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { : stock.nextRestock; value.address = stock.address; } - delete (value as any).stock; + delete value.stock; if (typeof value.minimum_age !== "undefined" && value.minimum_age < 1) { delete value.minimum_age; } - return value as MerchantBackend.Products.ProductDetail & { + return value as TalerMerchantApi.ProductDetail & { product_id: string; }; }, [value]); @@ -114,9 +114,8 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { onSubscribe(hasErrors ? undefined : submit); }, [submit, hasErrors]); - const { url: backendURL } = useBackendContext() const { i18n } = useTranslationContext(); - + const { state } = useSessionContext(); return ( <div> <FormProvider<Entity> @@ -128,7 +127,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) { {alreadyExist ? undefined : ( <InputWithAddon<Entity> name="product_id" - addonBefore={`${backendURL}/product/`} + addonBefore={new URL("product/", state.backendUrl.href).href} label={i18n.str`ID`} tooltip={i18n.str`product identification to use in URLs (for internal use only)`} /> diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx index 25751dd96..4fff66fd7 100644 --- a/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx +++ b/packages/merchant-backoffice-ui/src/components/product/ProductList.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,18 +13,17 @@ 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 { Amounts } from "@gnu-taler/taler-util"; +import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; import emptyImage from "../../assets/empty.png"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { MerchantBackend } from "../../declaration.js"; interface Props { - list: MerchantBackend.Product[]; + list: TalerMerchantApi.Product[]; actions?: { name: string; tooltip: string; - handler: (d: MerchantBackend.Product, index: number) => void; + handler: (d: TalerMerchantApi.Product, index: number) => void; }[]; } export function ProductList({ list, actions = [] }: Props): VNode { @@ -60,7 +59,7 @@ export function ProductList({ list, actions = [] }: Props): VNode { : Amounts.stringify( Amounts.mult( Amounts.parseOrThrow(entry.price), - entry.quantity, + entry.quantity ?? 0 ).amount, ); diff --git a/packages/merchant-backoffice-ui/src/context/backend.test.ts b/packages/merchant-backoffice-ui/src/context/backend.test.ts deleted file mode 100644 index 359859819..000000000 --- a/packages/merchant-backoffice-ui/src/context/backend.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import * as tests from "@gnu-taler/web-util/testing"; -import { ComponentChildren, h, VNode } from "preact"; -import { AccessToken, MerchantBackend } from "../declaration.js"; -import { - useAdminAPI, - useInstanceAPI, - useManagementAPI, -} from "../hooks/instance.js"; -import { expect } from "chai"; -import { ApiMockEnvironment } from "../hooks/testing.js"; -import { - API_CREATE_INSTANCE, - API_NEW_LOGIN, - API_UPDATE_CURRENT_INSTANCE_AUTH, - API_UPDATE_INSTANCE_AUTH_BY_ID, -} from "../hooks/urls.js"; - -interface TestingContextProps { - children?: ComponentChildren; -} - -describe("backend context api ", () => { - it("should use new token after updating the instance token in the settings as user", async () => { - const env = new ApiMockEnvironment(); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const instance = useInstanceAPI(); - const management = useManagementAPI("default"); - const admin = useAdminAPI(); - - return { instance, management, admin }; - }, - {}, - [ - ({ instance, management, admin }) => { - env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), { - request: { - method: "token", - token: "another_token", - }, - response: { - name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "another_token", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - - }); - - management.setNewAccessToken(undefined,"another_token" as AccessToken); - }, - ({ instance, management, admin }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - env.addRequestExpectation(API_CREATE_INSTANCE, { - // auth: "another_token", - request: { - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage, - }); - - admin.createInstance({ - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should use new token after updating the instance token in the settings as admin", async () => { - const env = new ApiMockEnvironment(); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const instance = useInstanceAPI(); - const management = useManagementAPI("default"); - const admin = useAdminAPI(); - - return { instance, management, admin }; - }, - {}, - [ - ({ instance, management, admin }) => { - env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { - request: { - method: "token", - token: "another_token", - }, - response: { - name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "another_token", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - }); - instance.setNewAccessToken(undefined, "another_token" as AccessToken); - }, - ({ instance, management, admin }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - env.addRequestExpectation(API_CREATE_INSTANCE, { - // auth: "another_token", - request: { - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage, - }); - - admin.createInstance({ - id: "new_instance_id", - } as MerchantBackend.Instances.InstanceConfigurationMessage); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts deleted file mode 100644 index 6f2fd2aff..000000000 --- a/packages/merchant-backoffice-ui/src/context/backend.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useMemoryStorage } from "@gnu-taler/web-util/browser"; -import { createContext, h, VNode } from "preact"; -import { useContext } from "preact/hooks"; -import { LoginToken } from "../declaration.js"; -import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js"; - -interface BackendContextType { - url: string, - alreadyTriedLogin: boolean; - token?: LoginToken; - updateToken: (token: LoginToken | undefined) => void; -} - -const BackendContext = createContext<BackendContextType>({ - url: "", - alreadyTriedLogin: false, - token: undefined, - updateToken: () => null, -}); - -function useBackendContextState( - defaultUrl?: string, -): BackendContextType { - const [url] = useBackendURL(defaultUrl); - const [token, updateToken] = useBackendDefaultToken(); - - return { - url, - token, - alreadyTriedLogin: token !== undefined, - updateToken, - }; -} - -export const BackendContextProvider = ({ - children, - defaultUrl, -}: { - children: any; - defaultUrl?: string; -}): VNode => { - const value = useBackendContextState(defaultUrl); - - return h(BackendContext.Provider, { value, children }); -}; - -export const useBackendContext = (): BackendContextType => - useContext(BackendContext); diff --git a/packages/merchant-backoffice-ui/src/context/config.ts b/packages/merchant-backoffice-ui/src/context/config.ts deleted file mode 100644 index 040bd0341..000000000 --- a/packages/merchant-backoffice-ui/src/context/config.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { createContext } from "preact"; -import { useContext } from "preact/hooks"; - -interface Type { - currency: string; - version: string; -} -const Context = createContext<Type>(null!); - -export const ConfigContextProvider = Context.Provider; -export const useConfigContext = (): Type => useContext(Context); diff --git a/packages/merchant-backoffice-ui/src/context/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts new file mode 100644 index 000000000..fa5e14ab3 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/context/session.ts @@ -0,0 +1,255 @@ +/* + 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 { + AccessToken, + Codec, + TalerMerchantApi, + buildCodecForObject, + codecForString, + codecForURL, + codecOptional, +} from "@gnu-taler/taler-util"; +import { + buildStorageKey, + useLocalStorage, + useMerchantApiContext, +} from "@gnu-taler/web-util/browser"; +import { ComponentChildren, VNode, createContext, h } from "preact"; +import { useContext, useEffect, useState } from "preact/hooks"; +import { mutate } from "swr"; +import { MerchantLib } from "../../../web-util/lib/context/activity.js"; + +/** + * Has the information to reach and + * authenticate at the bank's backend. + */ +export type SessionState = LoggedIn | LoggedOut; + +interface LoggedIn { + status: "loggedIn"; + + // is this instance admin? usually "default" name + isAdmin: boolean; + + // url where all the request will be made + // usually this is from where the SPA was loaded + // unless it's using impersonate feature + backendUrl: URL; + + // instance name + instance: string; + + // session is not the same from where it was loaded + impersonated: boolean; + + //instance access token + token: AccessToken | undefined; +} + +interface LoggedOut { + status: "loggedOut"; + backendUrl: URL; + instance: string; + isAdmin: boolean; + token: AccessToken | undefined; +} + +interface SavedSession { + backendUrl: URL; + token: AccessToken | undefined; + prevToken: AccessToken | undefined; +} + +export const codecForSessionState = (): Codec<SavedSession> => + buildCodecForObject<SavedSession>() + .property("backendUrl", codecForURL()) + .property("token", codecOptional(codecForString() as Codec<AccessToken>)) + .property( + "prevToken", + codecOptional(codecForString() as Codec<AccessToken>), + ) + .build("SavedSession"); + +function inferInstanceName(url: URL) { + const match = INSTANCE_ID_LOOKUP.exec(url.href); + return !match || !match[1] ? DEFAULT_ADMIN_USERNAME : match[1]; +} + +export const defaultState = (url: URL): SavedSession => { + return { + backendUrl: url, + token: undefined, + prevToken: undefined, + }; +}; + +export interface SessionStateHandler { + lib: MerchantLib; + config: TalerMerchantApi.VersionResponse; + + state: SessionState; + /** + * from every state to logout state + */ + logOut(): void; + /** + * from impersonate to loggedIn + */ + deImpersonate(): void; + /** + * from any to loggedIn + * @param info + */ + logIn(token: AccessToken | undefined): void; + /** + * from loggedIn to impersonate + * @param info + */ + impersonate(baseUrl: URL): void; +} + +const SESSION_STATE_KEY = buildStorageKey( + "merchant-session", + codecForSessionState(), +); + +export const DEFAULT_ADMIN_USERNAME = "default"; + +export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/; + +export function cleanAllCache(): void { + mutate(() => true, undefined, { revalidate: false }); +} + +const Context = createContext<SessionStateHandler>(undefined!); + +export const useSessionContext = (): SessionStateHandler => useContext(Context); + +/** + * Creates the session in loggedIn state. + * Infer the instance name based on the URL. + * Create the instance of the merchant api http rest. + * Returns API that handle impersonation. + * + * @param param0 + * @returns + */ +export const SessionContextProvider = ({ + children, + // value, +}: { + // value: MerchantUiSettings; + children: ComponentChildren; +}): VNode => { + const { + lib: rootLib, + config: rootConfig, + url: merchantUrl, + } = useMerchantApiContext(); + const [status, setStatus] = useState<"loggedIn" | "loggedOut">("loggedIn"); + const [currentConfig, setCurrentConfig] = + useState<TalerMerchantApi.VersionResponse>(); + const { value: state, update } = useLocalStorage( + SESSION_STATE_KEY, + defaultState(merchantUrl), + ); + + const currentInstance = inferInstanceName(state.backendUrl); + + let lib: MerchantLib; + let config: TalerMerchantApi.VersionResponse; + const doingImpersonation = state.backendUrl.href !== merchantUrl.href; + if (doingImpersonation) { + /** + * FIXME: can't impersonate other than local instances + */ + lib = rootLib.subInstanceApi(inferInstanceName(state.backendUrl)); + + config = currentConfig ?? rootConfig; + } else { + lib = rootLib; + config = rootConfig; + } + + useEffect(() => { + // FIXME: handle what happen if the subinstance /config + // fails + if (!doingImpersonation) return; + lib.instance.getConfig().then((resp) => { + if (resp.type === "ok") { + setCurrentConfig(resp.body); + } + }); + }, [state.backendUrl.href]); + + const value: SessionStateHandler = { + state: { + backendUrl: state.backendUrl, + token: state.token, + impersonated: doingImpersonation, + instance: currentInstance, + isAdmin: currentInstance === DEFAULT_ADMIN_USERNAME, + status: status, + }, + lib, + config, + logOut() { + setStatus("loggedOut"); + update({ + backendUrl: merchantUrl, + token: undefined, + prevToken: undefined, + }); + cleanAllCache(); + }, + deImpersonate() { + cleanAllCache(); + update({ + backendUrl: merchantUrl, + token: state.prevToken, + prevToken: undefined, + }); + setStatus("loggedIn"); + }, + impersonate(baseUrl) { + /** + * FIXME: can't impersonate other than local instances + */ + update({ + backendUrl: baseUrl, + token: undefined, + prevToken: state.token, + }); + setStatus("loggedIn"); + cleanAllCache(); + }, + logIn(token) { + cleanAllCache(); + setStatus("loggedIn"); + update({ + backendUrl: state.backendUrl, + token: token, + prevToken: state.prevToken, + }); + }, + }; + + return h(Context.Provider, { + value, + children, + }); +}; diff --git a/packages/merchant-backoffice-ui/src/context/instance.ts b/packages/merchant-backoffice-ui/src/context/settings.ts index 3c6cc2b63..8bd1506d6 100644 --- a/packages/merchant-backoffice-ui/src/context/instance.ts +++ b/packages/merchant-backoffice-ui/src/context/settings.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -14,23 +14,31 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { ComponentChildren, createContext, h, VNode } from "preact"; +import { useContext } from "preact/hooks"; +import { MerchantUiSettings } from "../settings.js"; + /** * * @author Sebastian Javier Marchano (sebasjm) */ -import { createContext } from "preact"; -import { useContext } from "preact/hooks"; -import { LoginToken } from "../declaration.js"; - -interface Type { - id: string; - token?: LoginToken; - admin?: boolean; - changeToken: (t?: LoginToken) => void; -} - -const Context = createContext<Type>({} as any); - -export const InstanceContextProvider = Context.Provider; -export const useInstanceContext = (): Type => useContext(Context); +export type Type = MerchantUiSettings; + +const initial: MerchantUiSettings = {}; +const Context = createContext<Type>(initial); + +export const useSettingsContext = (): Type => useContext(Context); + +export const SettingsProvider = ({ + children, + value, +}: { + value: MerchantUiSettings; + children: ComponentChildren; +}): VNode => { + return h(Context.Provider, { + value, + children, + }); +}; diff --git a/packages/merchant-backoffice-ui/src/custom.d.ts b/packages/merchant-backoffice-ui/src/custom.d.ts index e693c2951..34522a2dd 100644 --- a/packages/merchant-backoffice-ui/src/custom.d.ts +++ b/packages/merchant-backoffice-ui/src/custom.d.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts index 15bde2305..6f6e23b42 100644 --- a/packages/merchant-backoffice-ui/src/declaration.d.ts +++ b/packages/merchant-backoffice-ui/src/declaration.d.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,29 +19,6 @@ * @author Sebastian Javier Marchano (sebasjm) */ -type HashCode = string; -type EddsaPublicKey = string; -type EddsaSignature = string; -type WireTransferIdentifierRawP = string; -type RelativeTime = Duration; -type ImageDataUrl = string; -type MerchantUserType = "business" | "individual"; - - -export interface WithId { - id: string; -} - -interface Timestamp { - // Milliseconds since epoch, or the special - // value "forever" to represent an event that will - // never happen. - t_s: number | "never"; -} -interface Duration { - d_us: number | "forever"; -} - interface WithId { id: string; } diff --git a/packages/merchant-backoffice-ui/src/hooks/async.ts b/packages/merchant-backoffice-ui/src/hooks/async.ts index f22badc88..212ef2211 100644 --- a/packages/merchant-backoffice-ui/src/hooks/async.ts +++ b/packages/merchant-backoffice-ui/src/hooks/async.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts deleted file mode 100644 index 8d99546a8..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ /dev/null @@ -1,477 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util"; -import { - ErrorType, - HttpError, - HttpResponse, - HttpResponseOk, - RequestError, - RequestOptions, - useApiContext, -} from "@gnu-taler/web-util/browser"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import { useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js"; -import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js"; - - -export function useMatchMutate(): ( - re?: RegExp, - value?: unknown, -) => Promise<any> { - const { cache, mutate } = useSWRConfig(); - - if (!(cache instanceof Map)) { - throw new Error( - "matchMutate requires the cache provider to be a Map instance", - ); - } - - return function matchRegexMutate(re?: RegExp) { - return mutate((key) => { - // evict if no key or regex === all - if (!key || !re) return true - // match string - if (typeof key === 'string' && re.test(key)) return true - // record or object have the path at [0] - if (typeof key === 'object' && re.test(key[0])) return true - //key didn't match regex - return false - }, undefined, { - revalidate: true, - }); - }; -} - -export function useBackendInstancesTestForAdmin(): HttpResponse< - MerchantBackend.Instances.InstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); - - type Type = MerchantBackend.Instances.InstancesResponse; - - const [result, setResult] = useState< - HttpResponse<Type, MerchantBackend.ErrorDetail> - >({ loading: true }); - - useEffect(() => { - request<Type>(`/management/instances`) - .then((data) => setResult(data)) - .catch((error: RequestError<MerchantBackend.ErrorDetail>) => - setResult(error.cause), - ); - }, [request]); - - return result; -} - -const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000; -const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000; - -export function useBackendConfig(): HttpResponse< - MerchantBackend.VersionResponse | undefined, - RequestError<MerchantBackend.ErrorDetail> -> { - const { request } = useBackendBaseRequest(); - - type Type = MerchantBackend.VersionResponse; - type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number } - const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 }); - - useEffect(() => { - if (result.timer) { - clearTimeout(result.timer) - } - function tryConfig(): void { - request<Type>(`/config`) - .then((data) => { - const timer: any = setTimeout(() => { - tryConfig() - }, CHECK_CONFIG_INTERVAL_OK) - setResult({ data, timer }) - }) - .catch((error) => { - const timer: any = setTimeout(() => { - tryConfig() - }, CHECK_CONFIG_INTERVAL_FAIL) - const data = error.cause - setResult({ data, timer }) - }); - } - tryConfig() - }, [request]); - - return result.data; -} - -interface useBackendInstanceRequestType { - request: <T>( - endpoint: string, - options?: RequestOptions, - ) => Promise<HttpResponseOk<T>>; - fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>; - multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>; - orderFetcher: <T>( - params: [endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number,] - ) => Promise<HttpResponseOk<T>>; - transferFetcher: <T>( - params: [endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number,] - ) => Promise<HttpResponseOk<T>>; - templateFetcher: <T>( - params: [endpoint: string, - position?: string, - delta?: number] - ) => Promise<HttpResponseOk<T>>; - webhookFetcher: <T>( - params: [endpoint: string, - position?: string, - delta?: number] - ) => Promise<HttpResponseOk<T>>; -} -interface useBackendBaseRequestType { - request: <T>( - endpoint: string, - options?: RequestOptions, - ) => Promise<HttpResponseOk<T>>; -} - -type YesOrNo = "yes" | "no"; -type LoginResult = { - valid: true; - token: string; - expiration: Timestamp; -} | { - valid: false; - cause: HttpError<{}>; -} - -export function useCredentialsChecker() { - const { request } = useApiContext(); - //check against instance details endpoint - //while merchant backend doesn't have a login endpoint - async function requestNewLoginToken( - baseUrl: string, - token: AccessToken, - ): Promise<LoginResult> { - const data: MerchantBackend.Instances.LoginTokenRequest = { - scope: "write", - duration: { - d_us: "forever" - }, - refreshable: true, - } - try { - const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, { - method: "POST", - token, - data - }); - return { valid: true, token: response.data.token, expiration: response.data.expiration }; - } catch (error) { - if (error instanceof RequestError) { - return { valid: false, cause: error.cause }; - } - - return { - valid: false, cause: { - type: ErrorType.UNEXPECTED, - loading: false, - info: { - hasToken: true, - status: 0, - options: {}, - url: `/private/token`, - payload: {} - }, - exception: error, - message: (error instanceof Error ? error.message : "unpexepected error") - } - }; - } - }; - - async function refreshLoginToken( - baseUrl: string, - token: LoginToken - ): Promise<LoginResult> { - - if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) { - return { - valid: false, cause: { - type: ErrorType.CLIENT, - status: HttpStatusCode.Unauthorized, - message: "login token expired, login again.", - info: { - hasToken: true, - status: 401, - options: {}, - url: `/private/token`, - payload: {} - }, - payload: {} - }, - } - } - - return requestNewLoginToken(baseUrl, token.token as AccessToken) - } - return { requestNewLoginToken, refreshLoginToken } -} - -/** - * - * @param root the request is intended to the base URL and no the instance URL - * @returns request handler to - */ -export function useBackendBaseRequest(): useBackendBaseRequestType { - const { url: backend, token: loginToken } = useBackendContext(); - const { request: requestHandler } = useApiContext(); - const token = loginToken?.token; - - const request = useCallback( - function requestImpl<T>( - endpoint: string, - options: RequestOptions = {}, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => { - return res - }).catch(err => { - throw err - }); - }, - [backend, token], - ); - - return { request }; -} - -export function useBackendInstanceRequest(): useBackendInstanceRequestType { - const { url: rootBackendUrl, token: rootToken } = useBackendContext(); - const { token: instanceToken, id, admin } = useInstanceContext(); - const { request: requestHandler } = useApiContext(); - - const { baseUrl, token: loginToken } = !admin - ? { baseUrl: rootBackendUrl, token: rootToken } - : { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken }; - - const token = loginToken?.token; - - const request = useCallback( - function requestImpl<T>( - endpoint: string, - options: RequestOptions = {}, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { token, ...options }); - }, - [baseUrl, token], - ); - - const multiFetcher = useCallback( - function multiFetcherImpl<T>( - args: [endpoints: string[]], - ): Promise<HttpResponseOk<T>[]> { - const [endpoints] = args - return Promise.all( - endpoints.map((endpoint) => - requestHandler<T>(baseUrl, endpoint, { token }), - ), - ); - }, - [baseUrl, token], - ); - - const fetcher = useCallback( - function fetcherImpl<T>(endpoint: string): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { token }); - }, - [baseUrl, token], - ); - - const orderFetcher = useCallback( - function orderFetcherImpl<T>( - args: [endpoint: string, - paid?: YesOrNo, - refunded?: YesOrNo, - wired?: YesOrNo, - searchDate?: Date, - delta?: number,] - ): Promise<HttpResponseOk<T>> { - const [endpoint, paid, refunded, wired, searchDate, delta] = args - const date_s = - delta && delta < 0 && searchDate - ? Math.floor(searchDate.getTime() / 1000) + 1 - : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined; - const params: any = {}; - if (paid !== undefined) params.paid = paid; - if (delta !== undefined) params.delta = delta; - if (refunded !== undefined) params.refunded = refunded; - if (wired !== undefined) params.wired = wired; - if (date_s !== undefined) params.date_s = date_s; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { orders: [] } as T, - }) - } - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const reserveDetailFetcher = useCallback( - function reserveDetailFetcherImpl<T>( - endpoint: string, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { - params: { - rewards: "yes", - }, - token, - }); - }, - [baseUrl, token], - ); - - const rewardsDetailFetcher = useCallback( - function rewardsDetailFetcherImpl<T>( - endpoint: string, - ): Promise<HttpResponseOk<T>> { - return requestHandler<T>(baseUrl, endpoint, { - params: { - pickups: "yes", - }, - token, - }); - }, - [baseUrl, token], - ); - - const transferFetcher = useCallback( - function transferFetcherImpl<T>( - args: [endpoint: string, - payto_uri?: string, - verified?: string, - position?: string, - delta?: number,] - ): Promise<HttpResponseOk<T>> { - const [endpoint, payto_uri, verified, position, delta] = args - const params: any = {}; - if (payto_uri !== undefined) params.payto_uri = payto_uri; - if (verified !== undefined) params.verified = verified; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { transfers: [] } as T, - }) - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const templateFetcher = useCallback( - function templateFetcherImpl<T>( - args: [endpoint: string, - position?: string, - delta?: number,] - ): Promise<HttpResponseOk<T>> { - const [endpoint, position, delta] = args - const params: any = {}; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { templates: [] } as T, - }) - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - const webhookFetcher = useCallback( - function webhookFetcherImpl<T>( - args: [endpoint: string, - position?: string, - delta?: number,] - ): Promise<HttpResponseOk<T>> { - const [endpoint, position, delta] = args - const params: any = {}; - if (delta === 0) { - //in this case we can already assume the response - //and avoid network - return Promise.resolve({ - ok: true, - data: { webhooks: [] } as T, - }) - } - if (delta !== undefined) { - params.limit = delta; - } - if (position !== undefined) params.offset = position; - - return requestHandler<T>(baseUrl, endpoint, { params, token }); - }, - [baseUrl, token], - ); - - return { - request, - fetcher, - multiFetcher, - orderFetcher, - reserveDetailFetcher, - rewardsDetailFetcher, - transferFetcher, - templateFetcher, - webhookFetcher, - }; -} diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts index 03b064646..8857ad839 100644 --- a/packages/merchant-backoffice-ui/src/hooks/bank.ts +++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -14,204 +14,73 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, + useMerchantApiContext } from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; -// const MOCKED_ACCOUNTS: Record<string, MerchantBackend.BankAccounts.AccountAddDetails> = { -// "hwire1": { -// h_wire: "hwire1", -// payto_uri: "payto://fake/iban/123", -// salt: "qwe", -// }, -// "hwire2": { -// h_wire: "hwire2", -// payto_uri: "payto://fake/iban/123", -// salt: "qwe2", -// }, -// } +export interface InstanceBankAccountFilter { +} -export function useBankAccountAPI(): BankAccountAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); +export function revalidateInstanceBankAccounts() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listBankAccounts", + undefined, + { revalidate: true }, + ); +} +export function useInstanceBankAccounts() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); - const createBankAccount = async ( - data: MerchantBackend.BankAccounts.AccountAddDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_ACCOUNTS[data.h_wire] = data - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/accounts`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/accounts.*/); - return res; - }; + // const [offset, setOffset] = useState<string | undefined>(); - const updateBankAccount = async ( - h_wire: string, - data: MerchantBackend.BankAccounts.AccountPatchDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_ACCOUNTS[h_wire].credit_facade_credentials = data.credit_facade_credentials - // MOCKED_ACCOUNTS[h_wire].credit_facade_url = data.credit_facade_url - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/accounts/${h_wire}`, { - method: "PATCH", - data, + async function fetcher([token, _bid]: [AccessToken, string]) { + return await instance.listBankAccounts(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid, + // order: "dec", }); - await mutateAll(/.*private\/accounts.*/); - return res; - }; + } - const deleteBankAccount = async ( - h_wire: string, - ): Promise<HttpResponseOk<void>> => { - // delete MOCKED_ACCOUNTS[h_wire] - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/accounts/${h_wire}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/accounts.*/); - return res; - }; + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listBankAccounts">, + TalerHttpError + >([session.token, "offset", "listBankAccounts"], fetcher); - return { - createBankAccount, - updateBankAccount, - deleteBankAccount, - }; -} + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; -export interface BankAccountAPI { - createBankAccount: ( - data: MerchantBackend.BankAccounts.AccountAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateBankAccount: ( - id: string, - data: MerchantBackend.BankAccounts.AccountPatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteBankAccount: (id: string) => Promise<HttpResponseOk<void>>; + // return buildPaginatedResult(data.body.accounts, offset, setOffset, (d) => d.h_wire) + return data; } -export interface InstanceBankAccountFilter { +export function revalidateBankAccountDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getBankAccountDetails", + undefined, + { revalidate: true }, + ); } +export function useBankAccountDetails(h_wire: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -export function useInstanceBankAccounts( - args?: InstanceBankAccountFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.BankAccounts.AccountsSummaryResponse, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // loadMore() { }, - // loadMorePrev() { }, - // data: { - // accounts: Object.values(MOCKED_ACCOUNTS).map(e => ({ - // ...e, - // active: true, - // })) - // } - // } - const { fetcher } = useBackendInstanceRequest(); - - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.BankAccounts.AccountsSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/accounts`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.BankAccounts.AccountsSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData /*, beforeData*/]); - - if (afterError) return afterError.cause; - - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.accounts.length < totalAfter; - const isReachingStart = false; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.accounts.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.accounts[afterData.data.accounts.length - 1] - .h_wire - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - }, - }; - - const accounts = !afterData ? [] : (afterData || lastAfter).data.accounts; - if (loadingAfter /* || loadingBefore */) - return { loading: true, data: { accounts } }; - if (/*beforeData &&*/ afterData) { - return { ok: true, data: { accounts }, ...pagination }; + async function fetcher([token, wireId]: [AccessToken, string]) { + return await instance.getBankAccountDetails(token, wireId); } - return { loading: true }; -} -export function useBankAccountDetails( - h_wire: string, -): HttpResponse< - MerchantBackend.BankAccounts.BankAccountEntry, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // data: { - // ...MOCKED_ACCOUNTS[h_wire], - // active: true, - // } - // } - const { fetcher } = useBackendInstanceRequest(); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getBankAccountDetails">, + TalerHttpError + >([session.token, h_wire, "getBankAccountDetails"], fetcher); - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.BankAccounts.BankAccountEntry>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/accounts/${h_wire}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) { - return data; - } - if (error) return error.cause; - return { loading: true }; + if (data) return data; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts deleted file mode 100644 index f0cd1bfb9..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util"; -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; -import { LoginToken } from "../declaration.js"; -import { ValueOrFunction } from "../utils/types.js"; -import { useMatchMutate } from "./backend.js"; - -const calculateRootPath = () => { - const rootPath = - typeof window !== undefined - ? window.location.origin + window.location.pathname - : "/"; - - /** - * By default, merchant backend serves the html content - * from the /webui root. This should cover most of the - * cases and the rootPath will be the merchant backend - * URL where the instances are - */ - return rootPath.replace("/webui/", ""); -}; - -const loginTokenCodec = buildCodecForObject<LoginToken>() - .property("token", codecForString()) - .property("expiration", codecForTimestamp) - .build("loginToken") -const TOKENS_KEY = buildStorageKey("merchant-token", codecForMap(loginTokenCodec)); - - -export function useBackendURL( - url?: string, -): [string, StateUpdater<string>] { - const [value, setter] = useSimpleLocalStorage( - "merchant-base-url", - url || calculateRootPath(), - ); - - const checkedSetter = (v: ValueOrFunction<string>) => { - return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, "")); - }; - - return [value!, checkedSetter]; -} - -export function useBackendDefaultToken( -): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { - const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) - - const tokenOfDefaultInstance = tokenMap["default"] - const clearCache = useMatchMutate() - useEffect(() => { - clearCache() - }, [tokenOfDefaultInstance]) - - function updateToken( - value: (LoginToken | undefined) - ): void { - if (value === undefined) { - reset() - } else { - const res = { ...tokenMap, "default": value } - setToken(res) - } - } - return [tokenMap["default"], updateToken]; -} - -export function useBackendInstanceToken( - id: string, -): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] { - const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {}) - const [defaultToken, defaultSetToken] = useBackendDefaultToken(); - - // instance named 'default' use the default token - if (id === "default") { - return [defaultToken, defaultSetToken]; - } - function updateToken( - value: (LoginToken | undefined) - ): void { - if (value === undefined) { - reset() - } else { - const res = { ...tokenMap, [id]: value } - setToken(res) - } - } - - return [tokenMap[id], updateToken]; -} - -export function useLang(initial?: string): [string, StateUpdater<string>] { - const browserLang = - typeof window !== "undefined" - ? navigator.language || (navigator as any).userLanguage - : undefined; - const defaultLang = (browserLang || initial || "en").substring(0, 2); - return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater<string>]; -} - -export function useSimpleLocalStorage( - key: string, - initialValue?: string, -): [string | undefined, StateUpdater<string | undefined>] { - const [storedValue, setStoredValue] = useState<string | undefined>( - (): string | undefined => { - return typeof window !== "undefined" - ? window.localStorage.getItem(key) || initialValue - : initialValue; - }, - ); - - const setValue = ( - value?: string | ((val?: string) => string | undefined), - ) => { - setStoredValue((p) => { - const toStore = value instanceof Function ? value(p) : value; - if (typeof window !== "undefined") { - if (!toStore) { - window.localStorage.removeItem(key); - } else { - window.localStorage.setItem(key, toStore); - } - } - return toStore; - }); - }; - - return [storedValue, setValue]; -} diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts index ee1576764..f409592b0 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,15 +19,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; -import { AccessToken, MerchantBackend } from "../declaration.js"; import { - useAdminAPI, useBackendInstances, - useInstanceAPI, useInstanceDetails, - useManagementAPI, } from "./instance.js"; import { ApiMockEnvironment } from "./testing.js"; import { @@ -35,7 +33,6 @@ import { API_DELETE_INSTANCE, API_GET_CURRENT_INSTANCE, API_LIST_INSTANCES, - API_NEW_LOGIN, API_UPDATE_CURRENT_INSTANCE, API_UPDATE_CURRENT_INSTANCE_AUTH, API_UPDATE_INSTANCE_BY_ID, @@ -48,55 +45,56 @@ describe("instance api interaction with details", () => { env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { response: { name: "instance_name", - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useInstanceAPI(); + // const api = useInstanceAPI(); + const { lib: api } = useMerchantApiContext() const query = useInstanceDetails(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // }); env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, { request: { name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage, + } as TalerMerchantApi.InstanceReconfigurationMessage, }); env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { response: { name: "other_name", - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); - api.updateInstance({ + api.instance.updateCurrentInstance(undefined, { name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage); + } as TalerMerchantApi.InstanceReconfigurationMessage); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "other_name", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "other_name", + // }); }, ], env.buildTestingContext(), @@ -109,56 +107,56 @@ describe("instance api interaction with details", () => { it("should evict cache when setting the instance's token", async () => { const env = new ApiMockEnvironment(); - env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { - response: { - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - } as MerchantBackend.Instances.QueryInstancesResponse, - }); + // env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { + // response: { + // name: "instance_name", + // auth: { + // method: "token", + // // token: "not-secret", + // }, + // } as TalerMerchantApi.QueryInstancesResponse, + // }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useInstanceAPI(); + const { lib: api } = useMerchantApiContext() const query = useInstanceDetails(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - }, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // auth: { + // method: "token", + // }, + // }); env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { request: { method: "token", token: "secret", - } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, - }); - env.addRequestExpectation(API_NEW_LOGIN, { - auth: "secret", - request: { - scope: "write", - duration: { - "d_us": "forever", - }, - refreshable: true, - }, - }); + } as TalerMerchantApi.InstanceAuthConfigurationMessage, + }); + // env.addRequestExpectation(API_NEW_LOGIN, { + // auth: "secret", + // request: { + // scope: "write", + // duration: { + // "d_us": "forever", + // }, + // refreshable: true, + // }, + // }); env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { response: { name: "instance_name", @@ -166,24 +164,24 @@ describe("instance api interaction with details", () => { method: "token", // token: "secret", }, - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); - api.setNewAccessToken(undefined, "secret" as AccessToken); + // api.setNewAccessToken(undefined, "secret" as AccessToken); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - // token: "secret", - }, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // auth: { + // method: "token", + // // token: "secret", + // }, + // }); }, ], env.buildTestingContext(), @@ -202,38 +200,38 @@ describe("instance api interaction with details", () => { method: "token", // token: "not-secret", }, - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useInstanceAPI(); + const { lib: api } = useMerchantApiContext() const query = useInstanceDetails(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "token", - // token: "not-secret", - }, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // auth: { + // method: "token", + // // token: "not-secret", + // }, + // }); env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { request: { method: "external", - } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, + } as TalerMerchantApi.InstanceAuthConfigurationMessage, }); env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { response: { @@ -241,24 +239,26 @@ describe("instance api interaction with details", () => { auth: { method: "external", }, - } as MerchantBackend.Instances.QueryInstancesResponse, + } as TalerMerchantApi.QueryInstancesResponse, }); - api.clearAccessToken(undefined); + api.instance.updateCurrentInstanceAuthentication(undefined, { + method: "external" + }); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - name: "instance_name", - auth: { - method: "external", - }, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // name: "instance_name", + // auth: { + // method: "external", + // }, + // }); }, ], env.buildTestingContext(), @@ -331,76 +331,76 @@ describe("instance admin api interaction with listing", () => { instances: [ { name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useAdminAPI(); + const { lib: api } = useMerchantApiContext() const query = useBackendInstances(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - name: "instance_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // name: "instance_name", + // }, + // ], + // }); env.addRequestExpectation(API_CREATE_INSTANCE, { request: { name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage, + } as TalerMerchantApi.InstanceConfigurationMessage, }); env.addRequestExpectation(API_LIST_INSTANCES, { response: { instances: [ { name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, { name: "other_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); - api.createInstance({ + api.instance.createInstance(undefined, { name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage); + } as TalerMerchantApi.InstanceConfigurationMessage) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - name: "instance_name", - }, - { - name: "other_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // name: "instance_name", + // }, + // { + // name: "other_name", + // }, + // ], + // }); }, ], env.buildTestingContext(), @@ -418,45 +418,45 @@ describe("instance admin api interaction with listing", () => { { id: "default", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, { id: "the_id", name: "second_instance", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useAdminAPI(); + const { lib: api } = useMerchantApiContext() const query = useBackendInstances(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - { - id: "the_id", - name: "second_instance", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "default", + // name: "instance_name", + // }, + // { + // id: "the_id", + // name: "second_instance", + // }, + // ], + // }); env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), {}); env.addRequestExpectation(API_LIST_INSTANCES, { @@ -465,28 +465,28 @@ describe("instance admin api interaction with listing", () => { { id: "default", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); - api.deleteInstance("the_id"); + api.instance.deleteInstance(undefined, "the_id"); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "default", + // name: "instance_name", + // }, + // ], + // }); }, ], env.buildTestingContext(), @@ -542,7 +542,7 @@ describe("instance admin api interaction with listing", () => { // instances: [{ // id: 'default', // name: 'instance_name' - // } as MerchantBackend.Instances.Instance] + // } as TalerMerchantApi.Instance] // }, // }); @@ -572,45 +572,45 @@ describe("instance admin api interaction with listing", () => { { id: "default", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, { id: "the_id", name: "second_instance", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useAdminAPI(); + const { lib: api } = useMerchantApiContext() const query = useBackendInstances(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - { - id: "the_id", - name: "second_instance", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "default", + // name: "instance_name", + // }, + // { + // id: "the_id", + // name: "second_instance", + // }, + // ], + // }); env.addRequestExpectation(API_DELETE_INSTANCE("the_id"), { qparam: { @@ -623,28 +623,28 @@ describe("instance admin api interaction with listing", () => { { id: "default", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); - api.purgeInstance("the_id"); + api.instance.deleteInstance(undefined, "the_id", { purge: true }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "default", - name: "instance_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "default", + // name: "instance_name", + // }, + // ], + // }); }, ], env.buildTestingContext(), @@ -664,42 +664,42 @@ describe("instance management api interaction with listing", () => { { id: "managed", name: "instance_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { - const api = useManagementAPI("managed"); + const { lib: api } = useMerchantApiContext() const query = useBackendInstances(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "managed", - name: "instance_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "managed", + // name: "instance_name", + // }, + // ], + // }); env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID("managed"), { request: { name: "other_name", - } as MerchantBackend.Instances.InstanceReconfigurationMessage, + } as TalerMerchantApi.InstanceReconfigurationMessage, }); env.addRequestExpectation(API_LIST_INSTANCES, { response: { @@ -707,30 +707,30 @@ describe("instance management api interaction with listing", () => { { id: "managed", name: "other_name", - } as MerchantBackend.Instances.Instance, + } as TalerMerchantApi.Instance, ], }, }); - api.updateInstance({ + api.instance.updateCurrentInstance(undefined, { name: "other_name", - } as MerchantBackend.Instances.InstanceConfigurationMessage); + } as TalerMerchantApi.InstanceConfigurationMessage); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - instances: [ - { - id: "managed", - name: "other_name", - }, - ], - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // instances: [ + // { + // id: "managed", + // name: "other_name", + // }, + // ], + // }); }, ], env.buildTestingContext(), diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts index 0677191db..f5f8893cd 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,301 +13,112 @@ 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 { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useBackendContext } from "../context/backend.js"; -import { AccessToken, MerchantBackend } from "../declaration.js"; -import { - useBackendBaseRequest, - useBackendInstanceRequest, - useCredentialsChecker, - useMatchMutate, -} from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; -interface InstanceAPI { - updateInstance: ( - data: MerchantBackend.Instances.InstanceReconfigurationMessage, - ) => Promise<void>; - deleteInstance: () => Promise<void>; - clearAccessToken: (currentToken: AccessToken | undefined) => Promise<void>; - setNewAccessToken: (currentToken: AccessToken | undefined, token: AccessToken) => Promise<void>; -} - -export function useAdminAPI(): AdminAPI { - const { request } = useBackendBaseRequest(); - const mutateAll = useMatchMutate(); - - const createInstance = async ( - instance: MerchantBackend.Instances.InstanceConfigurationMessage, - ): Promise<void> => { - await request(`/management/instances`, { - method: "POST", - data: instance, - }); - - mutateAll(/\/management\/instances/); - }; - - const deleteInstance = async (id: string): Promise<void> => { - await request(`/management/instances/${id}`, { - method: "DELETE", - }); - - mutateAll(/\/management\/instances/); - }; - const purgeInstance = async (id: string): Promise<void> => { - await request(`/management/instances/${id}`, { - method: "DELETE", - params: { - purge: "YES", - }, - }); - - mutateAll(/\/management\/instances/); - }; - - return { createInstance, deleteInstance, purgeInstance }; +export function revalidateInstanceDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentInstanceDetails", + undefined, + { revalidate: true }, + ); } +export function useInstanceDetails() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -export interface AdminAPI { - createInstance: ( - data: MerchantBackend.Instances.InstanceConfigurationMessage, - ) => Promise<void>; - deleteInstance: (id: string) => Promise<void>; - purgeInstance: (id: string) => Promise<void>; -} - -export function useManagementAPI(instanceId: string): InstanceAPI { - const mutateAll = useMatchMutate(); - const { url: backendURL } = useBackendContext() - const { updateToken } = useBackendContext(); - const { request } = useBackendBaseRequest(); - const { requestNewLoginToken } = useCredentialsChecker() - - const updateInstance = async ( - instance: MerchantBackend.Instances.InstanceReconfigurationMessage, - ): Promise<void> => { - await request(`/management/instances/${instanceId}`, { - method: "PATCH", - data: instance, - }); - - mutateAll(/\/management\/instances/); - }; - - const deleteInstance = async (): Promise<void> => { - await request(`/management/instances/${instanceId}`, { - method: "DELETE", - }); - - mutateAll(/\/management\/instances/); - }; - - const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { - await request(`/management/instances/${instanceId}/auth`, { - method: "POST", - token: currentToken, - data: { method: "external" }, - }); - - mutateAll(/\/management\/instances/); - }; - - const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { - await request(`/management/instances/${instanceId}/auth`, { - method: "POST", - token: currentToken, - data: { method: "token", token: newToken }, - }); - - const resp = await requestNewLoginToken(backendURL, newToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) - } + async function fetcher([token]: [AccessToken]) { + return await instance.getCurrentInstanceDetails(token); + } - mutateAll(/\/management\/instances/); - }; + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getCurrentInstanceDetails">, + TalerHttpError + >([session.token, "getCurrentInstanceDetails"], fetcher); - return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; + if (data) return data; + if (error) return error; + return undefined; } -export function useInstanceAPI(): InstanceAPI { - const { mutate } = useSWRConfig(); - const { url: backendURL, updateToken } = useBackendContext() - - const { - token: adminToken, - } = useBackendContext(); - const { request } = useBackendInstanceRequest(); - const { requestNewLoginToken } = useCredentialsChecker() - - const updateInstance = async ( - instance: MerchantBackend.Instances.InstanceReconfigurationMessage, - ): Promise<void> => { - await request(`/private/`, { - method: "PATCH", - data: instance, - }); - - if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); - mutate([`/private/`], null); - }; - - const deleteInstance = async (): Promise<void> => { - await request(`/private/`, { - method: "DELETE", - // token: adminToken, - }); - - if (adminToken) mutate(["/private/instances", adminToken, backendURL], null); - mutate([`/private/`], null); - }; - - const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => { - await request(`/private/auth`, { - method: "POST", - token: currentToken, - data: { method: "external" }, - }); +export function revalidateInstanceKYCDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getCurrentIntanceKycStatus", + undefined, + { revalidate: true }, + ); +} +export function useInstanceKYCDetails() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); - mutate([`/private/`], null); - }; + async function fetcher([token]: [AccessToken]) { + return await instance.getCurrentIntanceKycStatus(token, {}); + } - const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => { - await request(`/private/auth`, { - method: "POST", - token: currentToken, - data: { method: "token", token: newToken }, - }); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getCurrentIntanceKycStatus">, + TalerHttpError + >([session.token, "getCurrentIntanceKycStatus"], fetcher); - const resp = await requestNewLoginToken(backendURL, newToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) - } + if (data) return data; + if (error) return error; + return undefined; - mutate([`/private/`], null); - }; - return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken }; } -export function useInstanceDetails(): HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - revalidateIfStale: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; +export function revalidateManagedInstanceDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getInstanceDetails", + undefined, + { revalidate: true }, + ); } +export function useManagedInstanceDetails(instanceId: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -type KYCStatus = - | { type: "ok" } - | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects }; - -export function useInstanceKYCDetails(): HttpResponse< - KYCStatus, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); + async function fetcher([token, instanceId]: [AccessToken, string]) { + return await instance.getInstanceDetails(token, instanceId); + } const { data, error } = useSWR< - HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/kyc`], fetcher, { - refreshInterval: 60 * 1000, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateIfStale: false, - revalidateOnMount: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); + TalerMerchantManagementResultByMethod<"getInstanceDetails">, + TalerHttpError + >([session.token, instanceId, "getInstanceDetails"], fetcher); - if (data) { - if (data.info?.status === 202) - return { ok: true, data: { type: "redirect", status: data.data } }; - return { ok: true, data: { type: "ok" } }; - } - if (error) return error.cause; - return { loading: true }; + if (data) return data; + if (error) return error; + return undefined; } -export function useManagedInstanceDetails( - instanceId: string, -): HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/management/instances/${instanceId}`], request, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - errorRetryCount: 0, - errorRetryInterval: 1, - shouldRetryOnError: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; +export function revalidateBackendInstances() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listInstances", + undefined, + { revalidate: true }, + ); } +export function useBackendInstances() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -export function useBackendInstances(): HttpResponse< - MerchantBackend.Instances.InstancesResponse, - MerchantBackend.ErrorDetail -> { - const { request } = useBackendBaseRequest(); + async function fetcher([token]: [AccessToken]) { + return await instance.listInstances(token); + } - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Instances.InstancesResponse>, - RequestError<MerchantBackend.ErrorDetail> - >(["/management/instances"], request); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listInstances">, + TalerHttpError + >([session.token, "listInstances"], fetcher); - if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/listener.ts b/packages/merchant-backoffice-ui/src/hooks/listener.ts index d101f7bb8..f59794fd4 100644 --- a/packages/merchant-backoffice-ui/src/hooks/listener.ts +++ b/packages/merchant-backoffice-ui/src/hooks/listener.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/hooks/notifications.ts b/packages/merchant-backoffice-ui/src/hooks/notifications.ts index 133ddd80b..137ef5333 100644 --- a/packages/merchant-backoffice-ui/src/hooks/notifications.ts +++ b/packages/merchant-backoffice-ui/src/hooks/notifications.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/hooks/order.test.ts b/packages/merchant-backoffice-ui/src/hooks/order.test.ts index c243309a8..9c1eaccbb 100644 --- a/packages/merchant-backoffice-ui/src/hooks/order.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/order.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,10 +19,10 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AbsoluteTime, AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js"; +import { useInstanceOrders, useOrderDetails } from "./order.js"; import { ApiMockEnvironment } from "./testing.js"; import { API_CREATE_ORDER, @@ -32,6 +32,7 @@ import { API_LIST_ORDERS, API_REFUND_ORDER_BY_ID, } from "./urls.js"; +import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; describe("order api interaction with listing", () => { it("should evict cache when creating an order", async () => { @@ -40,39 +41,40 @@ describe("order api interaction with listing", () => { env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -20, paid: "yes" }, response: { - orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], + orders: [{ order_id: "1" }, { order_id: "2" } as TalerMerchantApi.OrderHistoryEntry], }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ paid: true }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ - ({ query, api }) => { - expect(query.loading).true; + ({ query }) => { + expect(query).undefined; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "1" }, { order_id: "2" }], + // }); env.addRequestExpectation(API_CREATE_ORDER, { request: { - order: { amount: "ARS:12", summary: "pay me" }, + order: { amount: "ARS:12" as AmountString, summary: "pay me" }, + lock_uuids: [] }, response: { order_id: "3" }, }); @@ -84,21 +86,21 @@ describe("order api interaction with listing", () => { }, }); - api.createOrder({ - order: { amount: "ARS:12", summary: "pay me" }, - } as any); + api.instance.createOrder(undefined, { + order: { amount: "ARS:12" as AmountString, summary: "pay me" }, + }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], + // }); }, ], env.buildTestingContext(), @@ -112,45 +114,47 @@ describe("order api interaction with listing", () => { env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -20, paid: "yes" }, - response: { orders: [{ - order_id: "1", - amount: "EUR:12", - refundable: true, - } as MerchantBackend.Orders.OrderHistoryEntry] }, + response: { + orders: [{ + order_id: "1", + amount: "EUR:12", + refundable: true, + } as TalerMerchantApi.OrderHistoryEntry] + }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ paid: true }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { - order_id: "1", - amount: "EUR:12", - refundable: true, - }, - ], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [ + // { + // order_id: "1", + // amount: "EUR:12", + // refundable: true, + // }, + // ], + // }); env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { request: { reason: "double pay", @@ -160,33 +164,35 @@ describe("order api interaction with listing", () => { env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -20, paid: "yes" }, - response: { orders: [ - { order_id: "1", amount: "EUR:12", refundable: false } as any, - ] }, + response: { + orders: [ + { order_id: "1", amount: "EUR:12", refundable: false } as any, + ] + }, }); - api.refundOrder("1", { + api.instance.addRefund(undefined, "1", { reason: "double pay", - refund: "EUR:1", - }); + refund: "EUR:1" as AmountString, + }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { - order_id: "1", - amount: "EUR:12", - refundable: false, - }, - ], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [ + // { + // order_id: "1", + // amount: "EUR:12", + // refundable: false, + // }, + // ], + // }); }, ], env.buildTestingContext(), @@ -202,35 +208,35 @@ describe("order api interaction with listing", () => { env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -20, paid: "yes" }, response: { - orders: [{ order_id: "1" }, { order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], + orders: [{ order_id: "1" }, { order_id: "2" } as TalerMerchantApi.OrderHistoryEntry], }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ paid: true }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "1" }, { order_id: "2" }], + // }); env.addRequestExpectation(API_DELETE_ORDER("1"), {}); @@ -241,18 +247,18 @@ describe("order api interaction with listing", () => { }, }); - api.deleteOrder("1"); + api.instance.deleteOrder(undefined, "1") }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "2" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "2" }], + // }); }, ], env.buildTestingContext(), @@ -271,35 +277,31 @@ describe("order api interaction with details", () => { response: { summary: "description", refund_amount: "EUR:0", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - const newDate = (d: Date) => { - //console.log("new date", d); - }; - const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useOrderDetails("1"); - const api = useOrderAPI(); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:0", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // summary: "description", + // refund_amount: "EUR:0", + // }); env.addRequestExpectation(API_REFUND_ORDER_BY_ID("1"), { request: { reason: "double pay", @@ -311,25 +313,25 @@ describe("order api interaction with details", () => { response: { summary: "description", refund_amount: "EUR:1", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - api.refundOrder("1", { + api.instance.addRefund(undefined, "1", { reason: "double pay", - refund: "EUR:1", - }); + refund: "EUR:1" as AmountString, + }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:1", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // summary: "description", + // refund_amount: "EUR:1", + // }); }, ], env.buildTestingContext(), @@ -347,35 +349,31 @@ describe("order api interaction with details", () => { response: { summary: "description", refund_amount: "EUR:0", - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - const newDate = (d: Date) => { - //console.log("new date", d); - }; - const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useOrderDetails("1"); - const api = useOrderAPI(); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: "description", - refund_amount: "EUR:0", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // summary: "description", + // refund_amount: "EUR:0", + // }); env.addRequestExpectation(API_FORGET_ORDER_BY_ID("1"), { request: { fields: ["$.summary"], @@ -385,23 +383,23 @@ describe("order api interaction with details", () => { env.addRequestExpectation(API_GET_ORDER_BY_ID("1"), { response: { summary: undefined, - } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, + } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - api.forgetOrder("1", { + api.instance.forgetOrder(undefined, "1", { fields: ["$.summary"], - }); + }) }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - summary: undefined, - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // summary: undefined, + // }); }, ], env.buildTestingContext(), @@ -428,38 +426,35 @@ describe("order listing pagination", () => { }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { const date = new Date(12000); - const query = useInstanceOrders({ wired: "yes", date }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); - expect(query.isReachingEnd).true; - expect(query.isReachingStart).true; + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [{ order_id: "1" }, { order_id: "2" }], + // }); + // expect(query.isReachingEnd).true; + // expect(query.isReachingStart).true; - // should not trigger new state update or query - query.loadMore(); - query.loadMorePrev(); }, ], env.buildTestingContext(), @@ -469,7 +464,7 @@ describe("order listing pagination", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); }); - it("should load more if result brings more that PAGE_SIZE", async () => { + it("should load more if result brings more that PAGINATED_LIST_REQUEST", async () => { const env = new ApiMockEnvironment(); const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ @@ -478,7 +473,6 @@ describe("order listing pagination", () => { const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i + 20), })); - const ordersFrom20to0 = [...ordersFrom0to20].reverse(); env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: 20, wired: "yes", date_s: 12 }, @@ -494,34 +488,34 @@ describe("order listing pagination", () => { }, }); - const newDate = (d: Date) => { + const newDate = (_d: string | undefined) => { //console.log("new date", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { const date = new Date(12000); - const query = useInstanceOrders({ wired: "yes", date }, newDate); - const api = useOrderAPI(); + const query = useInstanceOrders({ wired: true, date: AbsoluteTime.fromMilliseconds(date.getTime()) }, newDate); + const { lib: api } = useMerchantApiContext() return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [...ordersFrom20to0, ...ordersFrom20to40], - }); - expect(query.isReachingEnd).false; - expect(query.isReachingStart).false; + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [...ordersFrom20to0, ...ordersFrom20to40], + // }); + // expect(query.isReachingEnd).false; + // expect(query.isReachingStart).false; env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: -40, wired: "yes", date_s: 13 }, @@ -530,25 +524,25 @@ describe("order listing pagination", () => { }, }); - query.loadMore(); + // query.loadMore(); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - ...ordersFrom20to0, - ...ordersFrom20to40, - { order_id: "41" }, - ], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [ + // ...ordersFrom20to0, + // ...ordersFrom20to40, + // { order_id: "41" }, + // ], + // }); env.addRequestExpectation(API_LIST_ORDERS, { qparam: { delta: 40, wired: "yes", date_s: 12 }, @@ -557,26 +551,26 @@ describe("order listing pagination", () => { }, }); - query.loadMorePrev(); + // query.loadMorePrev(); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - orders: [ - { order_id: "-1" }, - ...ordersFrom20to0, - ...ordersFrom20to40, - { order_id: "41" }, - ], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // orders: [ + // { order_id: "-1" }, + // ...ordersFrom20to0, + // ...ordersFrom20to40, + // { order_id: "41" }, + // ], + // }); }, ], env.buildTestingContext(), diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts index e7a893f2c..d0513dc40 100644 --- a/packages/merchant-backoffice-ui/src/hooks/order.ts +++ b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,277 +13,86 @@ 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 { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; +import { AbsoluteTime, AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { buildPaginatedResult } from "./webhooks.js"; const useSWR = _useSWR as unknown as SWRHook; -export interface OrderAPI { - //FIXME: add OutOfStockResponse on 410 - createOrder: ( - data: MerchantBackend.Orders.PostOrderRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>>; - forgetOrder: ( - id: string, - data: MerchantBackend.Orders.ForgetRequest, - ) => Promise<HttpResponseOk<void>>; - refundOrder: ( - id: string, - data: MerchantBackend.Orders.RefundRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>>; - deleteOrder: (id: string) => Promise<HttpResponseOk<void>>; - getPaymentURL: (id: string) => Promise<HttpResponseOk<string>>; -} - -type YesOrNo = "yes" | "no"; - -export function useOrderAPI(): OrderAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createOrder = async ( - data: MerchantBackend.Orders.PostOrderRequest, - ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => { - const res = await request<MerchantBackend.Orders.PostOrderResponse>( - `/private/orders`, - { - method: "POST", - data, - }, - ); - await mutateAll(/.*private\/orders.*/); - // mutate('') - return res; - }; - const refundOrder = async ( - orderId: string, - data: MerchantBackend.Orders.RefundRequest, - ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<MerchantBackend.Orders.MerchantRefundResponse>( - `/private/orders/${orderId}/refund`, - { - method: "POST", - data, - }, - ); - - // order list returns refundable information, so we must evict everything - await mutateAll(/.*private\/orders.*/); - return res; - }; - - const forgetOrder = async ( - orderId: string, - data: MerchantBackend.Orders.ForgetRequest, - ): Promise<HttpResponseOk<void>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<void>(`/private/orders/${orderId}/forget`, { - method: "PATCH", - data, - }); - // we may be forgetting some fields that are pare of the listing, so we must evict everything - await mutateAll(/.*private\/orders.*/); - return res; - }; - const deleteOrder = async ( - orderId: string, - ): Promise<HttpResponseOk<void>> => { - mutateAll(/@"\/private\/orders"@/); - const res = request<void>(`/private/orders/${orderId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/orders.*/); - return res; - }; - const getPaymentURL = async ( - orderId: string, - ): Promise<HttpResponseOk<string>> => { - return request<MerchantBackend.Orders.MerchantOrderStatusResponse>( - `/private/orders/${orderId}`, - { - method: "GET", - }, - ).then((res) => { - const url = - res.data.order_status === "unpaid" - ? res.data.taler_pay_uri - : res.data.contract_terms.fulfillment_url; - const response: HttpResponseOk<string> = res as any; - response.data = url || ""; - return response; - }); - }; - return { createOrder, forgetOrder, deleteOrder, refundOrder, getPaymentURL }; +export function revalidateOrderDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getOrderDetails", + undefined, + { revalidate: true }, + ); } +export function useOrderDetails(oderId: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -export function useOrderDetails( - oderId: string, -): HttpResponse< - MerchantBackend.Orders.MerchantOrderStatusResponse, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); + async function fetcher([dId, token]: [string, AccessToken]) { + return await instance.getOrderDetails(token, dId); + } - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/orders/${oderId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getOrderDetails">, + TalerHttpError + >([oderId, session.token, "getOrderDetails"], fetcher); - if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } export interface InstanceOrderFilter { - paid?: YesOrNo; - refunded?: YesOrNo; - wired?: YesOrNo; - date?: Date; + paid?: boolean; + refunded?: boolean; + wired?: boolean; + date?: AbsoluteTime; + position?: string; } +export function revalidateInstanceOrders() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listOrders", + undefined, + { revalidate: true }, + ); +} export function useInstanceOrders( args?: InstanceOrderFilter, - updateFilter?: (d: Date) => void, -): HttpResponsePaginated< - MerchantBackend.Orders.OrderHistory, - MerchantBackend.ErrorDetail -> { - const { orderFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.date ? pageBefore * PAGE_SIZE : 0; - - /** - * FIXME: this can be cleaned up a little - * - * the logic of double query should be inside the orderFetch so from the hook perspective and cache - * is just one query and one error status - */ - const { - data: beforeData, - error: beforeError, - isValidating: loadingBefore, - } = useSWR< - HttpResponseOk<MerchantBackend.Orders.OrderHistory>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/orders`, - args?.paid, - args?.refunded, - args?.wired, - args?.date, - totalBefore, - ], - orderFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Orders.OrderHistory>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/orders`, - args?.paid, - args?.refunded, - args?.wired, - args?.date, - -totalAfter, - ], - orderFetcher, - ); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Orders.OrderHistory, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Orders.OrderHistory, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); - - if (beforeError) return beforeError.cause; - if (afterError) return afterError.cause; + updatePosition: (d: string | undefined) => void = () => { }, +) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); + + // const [offset, setOffset] = useState<string | undefined>(args?.position); + + async function fetcher([token, o, p, r, w, d]: [AccessToken, string, boolean, boolean, boolean, AbsoluteTime]) { + return await instance.listOrders(token, { + limit: PAGINATED_LIST_REQUEST, + offset: o, + order: "dec", + paid: p, + refunded: r, + wired: w, + date: d, + }); + } - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = afterData && afterData.data.orders.length < totalAfter; - const isReachingStart = - args?.date === undefined || - (beforeData && beforeData.data.orders.length < totalBefore); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listOrders">, + TalerHttpError + >([session.token, args?.position, args?.paid, args?.refunded, args?.wired, args?.date, "listOrders"], fetcher); - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.orders.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = - afterData.data.orders[afterData.data.orders.length - 1].timestamp.t_s; - if (from && from !== "never" && updateFilter) - updateFilter(new Date(from * 1000)); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.orders.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = - beforeData.data.orders[beforeData.data.orders.length - 1].timestamp - .t_s; - if (from && from !== "never" && updateFilter) - updateFilter(new Date(from * 1000)); - } - }, - }; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - const orders = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.orders - .slice() - .reverse() - .concat((afterData || lastAfter).data.orders); - if (loadingAfter || loadingBefore) return { loading: true, data: { orders } }; - if (beforeData && afterData) { - return { ok: true, data: { orders }, ...pagination }; - } - return { loading: true }; + return buildPaginatedResult(data.body.orders, args?.position, updatePosition, (d) => String(d.row_id)) } diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts index b045e365a..41ed89f70 100644 --- a/packages/merchant-backoffice-ui/src/hooks/otp.ts +++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,211 +13,68 @@ 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 { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; -const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = { - "1": { - otp_device_description: "first device", - otp_algorithm: 1, - otp_device_id: "1", - otp_key: "123", - }, - "2": { - otp_device_description: "second device", - otp_algorithm: 0, - otp_device_id: "2", - otp_key: "456", - } +export function revalidateInstanceOtpDevices() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listOtpDevices", + undefined, + { revalidate: true }, + ); } +export function useInstanceOtpDevices() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -export function useOtpDeviceAPI(): OtpDeviceAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); + // const [offset, setOffset] = useState<string | undefined>(); - const createOtpDevice = async ( - data: MerchantBackend.OTP.OtpDeviceAddDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_DEVICES[data.otp_device_id] = data - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/otp-devices`, { - method: "POST", - data, + async function fetcher([token, _bid]: [AccessToken, string]) { + return await instance.listOtpDevices(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid, + // order: "dec", }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; + } - const updateOtpDevice = async ( - deviceId: string, - data: MerchantBackend.OTP.OtpDevicePatchDetails, - ): Promise<HttpResponseOk<void>> => { - // MOCKED_DEVICES[deviceId].otp_algorithm = data.otp_algorithm - // MOCKED_DEVICES[deviceId].otp_ctr = data.otp_ctr - // MOCKED_DEVICES[deviceId].otp_device_description = data.otp_device_description - // MOCKED_DEVICES[deviceId].otp_key = data.otp_key - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/otp-devices/${deviceId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listOtpDevices">, + TalerHttpError + >([session.token, "offset", "listOtpDevices"], fetcher); - const deleteOtpDevice = async ( - deviceId: string, - ): Promise<HttpResponseOk<void>> => { - // delete MOCKED_DEVICES[deviceId] - // return Promise.resolve({ ok: true, data: undefined }); - const res = await request<void>(`/private/otp-devices/${deviceId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/otp-devices.*/); - return res; - }; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - return { - createOtpDevice, - updateOtpDevice, - deleteOtpDevice, - }; + // return buildPaginatedResult(data.body.otp_devices, offset, setOffset, (d) => d.otp_device_id) + return data; } -export interface OtpDeviceAPI { - createOtpDevice: ( - data: MerchantBackend.OTP.OtpDeviceAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateOtpDevice: ( - id: string, - data: MerchantBackend.OTP.OtpDevicePatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteOtpDevice: (id: string) => Promise<HttpResponseOk<void>>; +export function revalidateOtpDeviceDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getOtpDeviceDetails", + undefined, + { revalidate: true }, + ); } +export function useOtpDeviceDetails(deviceId: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -export interface InstanceOtpDeviceFilter { -} - -export function useInstanceOtpDevices( - args?: InstanceOtpDeviceFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.OTP.OtpDeviceSummaryResponse, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // loadMore: () => { }, - // loadMorePrev: () => { }, - // data: { - // otp_devices: Object.values(MOCKED_DEVICES).map(d => ({ - // device_description: d.otp_device_description, - // otp_device_id: d.otp_device_id - // })) - // } - // } - - const { fetcher } = useBackendInstanceRequest(); - - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.OTP.OtpDeviceSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/otp-devices`], fetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.OTP.OtpDeviceSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData /*, beforeData*/]); - - if (afterError) return afterError.cause; - - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.otp_devices.length < totalAfter; - const isReachingStart = true; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.otp_devices.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.otp_devices[afterData.data.otp_devices.length - 1] - .otp_device_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - }, - }; - - const otp_devices = !afterData ? [] : (afterData || lastAfter).data.otp_devices; - if (loadingAfter /* || loadingBefore */) - return { loading: true, data: { otp_devices } }; - if (/*beforeData &&*/ afterData) { - return { ok: true, data: { otp_devices }, ...pagination }; + async function fetcher([dId, token]: [string, AccessToken]) { + return await instance.getOtpDeviceDetails(token, dId); } - return { loading: true }; -} -export function useOtpDeviceDetails( - deviceId: string, -): HttpResponse< - MerchantBackend.OTP.OtpDeviceDetails, - MerchantBackend.ErrorDetail -> { - // return { - // ok: true, - // data: { - // device_description: MOCKED_DEVICES[deviceId].otp_device_description, - // otp_algorithm: MOCKED_DEVICES[deviceId].otp_algorithm, - // otp_ctr: MOCKED_DEVICES[deviceId].otp_ctr - // } - // } - const { fetcher } = useBackendInstanceRequest(); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getOtpDeviceDetails">, + TalerHttpError + >([deviceId, session.token, "getOtpDeviceDetails"], fetcher); - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.OTP.OtpDeviceDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/otp-devices/${deviceId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) { - return data; - } - if (error) return error.cause; - return { loading: true }; + if (data) return data; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts new file mode 100644 index 000000000..a21d2921c --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/preference.ts @@ -0,0 +1,89 @@ +/* + 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 { + AbsoluteTime, + Codec, + buildCodecForObject, + codecForAbsoluteTime, + codecForBoolean, + codecForConstString, + codecForEither, +} from "@gnu-taler/taler-util"; +import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; + +export interface Preferences { + advanceOrderMode: boolean; + hideKycUntil: AbsoluteTime; + hideMissingAccountUntil: AbsoluteTime; + dateFormat: "ymd" | "dmy" | "mdy"; +} + +const defaultSettings: Preferences = { + advanceOrderMode: false, + hideKycUntil: AbsoluteTime.never(), + hideMissingAccountUntil: AbsoluteTime.never(), + dateFormat: "ymd", +}; + +export const codecForPreferences = (): Codec<Preferences> => + buildCodecForObject<Preferences>() + .property("advanceOrderMode", codecForBoolean()) + .property("hideKycUntil", codecForAbsoluteTime) + .property("hideMissingAccountUntil", codecForAbsoluteTime) + .property( + "dateFormat", + codecForEither( + codecForConstString("ymd"), + codecForConstString("dmy"), + codecForConstString("mdy"), + ), + ) + .build("Preferences"); + +const PREFERENCES_KEY = buildStorageKey( + "merchant-preferences", + codecForPreferences(), +); + +export function usePreference(): [ + Readonly<Preferences>, + <T extends keyof Preferences>(key: T, value: Preferences[T]) => void, + (s: Preferences) => void, +] { + const { value, update } = useLocalStorage(PREFERENCES_KEY, defaultSettings); + function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) { + const newValue = { ...value, [k]: v }; + update(newValue); + } + + return [value, updateField, update]; +} + +export function dateFormatForSettings(s: Preferences): string { + switch (s.dateFormat) { + case "ymd": + return "yyyy/MM/dd"; + case "dmy": + return "dd/MM/yyyy"; + case "mdy": + return "MM/dd/yyyy"; + } +} + +export function datetimeFormatForSettings(s: Preferences): string { + return dateFormatForSettings(s) + " HH:mm:ss"; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/product.test.ts b/packages/merchant-backoffice-ui/src/hooks/product.test.ts index 7cac10e25..39281241c 100644 --- a/packages/merchant-backoffice-ui/src/hooks/product.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/product.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -21,10 +21,8 @@ import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; import { useInstanceProducts, - useProductAPI, useProductDetails, } from "./product.js"; import { ApiMockEnvironment } from "./testing.js"; @@ -35,6 +33,8 @@ import { API_LIST_PRODUCTS, API_UPDATE_PRODUCT_BY_ID, } from "./urls.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; describe("product api interaction with listing", () => { it("should evict cache when creating a product", async () => { @@ -42,64 +42,64 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }], + products: [{ product_id: "1234", product_serial: 1 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, + response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useInstanceProducts(); - const api = useProductAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); env.addRequestExpectation(API_CREATE_PRODUCT, { request: { price: "ARS:23", - } as MerchantBackend.Products.ProductAddDetail, + } as TalerMerchantApi.ProductAddDetail, }); env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }, { product_id: "2345" }], + products: [{ product_id: "1234", product_serial: 1 }, { product_id: "2345", product_serial: 2 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { response: { price: "ARS:23", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); - api.createProduct({ + api.instance.addProduct(undefined, { price: "ARS:23", } as any); }, @@ -107,25 +107,25 @@ describe("product api interaction with listing", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { - id: "1234", - price: "ARS:12", - }, - { - id: "2345", - price: "ARS:23", - }, - ]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([ + // { + // id: "1234", + // price: "ARS:12", + // }, + // { + // id: "2345", + // price: "ARS:23", + // }, + // ]); }, ], env.buildTestingContext(), @@ -140,54 +140,54 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }], + products: [{ product_id: "1234", product_serial: 1 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, + response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useInstanceProducts(); - const api = useProductAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { request: { price: "ARS:13", - } as MerchantBackend.Products.ProductPatchDetail, + } as TalerMerchantApi.ProductPatchDetail, }); env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }], + products: [{ product_id: "1234", product_serial: 1 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:13", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); - api.updateProduct("1234", { + api.instance.updateProduct(undefined, "1234", { price: "ARS:13", } as any); }, @@ -195,15 +195,15 @@ describe("product api interaction with listing", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { - id: "1234", - price: "ARS:13", - }, - ]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([ + // { + // id: "1234", + // price: "ARS:13", + // }, + // ]); }, ], env.buildTestingContext(), @@ -218,71 +218,71 @@ describe("product api interaction with listing", () => { env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }, { product_id: "2345" }], + products: [{ product_id: "1234", product_serial: 1 }, { product_id: "2345", product_serial: 2 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, + response: { price: "ARS:12" } as TalerMerchantApi.ProductDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, + response: { price: "ARS:23" } as TalerMerchantApi.ProductDetail, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useInstanceProducts(); - const api = useProductAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([ - { id: "1234", price: "ARS:12" }, - { id: "2345", price: "ARS:23" }, - ]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([ + // { id: "1234", price: "ARS:12" }, + // { id: "2345", price: "ARS:23" }, + // ]); env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {}); env.addRequestExpectation(API_LIST_PRODUCTS, { response: { - products: [{ product_id: "1234" }], + products: [{ product_id: "1234", product_serial: 1 }], }, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { response: { price: "ARS:12", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); - api.deleteProduct("2345"); + api.instance.deleteProduct(undefined, "2345"); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals([{ id: "1234", price: "ARS:12" }]); }, ], env.buildTestingContext(), @@ -300,44 +300,44 @@ describe("product api interaction with details", () => { env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { response: { description: "this is a description", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useProductDetails("12"); - const api = useProductAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - description: "this is a description", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // description: "this is a description", + // }); env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), { request: { description: "other description", - } as MerchantBackend.Products.ProductPatchDetail, + } as TalerMerchantApi.ProductPatchDetail, }); env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { response: { description: "other description", - } as MerchantBackend.Products.ProductDetail, + } as TalerMerchantApi.ProductDetail, }); - api.updateProduct("12", { + api.instance.updateProduct(undefined, "12", { description: "other description", } as any); }, @@ -345,12 +345,12 @@ describe("product api interaction with details", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - description: "other description", - }); + // expect(query.loading).false; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // description: "other description", + // }); }, ], env.buildTestingContext(), diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts index b54cd4d91..defda5552 100644 --- a/packages/merchant-backoffice-ui/src/hooks/product.ts +++ b/packages/merchant-backoffice-ui/src/hooks/product.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,165 +13,91 @@ 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 { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { MerchantBackend, WithId } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; +import { AccessToken, OperationOk, TalerHttpError, TalerMerchantApi, TalerMerchantManagementErrorsByMethod, TalerMerchantManagementResultByMethod, opFixedSuccess } from "@gnu-taler/taler-util"; +import { useState } from "preact/hooks"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; +import { buildPaginatedResult } from "./webhooks.js"; const useSWR = _useSWR as unknown as SWRHook; -export interface ProductAPI { - getProduct: ( - id: string, - ) => Promise<void>; - createProduct: ( - data: MerchantBackend.Products.ProductAddDetail, - ) => Promise<void>; - updateProduct: ( - id: string, - data: MerchantBackend.Products.ProductPatchDetail, - ) => Promise<void>; - deleteProduct: (id: string) => Promise<void>; - lockProduct: ( - id: string, - data: MerchantBackend.Products.LockRequest, - ) => Promise<void>; +type ProductWithId = TalerMerchantApi.ProductDetail & { id: string, serial: number }; +function notUndefined(c: ProductWithId | undefined): c is ProductWithId { + return c !== undefined; } -export function useProductAPI(): ProductAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - - const { request } = useBackendInstanceRequest(); - - const createProduct = async ( - data: MerchantBackend.Products.ProductAddDetail, - ): Promise<void> => { - const res = await request(`/private/products`, { - method: "POST", - data, - }); - - return await mutateAll(/.*\/private\/products.*/); - }; - - const updateProduct = async ( - productId: string, - data: MerchantBackend.Products.ProductPatchDetail, - ): Promise<void> => { - const r = await request(`/private/products/${productId}`, { - method: "PATCH", - data, - }); +export function revalidateInstanceProducts() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listProductsWithId", + undefined, + { revalidate: true }, + ); +} +export function useInstanceProducts() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); - return await mutateAll(/.*\/private\/products.*/); - }; + const [offset, setOffset] = useState<number | undefined>(); - const deleteProduct = async (productId: string): Promise<void> => { - await request(`/private/products/${productId}`, { - method: "DELETE", - }); - await mutate([`/private/products`]); - }; - - const lockProduct = async ( - productId: string, - data: MerchantBackend.Products.LockRequest, - ): Promise<void> => { - await request(`/private/products/${productId}/lock`, { - method: "POST", - data, + async function fetcher([token, bid]: [AccessToken, number]) { + const list = await instance.listProducts(token, { + limit: PAGINATED_LIST_REQUEST, + offset: bid === undefined ? undefined: String(bid), + order: "dec", }); + if (list.type !== "ok") { + return list; + } + const all: Array<ProductWithId | undefined> = await Promise.all( + list.body.products.map(async (c) => { + const r = await instance.getProductDetails(token, c.product_id); + if (r.type === "fail") { + return undefined; + } + return { ...r.body, id: c.product_id, serial: c.product_serial }; + }), + ); + const products = all.filter(notUndefined); + + return opFixedSuccess({ products }); + } - return await mutateAll(/.*"\/private\/products.*/); - }; - - const getProduct = async ( - productId: string, - ): Promise<void> => { - await request(`/private/products/${productId}`, { - method: "GET", - }); + const { data, error } = useSWR< + OperationOk<{ products: ProductWithId[] }> | + TalerMerchantManagementErrorsByMethod<"listProducts">, + TalerHttpError + >([session.token, offset, "listProductsWithId"], fetcher); - return - }; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - return { createProduct, updateProduct, deleteProduct, lockProduct, getProduct }; + return buildPaginatedResult(data.body.products, offset, setOffset, (d) => d.serial) } -export function useInstanceProducts(): HttpResponse< - (MerchantBackend.Products.ProductDetail & WithId)[], - MerchantBackend.ErrorDetail -> { - const { fetcher, multiFetcher } = useBackendInstanceRequest(); - - const { data: list, error: listError } = useSWR< - HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/products`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - const paths = (list?.data.products || []).map( - (p) => `/private/products/${p.product_id}`, +export function revalidateProductDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getProductDetails", + undefined, + { revalidate: true }, ); - const { data: products, error: productError } = useSWR< - HttpResponseOk<MerchantBackend.Products.ProductDetail>[], - RequestError<MerchantBackend.ErrorDetail> - >([paths], multiFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (listError) return listError.cause; - if (productError) return productError.cause; - - if (products) { - const dataWithId = products.map((d) => { - //take the id from the queried url - return { - ...d.data, - id: d.info?.url.replace(/.*\/private\/products\//, "") || "", - }; - }); - return { ok: true, data: dataWithId }; - } - return { loading: true }; } +export function useProductDetails(productId: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); + + async function fetcher([pid, token]: [string, AccessToken]) { + return await instance.getProductDetails(token, pid); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getProductDetails">, + TalerHttpError + >([productId, session.token, "getProductDetails"], fetcher); -export function useProductDetails( - productId: string, -): HttpResponse< - MerchantBackend.Products.ProductDetail, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Products.ProductDetail>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/products/${productId}`], fetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts deleted file mode 100644 index b3eecd754..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts +++ /dev/null @@ -1,448 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { - useInstanceReserves, - useReserveDetails, - useReservesAPI, - useRewardDetails, -} from "./reserves.js"; -import { ApiMockEnvironment } from "./testing.js"; -import { - API_AUTHORIZE_REWARD, - API_AUTHORIZE_REWARD_FOR_RESERVE, - API_CREATE_RESERVE, - API_DELETE_RESERVE, - API_GET_RESERVE_BY_ID, - API_GET_REWARD_BY_ID, - API_LIST_RESERVES, -} from "./urls.js"; -import * as tests from "@gnu-taler/web-util/testing"; - -describe("reserve api interaction with listing", () => { - it("should evict cache when creating a reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useInstanceReserves(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [{ reserve_pub: "11" }], - }); - - env.addRequestExpectation(API_CREATE_RESERVE, { - request: { - initial_balance: "ARS:3333", - exchange_url: "http://url", - wire_method: "iban", - }, - response: { - reserve_pub: "22", - accounts: [], - }, - }); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - api.createReserve({ - initial_balance: "ARS:3333", - exchange_url: "http://url", - wire_method: "iban", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when deleting a reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "11", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "33", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useInstanceReserves(); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [ - { reserve_pub: "11" }, - { reserve_pub: "22" }, - { reserve_pub: "33" }, - ], - }); - - env.addRequestExpectation(API_DELETE_RESERVE("11"), {}); - env.addRequestExpectation(API_LIST_RESERVES, { - response: { - reserves: [ - { - reserve_pub: "22", - } as MerchantBackend.Rewards.ReserveStatusEntry, - { - reserve_pub: "33", - } as MerchantBackend.Rewards.ReserveStatusEntry, - ], - }, - }); - - api.deleteReserve("11"); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - reserves: [{ reserve_pub: "22" }, { reserve_pub: "33" }], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("reserve api interaction with details", () => { - it("should evict cache when adding a reward for a specific reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useReserveDetails("11"); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - }); - - env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), { - request: { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }, - response: { - reward_id: "id2", - taler_reward_uri: "uri", - reward_expiration: { t_s: 1 }, - reward_status_url: "url", - }, - }); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - api.authorizeRewardReserve("11", { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); - - it("should evict cache when adding a reward for a random reserve", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const api = useReservesAPI(); - const query = useReserveDetails("11"); - return { query, api }; - }, - {}, - [ - ({ query, api }) => { - expect(query.loading).true; - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }], - }); - - env.addRequestExpectation(API_AUTHORIZE_REWARD, { - request: { - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }, - response: { - reward_id: "id2", - taler_reward_uri: "uri", - reward_expiration: { t_s: 1 }, - reward_status_url: "url", - }, - }); - - env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { - response: { - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - } as MerchantBackend.Rewards.ReserveDetail, - qparam: { - rewards: "yes", - }, - }); - - api.authorizeReward({ - amount: "USD:12", - justification: "not", - next_url: "http://taler.net", - }); - }, - ({ query, api }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - - expect(query.data).deep.equals({ - accounts: [{ payto_uri: "payto://here" }], - rewards: [ - { reason: "why?", reward_id: "id1", total_amount: "USD:10" }, - { reason: "not", reward_id: "id2", total_amount: "USD:12" }, - ], - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); - -describe("reserve api interaction with reward details", () => { - it("should list rewards", async () => { - const env = new ApiMockEnvironment(); - - env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), { - response: { - total_picked_up: "USD:12", - reason: "not", - } as MerchantBackend.Rewards.RewardDetails, - qparam: { - pickups: "yes", - }, - }); - - const hookBehavior = await tests.hookBehaveLikeThis( - () => { - const query = useRewardDetails("11"); - return { query }; - }, - {}, - [ - ({ query }) => { - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ - result: "ok", - }); - expect(query.loading).true; - }, - ({ query }) => { - expect(query.loading).false; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - total_picked_up: "USD:12", - reason: "not", - }); - }, - ], - env.buildTestingContext(), - ); - - expect(hookBehavior).deep.eq({ result: "ok" }); - expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); - }); -}); diff --git a/packages/merchant-backoffice-ui/src/hooks/reserves.ts b/packages/merchant-backoffice-ui/src/hooks/reserves.ts deleted file mode 100644 index b719bfbe6..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/reserves.ts +++ /dev/null @@ -1,181 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 { - HttpResponse, - HttpResponseOk, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { MerchantBackend } from "../declaration.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; - -// FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook, useSWRConfig } from "swr"; -const useSWR = _useSWR as unknown as SWRHook; - -export function useReservesAPI(): ReserveMutateAPI { - const mutateAll = useMatchMutate(); - const { mutate } = useSWRConfig(); - const { request } = useBackendInstanceRequest(); - - const createReserve = async ( - data: MerchantBackend.Rewards.ReserveCreateRequest, - ): Promise< - HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation> - > => { - const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>( - `/private/reserves`, - { - method: "POST", - data, - }, - ); - - //evict reserve list query - await mutateAll(/.*private\/reserves.*/); - - return res; - }; - - const authorizeRewardReserve = async ( - pub: string, - data: MerchantBackend.Rewards.RewardCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { - const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( - `/private/reserves/${pub}/authorize-reward`, - { - method: "POST", - data, - }, - ); - - //evict reserve details query - await mutate([`/private/reserves/${pub}`]); - - return res; - }; - - const authorizeReward = async ( - data: MerchantBackend.Rewards.RewardCreateRequest, - ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => { - const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>( - `/private/rewards`, - { - method: "POST", - data, - }, - ); - - //evict all details query - await mutateAll(/.*private\/reserves\/.*/); - - return res; - }; - - const deleteReserve = async ( - pub: string, - ): Promise<HttpResponse<void, MerchantBackend.ErrorDetail>> => { - const res = await request<void>(`/private/reserves/${pub}`, { - method: "DELETE", - }); - - //evict reserve list query - await mutateAll(/.*private\/reserves.*/); - - return res; - }; - - return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve }; -} - -export interface ReserveMutateAPI { - createReserve: ( - data: MerchantBackend.Rewards.ReserveCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>; - authorizeRewardReserve: ( - id: string, - data: MerchantBackend.Rewards.RewardCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; - authorizeReward: ( - data: MerchantBackend.Rewards.RewardCreateRequest, - ) => Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>>; - deleteReserve: ( - id: string, - ) => Promise<HttpResponse<void, MerchantBackend.ErrorDetail>>; -} - -export function useInstanceReserves(): HttpResponse< - MerchantBackend.Rewards.RewardReserveStatus, - MerchantBackend.ErrorDetail -> { - const { fetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/reserves`], fetcher); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export function useReserveDetails( - reserveId: string, -): HttpResponse< - MerchantBackend.Rewards.ReserveDetail, - MerchantBackend.ErrorDetail -> { - const { reserveDetailFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/reserves/${reserveId}`], reserveDetailFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} - -export function useRewardDetails( - rewardId: string, -): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> { - const { rewardsDetailFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Rewards.RewardDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; - if (data) return data; - if (error) return error.cause; - return { loading: true }; -} diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts index ee8728cc8..12d99f3fc 100644 --- a/packages/merchant-backoffice-ui/src/hooks/templates.ts +++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,254 +13,75 @@ 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 { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { useState } from "preact/hooks"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { buildPaginatedResult } from "./webhooks.js"; const useSWR = _useSWR as unknown as SWRHook; -export function useTemplateAPI(): TemplateAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const createTemplate = async ( - data: MerchantBackend.Template.TemplateAddDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const updateTemplate = async ( - templateId: string, - data: MerchantBackend.Template.TemplatePatchDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates/${templateId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const deleteTemplate = async ( - templateId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates/${templateId}`, { - method: "DELETE", - }); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const createOrderFromTemplate = async ( - templateId: string, - data: MerchantBackend.Template.UsingTemplateDetails, - ): Promise< - HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse> - > => { - const res = await request<MerchantBackend.Template.UsingTemplateResponse>( - `/templates/${templateId}`, - { - method: "POST", - data, - }, - ); - await mutateAll(/.*private\/templates.*/); - return res; - }; - - const testTemplateExist = async ( - templateId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/templates/${templateId}`, { method: "GET", }); - return res; - }; - - - return { - createTemplate, - updateTemplate, - deleteTemplate, - testTemplateExist, - createOrderFromTemplate, - }; -} - -export interface TemplateAPI { - createTemplate: ( - data: MerchantBackend.Template.TemplateAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateTemplate: ( - id: string, - data: MerchantBackend.Template.TemplatePatchDetails, - ) => Promise<HttpResponseOk<void>>; - testTemplateExist: ( - id: string - ) => Promise<HttpResponseOk<void>>; - deleteTemplate: (id: string) => Promise<HttpResponseOk<void>>; - createOrderFromTemplate: ( - id: string, - data: MerchantBackend.Template.UsingTemplateDetails, - ) => Promise<HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>>; -} export interface InstanceTemplateFilter { - //FIXME: add filter to the template list - position?: string; } -export function useInstanceTemplates( - args?: InstanceTemplateFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail -> { - const { templateFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; +export function revalidateInstanceTemplates() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listTemplates", + undefined, + { revalidate: true }, + ); +} +export function useInstanceTemplates() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); - /** - * FIXME: this can be cleaned up a little - * - * the logic of double query should be inside the orderFetch so from the hook perspective and cache - * is just one query and one error status - */ - const { - data: beforeData, - error: beforeError, - isValidating: loadingBefore, - } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail>>( - [ - `/private/templates`, - args?.position, - totalBefore, - ], - templateFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/templates`, args?.position, -totalAfter], templateFetcher); + const [offset, setOffset] = useState<string | undefined>(); - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); + async function fetcher([token, bid]: [AccessToken, string]) { + return await instance.listTemplates(token, { + limit: PAGINATED_LIST_REQUEST, + offset: bid, + order: "dec", + }); + } - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Template.TemplateSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listTemplates">, + TalerHttpError + >([session.token, offset, "listTemplates"], fetcher); - if (beforeError) return beforeError.cause; - if (afterError) return afterError.cause; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.templates.length < totalAfter; - const isReachingStart = args?.position === undefined - || - (beforeData && beforeData.data.templates.length < totalBefore); + return buildPaginatedResult(data.body.templates, offset, setOffset, (d) => d.template_id) - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.templates.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${afterData.data.templates[afterData.data.templates.length - 1] - .template_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.templates.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = `${beforeData.data.templates[beforeData.data.templates.length - 1] - .template_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - }; +} - // const templates = !afterData ? [] : (afterData || lastAfter).data.templates; - const templates = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.templates - .slice() - .reverse() - .concat((afterData || lastAfter).data.templates); - if (loadingAfter || loadingBefore) - return { loading: true, data: { templates } }; - if (beforeData && afterData) { - return { ok: true, data: { templates }, ...pagination }; - } - return { loading: true }; +export function revalidateTemplateDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getTemplateDetails", + undefined, + { revalidate: true }, + ); } +export function useTemplateDetails(templateId: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); -export function useTemplateDetails( - templateId: string, -): HttpResponse< - MerchantBackend.Template.TemplateDetails, - MerchantBackend.ErrorDetail -> { - const { templateFetcher } = useBackendInstanceRequest(); + async function fetcher([tid, token]: [string, AccessToken]) { + return await instance.getTemplateDetails(token, tid); + } - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Template.TemplateDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/templates/${templateId}`], templateFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getTemplateDetails">, + TalerHttpError + >([templateId, session.token, "getTemplateDetails"], fetcher); - if (isValidating) return { loading: true, data: data?.data }; - if (data) { - return data; - } - if (error) return error.cause; - return { loading: true }; + if (data) return data; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx index 3ea22475b..fc78f6c58 100644 --- a/packages/merchant-backoffice-ui/src/hooks/testing.tsx +++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -24,10 +24,22 @@ import { ComponentChildren, FunctionalComponent, h, VNode } from "preact"; import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http"; import { SWRConfig } from "swr"; import { ApiContextProvider } from "@gnu-taler/web-util/browser"; -import { BackendContextProvider } from "../context/backend.js"; -import { InstanceContextProvider } from "../context/instance.js"; -import { HttpResponseOk, RequestOptions } from "@gnu-taler/web-util/browser"; -import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient } from "@gnu-taler/taler-util"; +import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util"; + +interface RequestOptions { + method?: "GET" | "POST" | "HEAD", + params?: any, + token?: string | undefined, + data?: any, +} +interface HttpResponseOk<T> { + ok: true, + data: T, + loading: boolean, + clientError: boolean, + serverError: boolean, + info: any, +} export class ApiMockEnvironment extends MockEnvironment { constructor(debug = false) { @@ -144,21 +156,21 @@ export class ApiMockEnvironment extends MockEnvironment { } const bankCore = new TalerCoreBankHttpClient("http://localhost", mockHttpClient) - const bankIntegration = bankCore.getIntegrationAPI() - const bankRevenue = bankCore.getRevenueAPI("a") - const bankWire = bankCore.getWireGatewayAPI("b") + const bankIntegration = new TalerBankIntegrationHttpClient(bankCore.getIntegrationAPI().href, mockHttpClient) + const bankRevenue = new TalerRevenueHttpClient(bankCore.getRevenueAPI("a").href, mockHttpClient) + const bankWire = new TalerWireGatewayHttpClient(bankCore.getWireGatewayAPI("b").href, "b", mockHttpClient) return ( - <BackendContextProvider defaultUrl="http://backend"> - <InstanceContextProvider - value={{ - token: undefined, - id: "default", - admin: true, - changeToken: () => null, - }} - > - <ApiContextProvider value={{ request, bankCore, bankIntegration, bankRevenue, bankWire }}> + // <BackendContextProvider defaultUrl="http://backend"> + // <InstanceContextProvider + // value={{ + // token: undefined, + // id: "default", + // admin: true, + // changeToken: () => null, + // }} + // > + <ApiContextProvider value={{ request : undefined as any, bankCore, bankIntegration, bankRevenue, bankWire }}> <SC value={{ loadingTimeout: 0, @@ -172,8 +184,8 @@ export class ApiMockEnvironment extends MockEnvironment { {children} </SC> </ApiContextProvider> - </InstanceContextProvider> - </BackendContextProvider> + // </InstanceContextProvider> + // </BackendContextProvider> ); }; } diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts index a7187af27..7daaf5049 100644 --- a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts +++ b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,12 +19,17 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { + AmountString, + PaytoString, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; -import { MerchantBackend } from "../declaration.js"; -import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js"; import { ApiMockEnvironment } from "./testing.js"; -import { useInstanceTransfers, useTransferAPI } from "./transfer.js"; +import { useInstanceTransfers } from "./transfer.js"; +import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js"; +import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; describe("transfer api interaction with listing", () => { it("should evict cache when informing a transfer", async () => { @@ -33,36 +38,36 @@ describe("transfer api interaction with listing", () => { env.addRequestExpectation(API_LIST_TRANSFERS, { qparam: { limit: -20 }, response: { - transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails], + transfers: [{ wtid: "2" } as TalerMerchantApi.TransferDetails], }, }); - const moveCursor = (d: string) => { + const moveCursor = (d: string | undefined) => { console.log("new position", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { const query = useInstanceTransfers({}, moveCursor); - const api = useTransferAPI(); + const { lib: api } = useMerchantApiContext(); return { query, api }; }, {}, [ ({ query, api }) => { - expect(query.loading).true; + // expect(query.loading).true; }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "2" }], - }); + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // transfers: [{ wtid: "2" }], + // }); env.addRequestExpectation(API_INFORM_TRANSFERS, { request: { @@ -81,24 +86,24 @@ describe("transfer api interaction with listing", () => { }, }); - api.informTransfer({ + api.instance.informWireTransfer(undefined, { wtid: "3", - credit_amount: "EUR:1", + credit_amount: "EUR:1" as AmountString, exchange_url: "exchange.url", - payto_uri: "payto://", + payto_uri: "payto://" as PaytoString, }); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "3" }, { wtid: "2" }], - }); + // expect(query.data).deep.equals({ + // transfers: [{ wtid: "3" }, { wtid: "2" }], + // }); }, ], env.buildTestingContext(), @@ -120,12 +125,16 @@ describe("transfer listing pagination", () => { }, }); - const moveCursor = (d: string) => { + const moveCursor = (d: string | undefined) => { console.log("new position", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor); + const query = useInstanceTransfers( + { payto_uri: "payto://" }, + moveCursor, + ); + return { query }; }, {}, [ @@ -133,22 +142,18 @@ describe("transfer listing pagination", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(query.loading).true; + // expect(query.loading).true; }, (query) => { - expect(query.loading).undefined; - expect(query.ok).true; - if (!query.ok) return; - expect(query.data).deep.equals({ - transfers: [{ wtid: "2" }, { wtid: "1" }], - }); - expect(query.isReachingEnd).true; - expect(query.isReachingStart).true; + // expect(query.loading).undefined; + // expect(query.ok).true; + // if (!query.ok) return; + // expect(query.data).deep.equals({ + // transfers: [{ wtid: "2" }, { wtid: "1" }], + // }); + // expect(query.isReachingEnd).true; + // expect(query.isReachingStart).true; - //check that this button won't trigger more updates since - //has reach end and start - query.loadMore(); - query.loadMorePrev(); }, ], env.buildTestingContext(), @@ -158,7 +163,7 @@ describe("transfer listing pagination", () => { expect(hookBehavior).deep.eq({ result: "ok" }); }); - it("should load more if result brings more that PAGE_SIZE", async () => { + it("should load more if result brings more that PAGINATED_LIST_REQUEST", async () => { const env = new ApiMockEnvironment(); const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ @@ -167,7 +172,7 @@ describe("transfer listing pagination", () => { const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ wtid: String(i + 20), })); - const transfersFrom20to0 = [...transfersFrom0to20].reverse(); + // const transfersFrom20to0 = [...transfersFrom0to20].reverse(); env.addRequestExpectation(API_LIST_TRANSFERS, { qparam: { limit: 20, payto_uri: "payto://", offset: "1" }, @@ -183,16 +188,17 @@ describe("transfer listing pagination", () => { }, }); - const moveCursor = (d: string) => { + const moveCursor = (d: string | undefined) => { console.log("new position", d); }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - return useInstanceTransfers( + const query = useInstanceTransfers( { payto_uri: "payto://", position: "1" }, moveCursor, ); + return { query }; }, {}, [ @@ -200,17 +206,17 @@ describe("transfer listing pagination", () => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(result.loading).true; + // expect(result.loading).true; }, (result) => { - expect(result.loading).undefined; - expect(result.ok).true; - if (!result.ok) return; - expect(result.data).deep.equals({ - transfers: [...transfersFrom20to0, ...transfersFrom20to40], - }); - expect(result.isReachingEnd).false; - expect(result.isReachingStart).false; + // expect(result.loading).undefined; + // expect(result.ok).true; + // if (!result.ok) return; + // expect(result.data).deep.equals({ + // transfers: [...transfersFrom20to0, ...transfersFrom20to40], + // }); + // expect(result.isReachingEnd).false; + // expect(result.isReachingStart).false; //query more env.addRequestExpectation(API_LIST_TRANSFERS, { @@ -219,30 +225,30 @@ describe("transfer listing pagination", () => { transfers: [...transfersFrom20to40, { wtid: "41" }], }, }); - result.loadMore(); + // result.loadMore(); }, (result) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(result.loading).true; + // expect(result.loading).true; }, (result) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok", }); - expect(result.loading).undefined; - expect(result.ok).true; - if (!result.ok) return; - expect(result.data).deep.equals({ - transfers: [ - ...transfersFrom20to0, - ...transfersFrom20to40, - { wtid: "41" }, - ], - }); - expect(result.isReachingEnd).true; - expect(result.isReachingStart).false; + // expect(result.loading).undefined; + // expect(result.ok).true; + // if (!result.ok) return; + // expect(result.data).deep.equals({ + // transfers: [ + // ...transfersFrom20to0, + // ...transfersFrom20to40, + // { wtid: "41" }, + // ], + // }); + // expect(result.isReachingEnd).true; + // expect(result.isReachingStart).false; }, ], env.buildTestingContext(), diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts index 27c3bdc75..6f77369c2 100644 --- a/packages/merchant-backoffice-ui/src/hooks/transfer.ts +++ b/packages/merchant-backoffice-ui/src/hooks/transfer.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,176 +13,56 @@ 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 { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { buildPaginatedResult } from "./webhooks.js"; const useSWR = _useSWR as unknown as SWRHook; -export function useTransferAPI(): TransferAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); - - const informTransfer = async ( - data: MerchantBackend.Transfers.TransferInformation, - ): Promise<HttpResponseOk<{}>> => { - const res = await request<{}>(`/private/transfers`, { - method: "POST", - data, - }); - - await mutateAll(/.*private\/transfers.*/); - return res; - }; - - return { informTransfer }; -} - -export interface TransferAPI { - informTransfer: ( - data: MerchantBackend.Transfers.TransferInformation, - ) => Promise<HttpResponseOk<{}>>; -} - export interface InstanceTransferFilter { payto_uri?: string; - verified?: "yes" | "no"; + verified?: boolean; position?: string; } +export function revalidateInstanceTransfers() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listWireTransfers", + undefined, + { revalidate: true }, + ); +} export function useInstanceTransfers( args?: InstanceTransferFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail -> { - const { transferFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position !== undefined ? pageBefore * PAGE_SIZE : 0; - - /** - * FIXME: this can be cleaned up a little - * - * the logic of double query should be inside the orderFetch so from the hook perspective and cache - * is just one query and one error status - */ - const { - data: beforeData, - error: beforeError, - isValidating: loadingBefore, - } = useSWR< - HttpResponseOk<MerchantBackend.Transfers.TransferList>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - totalBefore, - ], - transferFetcher, - ); - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Transfers.TransferList>, - RequestError<MerchantBackend.ErrorDetail> - >( - [ - `/private/transfers`, - args?.payto_uri, - args?.verified, - args?.position, - -totalAfter, - ], - transferFetcher, - ); - - //this will save last result - const [lastBefore, setLastBefore] = useState< - HttpResponse< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Transfers.TransferList, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - if (beforeData) setLastBefore(beforeData); - }, [afterData, beforeData]); + updatePosition: (id: string | undefined) => void = (() => { }), +) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); + + // const [offset, setOffset] = useState<string | undefined>(args?.position); + + async function fetcher([token, o, p, v]: [AccessToken, string, string, boolean]) { + return await instance.listWireTransfers(token, { + paytoURI: p, + verified: v, + limit: PAGINATED_LIST_REQUEST, + offset: o, + order: "dec", + }); + } - if (beforeError) return beforeError.cause; - if (afterError) return afterError.cause; + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listWireTransfers">, + TalerHttpError + >([session.token, args?.position, args?.payto_uri, args?.verified, "listWireTransfers"], fetcher); - // if the query returns less that we ask, then we have reach the end or beginning - const isReachingEnd = - afterData && afterData.data.transfers.length < totalAfter; - const isReachingStart = - args?.position === undefined || - (beforeData && beforeData.data.transfers.length < totalBefore); + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.transfers.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${ - afterData.data.transfers[afterData.data.transfers.length - 1] - .transfer_serial_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - loadMorePrev: () => { - if (!beforeData || isReachingStart) return; - if (beforeData.data.transfers.length < MAX_RESULT_SIZE) { - setPageBefore(pageBefore + 1); - } else if (beforeData) { - const from = `${ - beforeData.data.transfers[beforeData.data.transfers.length - 1] - .transfer_serial_id - }`; - if (from && updatePosition) updatePosition(from); - } - }, - }; + return buildPaginatedResult(data.body.transfers, args?.position, updatePosition, (d) => String(d.transfer_serial_id)) - const transfers = - !beforeData || !afterData - ? [] - : (beforeData || lastBefore).data.transfers - .slice() - .reverse() - .concat((afterData || lastAfter).data.transfers); - if (loadingAfter || loadingBefore) - return { loading: true, data: { transfers } }; - if (beforeData && afterData) { - return { ok: true, data: { transfers }, ...pagination }; - } - return { loading: true }; } diff --git a/packages/merchant-backoffice-ui/src/hooks/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts index b6485259f..95e1c04f3 100644 --- a/packages/merchant-backoffice-ui/src/hooks/urls.ts +++ b/packages/merchant-backoffice-ui/src/hooks/urls.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -18,16 +18,16 @@ * * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { Query } from "@gnu-taler/web-util/testing"; -import { MerchantBackend } from "../declaration.js"; //////////////////// // ORDER //////////////////// export const API_CREATE_ORDER: Query< - MerchantBackend.Orders.PostOrderRequest, - MerchantBackend.Orders.PostOrderResponse + TalerMerchantApi.PostOrderRequest, + TalerMerchantApi.PostOrderResponse > = { method: "POST", url: "http://backend/instances/default/private/orders", @@ -35,14 +35,14 @@ export const API_CREATE_ORDER: Query< export const API_GET_ORDER_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.Orders.MerchantOrderStatusResponse> => ({ +): Query<unknown, TalerMerchantApi.MerchantOrderStatusResponse> => ({ method: "GET", url: `http://backend/instances/default/private/orders/${id}`, }); export const API_LIST_ORDERS: Query< unknown, - MerchantBackend.Orders.OrderHistory + TalerMerchantApi.OrderHistory > = { method: "GET", url: "http://backend/instances/default/private/orders", @@ -51,8 +51,8 @@ export const API_LIST_ORDERS: Query< export const API_REFUND_ORDER_BY_ID = ( id: string, ): Query< - MerchantBackend.Orders.RefundRequest, - MerchantBackend.Orders.MerchantRefundResponse + TalerMerchantApi.RefundRequest, + TalerMerchantApi.MerchantRefundResponse > => ({ method: "POST", url: `http://backend/instances/default/private/orders/${id}/refund`, @@ -60,14 +60,14 @@ export const API_REFUND_ORDER_BY_ID = ( export const API_FORGET_ORDER_BY_ID = ( id: string, -): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ +): Query<TalerMerchantApi.ForgetRequest, unknown> => ({ method: "PATCH", url: `http://backend/instances/default/private/orders/${id}/forget`, }); export const API_DELETE_ORDER = ( id: string, -): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ +): Query<TalerMerchantApi.ForgetRequest, unknown> => ({ method: "DELETE", url: `http://backend/instances/default/private/orders/${id}`, }); @@ -78,14 +78,14 @@ export const API_DELETE_ORDER = ( export const API_LIST_TRANSFERS: Query< unknown, - MerchantBackend.Transfers.TransferList + TalerMerchantApi.TransferList > = { method: "GET", url: "http://backend/instances/default/private/transfers", }; export const API_INFORM_TRANSFERS: Query< - MerchantBackend.Transfers.TransferInformation, + TalerMerchantApi.TransferInformation, {} > = { method: "POST", @@ -97,7 +97,7 @@ export const API_INFORM_TRANSFERS: Query< //////////////////// export const API_CREATE_PRODUCT: Query< - MerchantBackend.Products.ProductAddDetail, + TalerMerchantApi.ProductAddDetail, unknown > = { method: "POST", @@ -106,7 +106,7 @@ export const API_CREATE_PRODUCT: Query< export const API_LIST_PRODUCTS: Query< unknown, - MerchantBackend.Products.InventorySummaryResponse + TalerMerchantApi.InventorySummaryResponse > = { method: "GET", url: "http://backend/instances/default/private/products", @@ -114,7 +114,7 @@ export const API_LIST_PRODUCTS: Query< export const API_GET_PRODUCT_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.Products.ProductDetail> => ({ +): Query<unknown, TalerMerchantApi.ProductDetail> => ({ method: "GET", url: `http://backend/instances/default/private/products/${id}`, }); @@ -122,8 +122,8 @@ export const API_GET_PRODUCT_BY_ID = ( export const API_UPDATE_PRODUCT_BY_ID = ( id: string, ): Query< - MerchantBackend.Products.ProductPatchDetail, - MerchantBackend.Products.InventorySummaryResponse + TalerMerchantApi.ProductPatchDetail, + TalerMerchantApi.InventorySummaryResponse > => ({ method: "PATCH", url: `http://backend/instances/default/private/products/${id}`, @@ -135,67 +135,11 @@ export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({ }); //////////////////// -// RESERVES -//////////////////// - -export const API_CREATE_RESERVE: Query< - MerchantBackend.Rewards.ReserveCreateRequest, - MerchantBackend.Rewards.ReserveCreateConfirmation -> = { - method: "POST", - url: "http://backend/instances/default/private/reserves", -}; -export const API_LIST_RESERVES: Query< - unknown, - MerchantBackend.Rewards.RewardReserveStatus -> = { - method: "GET", - url: "http://backend/instances/default/private/reserves", -}; - -export const API_GET_RESERVE_BY_ID = ( - pub: string, -): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({ - method: "GET", - url: `http://backend/instances/default/private/reserves/${pub}`, -}); - -export const API_GET_REWARD_BY_ID = ( - pub: string, -): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({ - method: "GET", - url: `http://backend/instances/default/private/rewards/${pub}`, -}); - -export const API_AUTHORIZE_REWARD_FOR_RESERVE = ( - pub: string, -): Query< - MerchantBackend.Rewards.RewardCreateRequest, - MerchantBackend.Rewards.RewardCreateConfirmation -> => ({ - method: "POST", - url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`, -}); - -export const API_AUTHORIZE_REWARD: Query< - MerchantBackend.Rewards.RewardCreateRequest, - MerchantBackend.Rewards.RewardCreateConfirmation -> = { - method: "POST", - url: `http://backend/instances/default/private/rewards`, -}; - -export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({ - method: "DELETE", - url: `http://backend/instances/default/private/reserves/${id}`, -}); - -//////////////////// // INSTANCE ADMIN //////////////////// export const API_CREATE_INSTANCE: Query< - MerchantBackend.Instances.InstanceConfigurationMessage, + TalerMerchantApi.InstanceConfigurationMessage, unknown > = { method: "POST", @@ -204,21 +148,21 @@ export const API_CREATE_INSTANCE: Query< export const API_GET_INSTANCE_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.Instances.QueryInstancesResponse> => ({ +): Query<unknown, TalerMerchantApi.QueryInstancesResponse> => ({ method: "GET", url: `http://backend/management/instances/${id}`, }); export const API_GET_INSTANCE_KYC_BY_ID = ( id: string, -): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({ +): Query<unknown, TalerMerchantApi.AccountKycRedirects> => ({ method: "GET", url: `http://backend/management/instances/${id}/kyc`, }); export const API_LIST_INSTANCES: Query< unknown, - MerchantBackend.Instances.InstancesResponse + TalerMerchantApi.InstancesResponse > = { method: "GET", url: "http://backend/management/instances", @@ -227,7 +171,7 @@ export const API_LIST_INSTANCES: Query< export const API_UPDATE_INSTANCE_BY_ID = ( id: string, ): Query< - MerchantBackend.Instances.InstanceReconfigurationMessage, + TalerMerchantApi.InstanceReconfigurationMessage, unknown > => ({ method: "PATCH", @@ -237,7 +181,7 @@ export const API_UPDATE_INSTANCE_BY_ID = ( export const API_UPDATE_INSTANCE_AUTH_BY_ID = ( id: string, ): Query< - MerchantBackend.Instances.InstanceAuthConfigurationMessage, + TalerMerchantApi.InstanceAuthConfigurationMessage, unknown > => ({ method: "POST", @@ -250,24 +194,12 @@ export const API_DELETE_INSTANCE = (id: string): Query<unknown, unknown> => ({ }); //////////////////// -// AUTH -//////////////////// - -export const API_NEW_LOGIN: Query< - MerchantBackend.Instances.LoginTokenRequest, - unknown -> = ({ - method: "POST", - url: `http://backend/private/token`, -}); - -//////////////////// // INSTANCE //////////////////// export const API_GET_CURRENT_INSTANCE: Query< unknown, - MerchantBackend.Instances.QueryInstancesResponse + TalerMerchantApi.QueryInstancesResponse > = { method: "GET", url: `http://backend/instances/default/private/`, @@ -275,14 +207,14 @@ export const API_GET_CURRENT_INSTANCE: Query< export const API_GET_CURRENT_INSTANCE_KYC: Query< unknown, - MerchantBackend.KYC.AccountKycRedirects + TalerMerchantApi.AccountKycRedirects > = { method: "GET", url: `http://backend/instances/default/private/kyc`, }; export const API_UPDATE_CURRENT_INSTANCE: Query< - MerchantBackend.Instances.InstanceReconfigurationMessage, + TalerMerchantApi.InstanceReconfigurationMessage, unknown > = { method: "PATCH", @@ -290,7 +222,7 @@ export const API_UPDATE_CURRENT_INSTANCE: Query< }; export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query< - MerchantBackend.Instances.InstanceAuthConfigurationMessage, + TalerMerchantApi.InstanceAuthConfigurationMessage, unknown > = { method: "POST", diff --git a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts deleted file mode 100644 index 8c1ebd9f6..000000000 --- a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2022 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 { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; -import { - Codec, - buildCodecForObject, - codecForBoolean, - codecForConstString, - codecForEither, - codecForString, -} from "@gnu-taler/taler-util"; - -export interface Settings { - advanceOrderMode: boolean; - dateFormat: "ymd" | "dmy" | "mdy"; -} - -const defaultSettings: Settings = { - advanceOrderMode: false, - dateFormat: "ymd", -} - -export const codecForSettings = (): Codec<Settings> => - buildCodecForObject<Settings>() - .property("advanceOrderMode", codecForBoolean()) - .property("dateFormat", codecForEither( - codecForConstString("ymd"), - codecForConstString("dmy"), - codecForConstString("mdy"), - )) - .build("Settings"); - -const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings()); - -export function useSettings(): [ - Readonly<Settings>, - (s: Settings) => void, -] { - const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings); - - // const parsed: Settings = value ?? defaultSettings; - // function updateField<T extends keyof Settings>(k: T, v: Settings[T]) { - // const next = { ...parsed, [k]: v } - // update(next); - // } - return [value, update]; -} - -export function dateFormatForSettings(s: Settings): string { - switch (s.dateFormat) { - case "ymd": return "yyyy/MM/dd" - case "dmy": return "dd/MM/yyyy" - case "mdy": return "MM/dd/yyyy" - } -} - -export function datetimeFormatForSettings(s: Settings): string { - return dateFormatForSettings(s) + " HH:mm:ss" -}
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts index ad6bf96e2..fe37162aa 100644 --- a/packages/merchant-backoffice-ui/src/hooks/webhooks.ts +++ b/packages/merchant-backoffice-ui/src/hooks/webhooks.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,166 +13,106 @@ 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 { - HttpResponse, - HttpResponseOk, - HttpResponsePaginated, - RequestError, -} from "@gnu-taler/web-util/browser"; -import { useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../declaration.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useBackendInstanceRequest, useMatchMutate } from "./backend.js"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; // FIX default import https://github.com/microsoft/TypeScript/issues/49189 -import _useSWR, { SWRHook } from "swr"; +import { AccessToken, OperationOk, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; const useSWR = _useSWR as unknown as SWRHook; -export function useWebhookAPI(): WebhookAPI { - const mutateAll = useMatchMutate(); - const { request } = useBackendInstanceRequest(); +export interface InstanceWebhookFilter { +} - const createWebhook = async ( - data: MerchantBackend.Webhooks.WebhookAddDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks`, { - method: "POST", - data, - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; +export function revalidateInstanceWebhooks() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listWebhooks", + undefined, + { revalidate: true }, + ); +} +export function useInstanceWebhooks() { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); - const updateWebhook = async ( - webhookId: string, - data: MerchantBackend.Webhooks.WebhookPatchDetails, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks/${webhookId}`, { - method: "PATCH", - data, - }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; + // const [offset, setOffset] = useState<string | undefined>(); - const deleteWebhook = async ( - webhookId: string, - ): Promise<HttpResponseOk<void>> => { - const res = await request<void>(`/private/webhooks/${webhookId}`, { - method: "DELETE", + async function fetcher([token, _bid]: [AccessToken, string]) { + return await instance.listWebhooks(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid, + // order: "dec", }); - await mutateAll(/.*private\/webhooks.*/); - return res; - }; + } - return { createWebhook, updateWebhook, deleteWebhook }; -} + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listWebhooks">, + TalerHttpError + >([session.token, "offset", "listWebhooks"], fetcher); -export interface WebhookAPI { - createWebhook: ( - data: MerchantBackend.Webhooks.WebhookAddDetails, - ) => Promise<HttpResponseOk<void>>; - updateWebhook: ( - id: string, - data: MerchantBackend.Webhooks.WebhookPatchDetails, - ) => Promise<HttpResponseOk<void>>; - deleteWebhook: (id: string) => Promise<HttpResponseOk<void>>; + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + // return buildPaginatedResult(data.body.webhooks, offset, setOffset, (d) => d.webhook_id) + return data; } -export interface InstanceWebhookFilter { - //FIXME: add filter to the webhook list - position?: string; +type PaginatedResult<T> = OperationOk<T> & { + isLastPage: boolean; + isFirstPage: boolean; + loadNext(): void; + loadFirst(): void; } -export function useInstanceWebhooks( - args?: InstanceWebhookFilter, - updatePosition?: (id: string) => void, -): HttpResponsePaginated< - MerchantBackend.Webhooks.WebhookSummaryResponse, - MerchantBackend.ErrorDetail -> { - const { webhookFetcher } = useBackendInstanceRequest(); - - const [pageBefore, setPageBefore] = useState(1); - const [pageAfter, setPageAfter] = useState(1); - - const totalAfter = pageAfter * PAGE_SIZE; - const totalBefore = args?.position ? pageBefore * PAGE_SIZE : 0; - - const { - data: afterData, - error: afterError, - isValidating: loadingAfter, - } = useSWR< - HttpResponseOk<MerchantBackend.Webhooks.WebhookSummaryResponse>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/webhooks`, args?.position, -totalAfter], webhookFetcher); - - const [lastAfter, setLastAfter] = useState< - HttpResponse< - MerchantBackend.Webhooks.WebhookSummaryResponse, - MerchantBackend.ErrorDetail - > - >({ loading: true }); - useEffect(() => { - if (afterData) setLastAfter(afterData); - }, [afterData]); - - if (afterError) return afterError.cause; - - const isReachingEnd = - afterData && afterData.data.webhooks.length < totalAfter; - const isReachingStart = true; - - const pagination = { - isReachingEnd, - isReachingStart, - loadMore: () => { - if (!afterData || isReachingEnd) return; - if (afterData.data.webhooks.length < MAX_RESULT_SIZE) { - setPageAfter(pageAfter + 1); - } else { - const from = `${ - afterData.data.webhooks[afterData.data.webhooks.length - 1].webhook_id - }`; - if (from && updatePosition) updatePosition(from); - } +//TODO: consider sending this to web-util +export function buildPaginatedResult<R, OffId>(data: R[], offset: OffId | undefined, setOffset: (o: OffId | undefined) => void, getId: (r: R) => OffId): PaginatedResult<R[]> { + + const isLastPage = data.length < PAGINATED_LIST_REQUEST; + const isFirstPage = offset === undefined; + + const result = structuredClone(data); + if (result.length == PAGINATED_LIST_REQUEST) { + result.pop(); + } + return { + type: "ok", + body: result, + isLastPage, + isFirstPage, + loadNext: () => { + if (!result.length) return; + const id = getId(result[result.length - 1]) + setOffset(id); }, - loadMorePrev: () => { - return; + loadFirst: () => { + setOffset(undefined); }, }; +} - const webhooks = !afterData ? [] : (afterData || lastAfter).data.webhooks; - if (loadingAfter) return { loading: true, data: { webhooks } }; - if (afterData) { - return { ok: true, data: { webhooks }, ...pagination }; - } - return { loading: true }; +export function revalidateWebhookDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getWebhookDetails", + undefined, + { revalidate: true }, + ); } +export function useWebhookDetails(webhookId: string) { + const { state: session } = useSessionContext(); + const { lib: { instance } } = useSessionContext(); + + async function fetcher([hookId, token]: [string, AccessToken]) { + return await instance.getWebhookDetails(token, hookId); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getWebhookDetails">, + TalerHttpError + >([webhookId, session.token, "getWebhookDetails"], fetcher); -export function useWebhookDetails( - webhookId: string, -): HttpResponse< - MerchantBackend.Webhooks.WebhookDetails, - MerchantBackend.ErrorDetail -> { - const { webhookFetcher } = useBackendInstanceRequest(); - - const { data, error, isValidating } = useSWR< - HttpResponseOk<MerchantBackend.Webhooks.WebhookDetails>, - RequestError<MerchantBackend.ErrorDetail> - >([`/private/webhooks/${webhookId}`], webhookFetcher, { - refreshInterval: 0, - refreshWhenHidden: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }); - - if (isValidating) return { loading: true, data: data?.data }; if (data) return data; - if (error) return error.cause; - return { loading: true }; + if (error) return error; + return undefined; } diff --git a/packages/merchant-backoffice-ui/src/i18n/de.po b/packages/merchant-backoffice-ui/src/i18n/de.po index 2cf0a7c1c..f34d5dd20 100644 --- a/packages/merchant-backoffice-ui/src/i18n/de.po +++ b/packages/merchant-backoffice-ui/src/i18n/de.po @@ -17,7 +17,7 @@ msgstr "" "Project-Id-Version: Taler Wallet\n" "Report-Msgid-Bugs-To: taler@gnu.org\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n" -"PO-Revision-Date: 2023-12-04 13:44+0000\n" +"PO-Revision-Date: 2024-03-21 21:39+0000\n" "Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Language-Team: German <https://weblate.taler.net/projects/gnu-taler/" "merchant-backoffice/de/>\n" @@ -56,7 +56,7 @@ msgstr "" #: src/components/modal/index.tsx:190 #, c-format msgid "Confirm" -msgstr "" +msgstr "Bestätigen" #: src/components/modal/index.tsx:296 #, c-format @@ -440,7 +440,7 @@ msgstr "" #: src/components/form/InputTaxes.tsx:119 #, c-format msgid "Amount" -msgstr "" +msgstr "Betrag" #: src/components/form/InputTaxes.tsx:120 #, c-format @@ -888,7 +888,7 @@ msgstr "" #: src/paths/instance/orders/list/Table.tsx:154 #, c-format msgid "Date" -msgstr "" +msgstr "Datum" #: src/paths/instance/orders/list/Table.tsx:200 #, c-format @@ -1634,7 +1634,7 @@ msgstr "" #: src/paths/instance/reserves/details/DetailPage.tsx:119 #, c-format msgid "Subject" -msgstr "" +msgstr "Verwendungszweck" #: src/paths/instance/reserves/details/DetailPage.tsx:130 #, c-format @@ -2318,22 +2318,22 @@ msgstr "" #: src/components/form/InputPaytoForm.tsx:118 #, c-format msgid "IBAN numbers usually have more that 4 digits" -msgstr "" +msgstr "IBAN-Nummern haben normalerweise mehr als 4 Ziffern" #: src/components/form/InputPaytoForm.tsx:120 #, c-format msgid "IBAN numbers usually have less that 34 digits" -msgstr "" +msgstr "IBAN-Nummern haben normalerweise weniger als 34 Ziffern" #: src/components/form/InputPaytoForm.tsx:128 #, c-format msgid "IBAN country code not found" -msgstr "" +msgstr "IBAN-Ländercode wurde nicht gefunden" #: src/components/form/InputPaytoForm.tsx:153 #, c-format msgid "IBAN number is not valid, checksum is wrong" -msgstr "" +msgstr "IBAN-Nummer ist ungültig, die Prüfsumme ist falsch" #: src/components/form/InputPaytoForm.tsx:248 #, c-format diff --git a/packages/merchant-backoffice-ui/src/i18n/es.po b/packages/merchant-backoffice-ui/src/i18n/es.po index 10ec0cf3b..2c4bc64a7 100644 --- a/packages/merchant-backoffice-ui/src/i18n/es.po +++ b/packages/merchant-backoffice-ui/src/i18n/es.po @@ -17,8 +17,8 @@ msgstr "" "Project-Id-Version: Taler Wallet\n" "Report-Msgid-Bugs-To: taler@gnu.org\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n" -"PO-Revision-Date: 2023-08-13 10:14+0000\n" -"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n" +"PO-Revision-Date: 2024-02-13 14:40+0000\n" +"Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/" "merchant-backoffice/es/>\n" "Language: es\n" @@ -26,7 +26,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.13.1\n" +"X-Generator: Weblate 5.2.1\n" #: src/components/modal/index.tsx:71 #, c-format @@ -41,7 +41,7 @@ msgstr "%1$s" #: src/components/modal/index.tsx:84 #, c-format msgid "Close" -msgstr "" +msgstr "Cerrar" #: src/components/modal/index.tsx:124 #, c-format @@ -165,7 +165,7 @@ msgstr "Instancias" #: src/paths/admin/list/TableActive.tsx:93 #, c-format msgid "Delete" -msgstr "Eliminar" +msgstr "Borrar" #: src/paths/admin/list/TableActive.tsx:99 #, c-format @@ -558,9 +558,9 @@ msgid "required" msgstr "requerido" #: src/paths/instance/orders/create/CreatePage.tsx:157 -#, fuzzy, c-format +#, c-format msgid "not valid" -msgstr "no es un json válido" +msgstr "no válido" #: src/paths/instance/orders/create/CreatePage.tsx:159 #, c-format @@ -2338,22 +2338,22 @@ msgstr "Esta no es una dirección de Ethereum válida." #: src/components/form/InputPaytoForm.tsx:118 #, c-format msgid "IBAN numbers usually have more that 4 digits" -msgstr "Números IBAN usualmente tienen más de 4 dÃgitos" +msgstr "Los números IBAN usualmente tienen mas de 4 digitos" #: src/components/form/InputPaytoForm.tsx:120 #, c-format msgid "IBAN numbers usually have less that 34 digits" -msgstr "Número IBAN usualmente tienen menos de 34 dÃgitos" +msgstr "Los números IBAN usualmente tienen menos de 34 digitos" #: src/components/form/InputPaytoForm.tsx:128 #, c-format msgid "IBAN country code not found" -msgstr "Código IBAN de paÃs no encontrado" +msgstr "Código de pais de IBAN no encontrado" #: src/components/form/InputPaytoForm.tsx:153 #, c-format msgid "IBAN number is not valid, checksum is wrong" -msgstr "Número IBAN no es válido, la suma de verificación es incorrecta" +msgstr "El número IBAN no es válido, falló la verificación" #: src/components/form/InputPaytoForm.tsx:248 #, c-format @@ -2460,7 +2460,7 @@ msgstr "" #: src/components/instance/DefaultInstanceFormFields.tsx:64 #, c-format msgid "Email" -msgstr "" +msgstr "Correo eletrónico" #: src/components/instance/DefaultInstanceFormFields.tsx:65 #, c-format diff --git a/packages/merchant-backoffice-ui/src/i18n/fr.po b/packages/merchant-backoffice-ui/src/i18n/fr.po index d8d0bae29..4da5c5b59 100644 --- a/packages/merchant-backoffice-ui/src/i18n/fr.po +++ b/packages/merchant-backoffice-ui/src/i18n/fr.po @@ -12,25 +12,26 @@ # You should have received a copy of the GNU General Public License along with # TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> # -#, fuzzy msgid "" msgstr "" "Project-Id-Version: Taler Wallet\n" "Report-Msgid-Bugs-To: taler@gnu.org\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" -"Language-Team: LANGUAGE <LL@li.org>\n" -"Language: \n" +"PO-Revision-Date: 2024-02-28 08:07+0000\n" +"Last-Translator: d0p1 <contact@d0p1.eu>\n" +"Language-Team: French <https://weblate.taler.net/projects/gnu-taler/" +"merchant-backoffice/fr/>\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=2; plural=(n!=1);\n" +"X-Generator: Weblate 5.2.1\n" #: src/components/modal/index.tsx:71 #, c-format msgid "Cancel" -msgstr "" +msgstr "Annuler" #: src/components/modal/index.tsx:79 #, c-format @@ -40,12 +41,12 @@ msgstr "" #: src/components/modal/index.tsx:84 #, c-format msgid "Close" -msgstr "" +msgstr "Fermer" #: src/components/modal/index.tsx:124 #, c-format msgid "Continue" -msgstr "" +msgstr "Continuer" #: src/components/modal/index.tsx:178 #, c-format @@ -55,7 +56,7 @@ msgstr "" #: src/components/modal/index.tsx:190 #, c-format msgid "Confirm" -msgstr "" +msgstr "Confirmer" #: src/components/modal/index.tsx:296 #, c-format @@ -65,7 +66,7 @@ msgstr "" #: src/components/modal/index.tsx:299 #, c-format msgid "cannot be empty" -msgstr "" +msgstr "ne peux pas être vide" #: src/components/modal/index.tsx:301 #, c-format diff --git a/packages/merchant-backoffice-ui/src/index.html b/packages/merchant-backoffice-ui/src/index.html index 3e027f056..b005f967d 100644 --- a/packages/merchant-backoffice-ui/src/index.html +++ b/packages/merchant-backoffice-ui/src/index.html @@ -1,6 +1,6 @@ <!-- This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -35,7 +35,7 @@ <title>Merchant Backoffice</title> <!-- Optional customization script. --> <script src="merchant-backoffice-ui-settings.js"></script> - <!-- Entry point for the demobank SPA. --> + <!-- Entry point for the SPA. --> <script type="module" src="index.js"></script> <link rel="stylesheet" href="index.css" /> </head> diff --git a/packages/merchant-backoffice-ui/src/index.tsx b/packages/merchant-backoffice-ui/src/index.tsx index 87ed8e26f..46a3223bb 100644 --- a/packages/merchant-backoffice-ui/src/index.tsx +++ b/packages/merchant-backoffice-ui/src/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -21,4 +21,8 @@ import "./scss/main.scss"; const app = document.getElementById("app"); -render(<Application />, app as any); +if (app) { + render(<Application />, app); +} else { + console.error("HTML element with id 'app' not found."); +} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx index 91b6b4b56..54d947e14 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,8 +19,8 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; -import { ConfigContextProvider } from "../../../context/config.js"; +import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser"; +import { FunctionalComponent, h } from "preact"; import { CreatePage as TestedComponent } from "./CreatePage.js"; export default { @@ -37,14 +37,33 @@ function createExample<Props>( props: Partial<Props>, ) { const r = (args: any) => ( - <ConfigContextProvider + <MerchantApiProviderTesting value={{ - currency: "ARS", - version: "1", + cancelRequest: () => {}, + changeBackend: () => {}, + config: { + currency: "ARS", + version: "1", + currencies: { + "ASD": { + name: "testkudos", + alt_unit_names: {}, + num_fractional_input_digits: 1, + num_fractional_normal_digits: 1, + num_fractional_trailing_zero_digits: 1, + } + }, + exchanges: [], + name: "taler-merchant" + }, + hints: [], + lib: {} as any, + onActivity: (() => {}) as any, + url: new URL("asdasd"), }} > <Component {...args} /> - </ConfigContextProvider> + </MerchantApiProviderTesting> ); r.args = props; return r; diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx index 093c24c3d..4a5ab440b 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,6 +19,11 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { + Duration, + TalerMerchantApi, + createAccessToken, +} from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -28,17 +33,21 @@ import { FormProvider, } from "../../../components/form/FormProvider.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; -import { MerchantBackend } from "../../../declaration.js"; +import { SetTokenNewInstanceModal } from "../../../components/modal/index.js"; import { INSTANCE_ID_REGEX } from "../../../utils/constants.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; -import { SetTokenNewInstanceModal } from "../../../components/modal/index.js"; -export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & { +export type Entity = Omit< + Omit<TalerMerchantApi.InstanceConfigurationMessage, "default_pay_delay">, + "default_wire_transfer_delay" +> & { auth_token?: string; + default_pay_delay: Duration; + default_wire_transfer_delay: Duration; }; interface Props { - onCreate: (d: Entity) => Promise<void>; + onCreate: (d: TalerMerchantApi.InstanceConfigurationMessage) => Promise<void>; onBack?: () => void; forceId?: string; } @@ -49,8 +58,8 @@ function with_defaults(id?: string): Partial<Entity> { // accounts: [], user_type: "business", use_stefan: true, - default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours - default_wire_transfer_delay: { d_us: 1000 * 2 * 60 * 60 * 24 * 1000 }, // two days + default_pay_delay: { d_ms: 2 * 60 * 60 * 1000 }, // two hours + default_wire_transfer_delay: { d_ms: 2 * 60 * 60 * 24 * 1000 }, // two days }; } @@ -88,10 +97,11 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { default_pay_delay: !value.default_pay_delay ? i18n.str`required` : !!value.default_wire_transfer_delay && - value.default_wire_transfer_delay.d_us !== "forever" && - value.default_pay_delay.d_us !== "forever" && - value.default_pay_delay.d_us > value.default_wire_transfer_delay.d_us ? - i18n.str`pay delay can't be greater than wire transfer delay` : undefined, + value.default_wire_transfer_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms + ? i18n.str`pay delay can't be greater than wire transfer delay` + : undefined, default_wire_transfer_delay: !value.default_wire_transfer_delay ? i18n.str`required` : undefined, @@ -110,21 +120,35 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { }; const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); const submit = (): Promise<void> => { // use conversion instead of this - const newToken = value.auth_token; - value.auth_token = undefined; - value.auth = newToken === null || newToken === undefined - ? { method: "external" } - : { method: "token", token: `secret-token:${newToken}` }; - if (!value.address) value.address = {}; - if (!value.jurisdiction) value.jurisdiction = {}; + const newValue = structuredClone(value); + + const newToken = newValue.auth_token; + newValue.auth_token = undefined; + newValue.auth = + newToken === null || newToken === undefined + ? { method: "external" } + : { method: "token", token: createAccessToken(newToken) }; + if (!newValue.address) newValue.address = {}; + if (!newValue.jurisdiction) newValue.jurisdiction = {}; // remove above use conversion // schema.validateSync(value, { abortEarly: false }) - return onCreate(value as Entity); + newValue.default_pay_delay = Duration.toTalerProtocolDuration( + newValue.default_pay_delay!, + ) as any; + newValue.default_wire_transfer_delay = Duration.toTalerProtocolDuration( + newValue.default_wire_transfer_delay!, + ) as any; + // delete value.default_pay_delay; + // delete value.default_wire_transfer_delay; + + return onCreate( + newValue as any as TalerMerchantApi.InstanceConfigurationMessage, + ); }; function updateToken(token: string | null) { @@ -174,54 +198,54 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode { </FormProvider> <div class="level"> - <div class="level-item has-text-centered"> - <h1 class="title"> - <button - class={ - !isTokenSet - ? "button is-danger has-tooltip-bottom" - : !value.auth_token - ? "button has-tooltip-bottom" - : "button is-info has-tooltip-bottom" - } - data-tooltip={i18n.str`change authorization configuration`} - onClick={() => updateIsTokenDialogActive(true)} - > - <div class="icon is-centered"> - <i class="mdi mdi-lock-reset" /> - </div> - <span> - <i18n.Translate>Set access token</i18n.Translate> - </span> - </button> - </h1> + <div class="level-item has-text-centered"> + <h1 class="title"> + <button + class={ + !isTokenSet + ? "button is-danger has-tooltip-bottom" + : !value.auth_token + ? "button has-tooltip-bottom" + : "button is-info has-tooltip-bottom" + } + data-tooltip={i18n.str`change authorization configuration`} + onClick={() => updateIsTokenDialogActive(true)} + > + <div class="icon is-centered"> + <i class="mdi mdi-lock-reset" /> + </div> + <span> + <i18n.Translate>Set access token</i18n.Translate> + </span> + </button> + </h1> + </div> </div> - </div> - <div class="level"> - <div class="level-item has-text-centered"> - {!isTokenSet ? ( - <p class="is-size-6"> - <i18n.Translate> - Access token is not yet configured. This instance can't be - created. - </i18n.Translate> - </p> - ) : value.auth_token === undefined ? ( - <p class="is-size-6"> - <i18n.Translate> - No access token. Authorization must be handled externally. - </i18n.Translate> - </p> - ) : ( - <p class="is-size-6"> - <i18n.Translate> - Access token is set. Authorization is handled by the - merchant backend. - </i18n.Translate> - </p> - )} + <div class="level"> + <div class="level-item has-text-centered"> + {!isTokenSet ? ( + <p class="is-size-6"> + <i18n.Translate> + Access token is not yet configured. This instance can't be + created. + </i18n.Translate> + </p> + ) : value.auth_token === undefined ? ( + <p class="is-size-6"> + <i18n.Translate> + No access token. Authorization must be handled externally. + </i18n.Translate> + </p> + ) : ( + <p class="is-size-6"> + <i18n.Translate> + Access token is set. Authorization is handled by the + merchant backend. + </i18n.Translate> + </p> + )} + </div> </div> - </div> <div class="buttons is-right mt-5"> {onBack && ( <button class="button" onClick={onBack}> diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx index c620c6482..939f9b06a 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/InstanceCreatedSuccessfully.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx index 23f41ecff..b00cfbe7d 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -17,30 +17,29 @@ * * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../components/menu/index.js"; -import { AccessToken, MerchantBackend } from "../../../declaration.js"; -import { useAdminAPI, useInstanceAPI } from "../../../hooks/instance.js"; +import { useSessionContext } from "../../../context/session.js"; import { Notification } from "../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -import { useCredentialsChecker } from "../../../hooks/backend.js"; -import { useBackendContext } from "../../../context/backend.js"; interface Props { onBack?: () => void; onConfirm: () => void; forceId?: string; } -export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage; +export type Entity = TalerMerchantApi.InstanceConfigurationMessage; export default function Create({ onBack, onConfirm, forceId }: Props): VNode { - const { createInstance } = useAdminAPI(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const { requestNewLoginToken } = useCredentialsChecker() - const { url: backendURL, updateToken } = useBackendContext() + const { lib } = useSessionContext(); + const { state, logIn } = useSessionContext(); return ( <Fragment> @@ -50,19 +49,28 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode { onBack={onBack} forceId={forceId} onCreate={async ( - d: MerchantBackend.Instances.InstanceConfigurationMessage, + d: TalerMerchantApi.InstanceConfigurationMessage, ) => { + if (state.status !== "loggedIn") return; try { - await createInstance(d) + await lib.instance.createInstance(state.token, d); if (d.auth.token) { - const resp = await requestNewLoginToken(backendURL, d.auth.token as AccessToken) - if (resp.valid) { - const { token, expiration } = resp - updateToken({ token, expiration }); - } else { - updateToken(undefined) + //if auth has been updated, request a new access token + const result = await lib.authenticate.createAccessTokenBearer( + d.auth.token, + { + scope: "write", + duration: { + d_us: "forever", + }, + refreshable: true, + }, + ); + if (result.type === "ok") { + const { token } = result.body; + logIn(token); } - } + } onConfirm(); } catch (ex) { if (ex instanceof Error) { @@ -72,7 +80,7 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode { description: ex.message, }); } else { - console.error(ex) + console.error(ex); } } }} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx index 0012f9b9b..d4258058b 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,9 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; -import { ConfigContextProvider } from "../../../context/config.js"; +import { FunctionalComponent, h } from "preact"; import { CreatePage as TestedComponent } from "./CreatePage.js"; +import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser"; export default { title: "Pages/Instance/Create", @@ -37,14 +37,33 @@ function createExample<Props>( props: Partial<Props>, ) { const component = (args: any) => ( - <ConfigContextProvider + <MerchantApiProviderTesting value={{ - currency: "TESTKUDOS", - version: "1", + cancelRequest: () => {}, + changeBackend: () => {}, + config: { + currency: "ARS", + version: "1", + currencies: { + "ASD": { + name: "testkudos", + alt_unit_names: {}, + num_fractional_input_digits: 1, + num_fractional_normal_digits: 1, + num_fractional_trailing_zero_digits: 1, + } + }, + exchanges: [], + name: "taler-merchant" + }, + hints: [], + lib: {} as any, + onActivity: (() => {}) as any, + url: new URL("asdasd"), }} > <Internal {...(props as any)} /> - </ConfigContextProvider> + </MerchantApiProviderTesting> ); return { component, props }; } diff --git a/packages/merchant-backoffice-ui/src/paths/admin/index.stories.ts b/packages/merchant-backoffice-ui/src/paths/admin/index.stories.ts index fdae1a24d..3c16c0e57 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/index.stories.ts +++ b/packages/merchant-backoffice-ui/src/paths/admin/index.stories.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx index 885a351d2..cff3c5a02 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,19 +19,21 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; import { StateUpdater, useEffect, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../declaration.js"; +import { useSessionContext } from "../../../context/session.js"; interface Props { - instances: MerchantBackend.Instances.Instance[]; + instances: TalerMerchantApi.Instance[]; onUpdate: (id: string) => void; - onDelete: (id: MerchantBackend.Instances.Instance) => void; - onPurge: (id: MerchantBackend.Instances.Instance) => void; + onDelete: (id: TalerMerchantApi.Instance) => void; + onPurge: (id: TalerMerchantApi.Instance) => void; onCreate: () => void; selected?: boolean; - setInstanceName: (s: string) => void; } export function CardTable({ @@ -39,7 +41,6 @@ export function CardTable({ onCreate, onUpdate, onPurge, - setInstanceName, onDelete, selected, }: Props): VNode { @@ -114,7 +115,6 @@ export function CardTable({ instances={instances} onPurge={onPurge} onUpdate={onUpdate} - setInstanceName={setInstanceName} onDelete={onDelete} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} @@ -130,12 +130,11 @@ export function CardTable({ } interface TableProps { rowSelection: string[]; - instances: MerchantBackend.Instances.Instance[]; + instances: TalerMerchantApi.Instance[]; onUpdate: (id: string) => void; - onDelete: (id: MerchantBackend.Instances.Instance) => void; - onPurge: (id: MerchantBackend.Instances.Instance) => void; + onDelete: (id: TalerMerchantApi.Instance) => void; + onPurge: (id: TalerMerchantApi.Instance) => void; rowSelectionHandler: StateUpdater<string[]>; - setInstanceName: (s: string) => void; } function toggleSelected<T>(id: T): (prev: T[]) => T[] { @@ -146,13 +145,14 @@ function toggleSelected<T>(id: T): (prev: T[]) => T[] { function Table({ rowSelection, rowSelectionHandler, - setInstanceName, instances, onUpdate, onDelete, onPurge, }: TableProps): VNode { const { i18n } = useTranslationContext(); + const { lib } = useSessionContext(); + const { impersonate } = useSessionContext(); return ( <div class="table-container"> <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -201,9 +201,12 @@ function Table({ </td> <td> <a - href={`#/orders?instance=${i.id}`} - onClick={(e) => { - setInstanceName(i.id); + href={`#/orders`} + onClick={async (_e) => { + // e.preventDefault(); + const newInstanceApi = lib.subInstanceApi(i.id); + //not checking /config since this comes from instance list + impersonate(new URL(newInstanceApi.instance.baseUrl)); }} > {i.id} @@ -254,7 +257,7 @@ function EmptyTable(): VNode { <div class="content has-text-grey has-text-centered"> <p> <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> + <i class="mdi mdi-magnify mdi-48px" /> </span> </p> <p> @@ -267,7 +270,7 @@ function EmptyTable(): VNode { } interface Actions { - element: MerchantBackend.Instances.Instance; + element: TalerMerchantApi.Instance; type: "DELETE" | "UPDATE"; } @@ -276,7 +279,7 @@ function notEmpty<TValue>(value: TValue | null | undefined): value is TValue { } function buildActions( - instances: MerchantBackend.Instances.Instance[], + instances: TalerMerchantApi.Instance[], selected: string[], action: "DELETE", ): Actions[] { diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx index e0f5d5430..c4c0996f6 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx index b59112338..940d14334 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/View.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,20 +19,19 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; -import { MerchantBackend } from "../../../declaration.js"; import { CardTable as CardTableActive } from "./TableActive.js"; interface Props { - instances: MerchantBackend.Instances.Instance[]; + instances: TalerMerchantApi.Instance[]; onCreate: () => void; onUpdate: (id: string) => void; - onDelete: (id: MerchantBackend.Instances.Instance) => void; - onPurge: (id: MerchantBackend.Instances.Instance) => void; + onDelete: (id: TalerMerchantApi.Instance) => void; + onPurge: (id: TalerMerchantApi.Instance) => void; selected?: boolean; - setInstanceName: (s: string) => void; } export function View({ @@ -41,7 +40,6 @@ export function View({ onDelete, onPurge, onUpdate, - setInstanceName, selected, }: Props): VNode { const [show, setShow] = useState<"active" | "deleted" | null>("active"); @@ -100,7 +98,6 @@ export function View({ instances={showingInstances} onDelete={onDelete} onPurge={onPurge} - setInstanceName={setInstanceName} onUpdate={onUpdate} selected={selected} onCreate={onCreate} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx index 2f839291b..5b492e45c 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,74 +19,66 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../components/exception/loading.js"; import { NotificationCard } from "../../../components/menu/index.js"; import { DeleteModal, PurgeModal } from "../../../components/modal/index.js"; -import { MerchantBackend } from "../../../declaration.js"; -import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js"; +import { useSessionContext } from "../../../context/session.js"; +import { useBackendInstances } from "../../../hooks/instance.js"; import { Notification } from "../../../utils/types.js"; +import { LoginPage } from "../../login/index.js"; import { View } from "./View.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; interface Props { onCreate: () => void; onUpdate: (id: string) => void; - instances: MerchantBackend.Instances.Instance[]; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - setInstanceName: (s: string) => void; + instances: TalerMerchantApi.Instance[]; } export default function Instances({ - onUnauthorized, - onLoadError, - onNotFound, onCreate, onUpdate, - setInstanceName, }: Props): VNode { const result = useBackendInstances(); const [deleting, setDeleting] = - useState<MerchantBackend.Instances.Instance | null>(null); + useState<TalerMerchantApi.Instance | null>(null); const [purging, setPurging] = - useState<MerchantBackend.Instances.Instance | null>(null); - const { deleteInstance, purgeInstance } = useAdminAPI(); + useState<TalerMerchantApi.Instance | null>(null); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result.case) + } + } } return ( <Fragment> <NotificationCard notification={notif} /> <View - instances={result.data.instances} + instances={result.body.instances} onDelete={setDeleting} onCreate={onCreate} onPurge={setPurging} onUpdate={onUpdate} - setInstanceName={setInstanceName} selected={!!deleting} /> {deleting && ( @@ -94,9 +86,12 @@ export default function Instances({ element={deleting} onCancel={() => setDeleting(null)} onConfirm={async (): Promise<void> => { + if (state.status !== "loggedIn") { + return; + } try { - await deleteInstance(deleting.id); - // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) + await lib.instance.deleteInstance(state.token, deleting.id); + // pushNotification({message: 'delete_success', type: 'SUCCESS' }) setNotif({ message: i18n.str`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`, type: "SUCCESS", @@ -107,7 +102,7 @@ export default function Instances({ type: "ERROR", description: error instanceof Error ? error.message : undefined, }); - // pushNotification({ message: 'delete_error', type: 'ERROR' }) + // pushNotification({message: 'delete_error', type: 'ERROR' }) } setDeleting(null); }} @@ -118,8 +113,11 @@ export default function Instances({ element={purging} onCancel={() => setPurging(null)} onConfirm={async (): Promise<void> => { + if (state.status !== "loggedIn") { + return; + } try { - await purgeInstance(purging.id); + await lib.instance.deleteInstance(state.token, purging.id, { purge: true }); setNotif({ message: i18n.str`Instance '${purging.name}' (ID: ${purging.id}) has been disabled`, type: "SUCCESS", diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx index 3336c53a4..50cd801d8 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 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 index 6e4786a47..d05375b6c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -30,66 +31,89 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; +import { safeConvertURL } from "../update/UpdatePage.js"; -type Entity = MerchantBackend.BankAccounts.AccountAddDetails & { repeatPassword: string }; +type Entity = TalerMerchantApi.AccountAddDetails & { repeatPassword: string }; interface Props { - onCreate: (d: Entity) => Promise<void>; + onCreate: (d: TalerMerchantApi.AccountAddDetails) => Promise<void>; onBack?: () => void; } const accountAuthType = ["none", "basic"]; -function isValidURL(s: string): boolean { - try { - const u = new URL(s) - return true; - } catch (e) { - return false; - } -} - export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>({}); + const facadeURL = safeConvertURL(state.credit_facade_url); const errors: FormErrors<Entity> = { payto_uri: !state.payto_uri ? i18n.str`required` : undefined, credit_facade_credentials: !state.credit_facade_credentials ? undefined : undefinedIfEmpty({ - username: - state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.username - ? i18n.str`required` - : undefined, - password: - state.credit_facade_credentials.type === "basic" && !state.credit_facade_credentials.password - ? i18n.str`required` - : undefined, - }), + username: + state.credit_facade_credentials.type === "basic" && + !state.credit_facade_credentials.username + ? i18n.str`required` + : undefined, + password: + state.credit_facade_credentials.type === "basic" && + !state.credit_facade_credentials.password + ? i18n.str`required` + : undefined, + }), credit_facade_url: !state.credit_facade_url ? undefined - : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url` + : !facadeURL + ? i18n.str`Invalid url` + : !facadeURL.href.endsWith("/") + ? i18n.str`URL should end with a '/'` + : facadeURL.searchParams.size > 0 + ? i18n.str`URL should not contain params` + : facadeURL.hash + ? i18n.str`URL should not hash param` + : undefined, + repeatPassword: !state.credit_facade_credentials + ? undefined + : state.credit_facade_credentials.type === "basic" && + (!state.credit_facade_credentials.password || + state.credit_facade_credentials.password !== state.repeatPassword) + ? i18n.str`is not the same` : undefined, - repeatPassword: - !state.credit_facade_credentials - ? undefined - : state.credit_facade_credentials.type === "basic" && (!state.credit_facade_credentials.password || state.credit_facade_credentials.password !== state.repeatPassword) - ? i18n.str`is not the same` - : undefined, }; const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); const submitForm = () => { if (hasErrors) return Promise.reject(); - delete state.repeatPassword - return onCreate(state as any); + const credit_facade_url = !state.credit_facade_url + ? undefined + : facadeURL?.href; + const credit_facade_credentials: + | TalerMerchantApi.FacadeCredentials + | undefined = + credit_facade_url == undefined + ? undefined + : state.credit_facade_credentials?.type === "basic" + ? { + type: "basic", + password: state.credit_facade_credentials.password, + username: state.credit_facade_credentials.username, + } + : { + type: "none", + }; + + return onCreate({ + payto_uri: state.payto_uri!, + credit_facade_credentials, + credit_facade_url, + }); }; return ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx index 7d33d25ce..9bab33f6f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,25 +19,37 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { + FacadeCredentials, + HttpStatusCode, + OperationFail, + OperationOk, + TalerError, + TalerMerchantApi, + TalerRevenueHttpClient, + assertUnreachable, + opFixedSuccess, +} from "@gnu-taler/taler-util"; +import { + BrowserFetchHttpLib, + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useWebhookAPI } from "../../../../hooks/webhooks.js"; +import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -import { useOtpDeviceAPI } from "../../../../hooks/otp.js"; -import { useBankAccountAPI } from "../../../../hooks/bank.js"; -export type Entity = MerchantBackend.BankAccounts.AccountAddDetails; +export type Entity = TalerMerchantApi.AccountAddDetails; interface Props { onBack?: () => void; onConfirm: () => void; } export default function CreateValidator({ onConfirm, onBack }: Props): VNode { - const { createBankAccount } = useBankAccountAPI(); + const { lib: api } = useSessionContext(); + const { state } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); @@ -46,14 +58,73 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: Entity) => { - return createBankAccount(request) - .then((d) => { - onConfirm() + onCreate={async (request: Entity) => { + const revenueAPI = !request.credit_facade_url + ? undefined + : new URL("./", request.credit_facade_url); + + if (revenueAPI) { + const resp = await testRevenueAPI( + revenueAPI, + request.credit_facade_credentials, + ); + if (resp.type === "fail") { + switch (resp.case) { + case TestRevenueErrorType.NO_CONFIG: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`, + }); + return; + } + case TestRevenueErrorType.CLIENT_BAD_REQUEST: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Server replied with "bad request".`, + }); + return; + } + case TestRevenueErrorType.UNAUTHORIZED: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Unauthorized, try with another credentials.`, + }); + return; + } + case TestRevenueErrorType.NOT_FOUND: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Check facade URL, server replied with "not found".`, + }); + return; + } + case TestRevenueErrorType.GENERIC_ERROR: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: resp.detail.hint, + }); + return; + } + default: { + assertUnreachable(resp.case); + } + } + } + } + + return api.instance + .addBankAccount(state.token, request) + .then(() => { + onConfirm(); }) .catch((error) => { setNotif({ - message: i18n.str`could not create device`, + message: i18n.str`could not create account`, type: "ERROR", description: error.message, }); @@ -63,3 +134,103 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { </> ); } + +export enum TestRevenueErrorType { + NO_CONFIG, + CLIENT_BAD_REQUEST, + UNAUTHORIZED, + NOT_FOUND, + GENERIC_ERROR, +} + +export async function testRevenueAPI( + revenueAPI: URL, + creds: FacadeCredentials | undefined, +): Promise<OperationOk<void> | OperationFail<TestRevenueErrorType>> { + const api = new TalerRevenueHttpClient( + revenueAPI.href, + new BrowserFetchHttpLib(), + ); + const auth = + creds === undefined + ? undefined + : creds.type === "none" + ? undefined + : creds.type === "basic" + ? { + username: creds.username, + password: creds.password, + } + : undefined; + + try { + const config = await api.getConfig(auth); + + if (config.type === "fail") { + switch (config.case) { + case HttpStatusCode.Unauthorized: { + return { + type: "fail", + case: TestRevenueErrorType.UNAUTHORIZED, + detail: { + code: 1, + }, + }; + } + case HttpStatusCode.NotFound: { + return { + type: "fail", + case: TestRevenueErrorType.NO_CONFIG, + detail: { + code: 1, + }, + }; + } + } + } + + const history = await api.getHistory(auth); + + if (history.type === "fail") { + switch (history.case) { + case HttpStatusCode.BadRequest: { + return { + type: "fail", + case: TestRevenueErrorType.CLIENT_BAD_REQUEST, + detail: { + code: 1, + }, + }; + } + case HttpStatusCode.Unauthorized: { + return { + type: "fail", + case: TestRevenueErrorType.UNAUTHORIZED, + detail: { + code: 1, + }, + }; + } + case HttpStatusCode.NotFound: { + return { + type: "fail", + case: TestRevenueErrorType.NOT_FOUND, + detail: { + code: 1, + }, + }; + } + } + } + } catch (err) { + if (err instanceof TalerError) { + return { + type: "fail", + case: TestRevenueErrorType.GENERIC_ERROR, + detail: err.errorDetail, + }; + } + } + + return opFixedSuccess(undefined); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx index 6b4b63735..18e762642 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx index 24da755b9..4ee68cd80 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,18 +19,17 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { CardTable } from "./Table.js"; export interface Props { - devices: MerchantBackend.BankAccounts.BankAccountEntry[]; - onLoadMoreBefore?: () => void; - onLoadMoreAfter?: () => void; + devices: TalerMerchantApi.BankAccountSummaryEntry[]; + // onLoadMoreBefore?: () => void; + // onLoadMoreAfter?: () => void; onCreate: () => void; - onDelete: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void; - onSelect: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void; + onDelete: (e: TalerMerchantApi.BankAccountSummaryEntry) => void; + onSelect: (e: TalerMerchantApi.BankAccountSummaryEntry) => void; } export function ListPage({ @@ -38,12 +37,10 @@ export function ListPage({ onCreate, onDelete, onSelect, - onLoadMoreBefore, - onLoadMoreAfter, + // onLoadMoreBefore, + // onLoadMoreAfter, }: Props): VNode { - const form = { payto_uri: "" }; - const { i18n } = useTranslationContext(); return ( <section class="section is-main-section"> <CardTable @@ -54,10 +51,10 @@ export function ListPage({ onCreate={onCreate} onDelete={onDelete} onSelect={onSelect} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} + // onLoadMoreBefore={onLoadMoreBefore} + // hasMoreBefore={!onLoadMoreBefore} + // onLoadMoreAfter={onLoadMoreAfter} + // hasMoreAfter={!onLoadMoreAfter} /> </section> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx index 7d6db0782..efe484402 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,23 +19,18 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../../declaration.js"; -import { parsePaytoUri, PaytoType, PaytoUri, PaytoUriBitcoin, PaytoUriIBAN, PaytoUriTalerBank, PaytoUriUnknown } from "@gnu-taler/taler-util"; -type Entity = MerchantBackend.BankAccounts.BankAccountEntry; +type Entity = TalerMerchantApi.BankAccountSummaryEntry; interface Props { accounts: Entity[]; onDelete: (e: Entity) => void; onSelect: (e: Entity) => void; onCreate: () => void; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; } export function CardTable({ @@ -43,10 +38,6 @@ export function CardTable({ onCreate, onDelete, onSelect, - onLoadMoreAfter, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -84,10 +75,6 @@ export function CardTable({ onSelect={onSelect} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} - onLoadMoreAfter={onLoadMoreAfter} - onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} /> ) : ( <EmptyTable /> @@ -104,25 +91,12 @@ interface TableProps { onDelete: (e: Entity) => void; onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; - onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; - onLoadMoreAfter?: () => void; -} - -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); } function Table({ accounts, - onLoadMoreAfter, onDelete, onSelect, - onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); const emptyList: Record<PaytoType | "unknown", { parsed: PaytoUri, acc: Entity }[]> = { "bitcoin": [], "x-taler-bank": [], "iban": [], "unknown": [], } @@ -372,7 +346,7 @@ function EmptyTable(): VNode { <div class="content has-text-grey has-text-centered"> <p> <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> + <i class="mdi mdi-magnify mdi-48px" /> </span> </p> <p> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx index 100241e22..1eda7382d 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,74 +19,77 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; -import { useBankAccountAPI, useInstanceBankAccounts } from "../../../../hooks/bank.js"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; onCreate: () => void; onSelect: (id: string) => void; } export default function ListOtpDevices({ - onUnauthorized, - onLoadError, onCreate, onSelect, - onNotFound, }: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteBankAccount } = useBankAccountAPI(); - const result = useInstanceBankAccounts({ position }, (id) => setPosition(id)); + const { lib: api } = useSessionContext(); + const { state } = useSessionContext(); + const result = useInstanceBankAccounts(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } } return ( <Fragment> <NotificationCard notification={notif} /> - + {result.body.accounts.length < 1 && + <NotificationCard notification={{ + type: "WARN", + message: i18n.str`You need to associate a bank account to receive revenue.`, + description: i18n.str`Without this the merchant backend will refuse to create new orders.` + }} /> + } <ListPage - devices={result.data.accounts} - onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined - } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + devices={result.body.accounts} + // onLoadMoreBefore={ + // result.isFirstPage ? undefined: result.loadFirst + // } + // onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} onCreate={onCreate} onSelect={(e) => { onSelect(e.h_wire); }} - onDelete={(e: MerchantBackend.BankAccounts.BankAccountEntry) => - deleteBankAccount(e.h_wire) + onDelete={(e: TalerMerchantApi.BankAccountSummaryEntry) => { + return api.instance.deleteBankAccount(state.token, e.h_wire) .then(() => setNotif({ message: i18n.str`bank account delete successfully`, @@ -101,6 +104,7 @@ export default function ListOtpDevices({ }), ) } + } /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx index d6b1d65e0..06ea9d07a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 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 index e0e0ba7ed..1a8e9bdc1 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -28,41 +29,69 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -type Entity = MerchantBackend.BankAccounts.BankAccountEntry - & WithId; +type Entity = TalerMerchantApi.BankAccountEntry & WithId; const accountAuthType = ["unedit", "none", "basic"]; interface Props { - onUpdate: (d: MerchantBackend.BankAccounts.AccountPatchDetails) => Promise<void>; + onUpdate: (d: TalerMerchantApi.AccountPatchDetails) => Promise<void>; onBack?: () => void; account: Entity; } - export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const [state, setState] = useState<Partial<MerchantBackend.BankAccounts.AccountPatchDetails>>(account); - - const errors: FormErrors<MerchantBackend.BankAccounts.AccountPatchDetails> = { - credit_facade_url: !state.credit_facade_url ? i18n.str`required` : !isValidURL(state.credit_facade_url) ? i18n.str`invalid url` : undefined, - credit_facade_credentials: undefinedIfEmpty({ + const [state, setState] = + useState<Partial<TalerMerchantApi.AccountPatchDetails>>(account); - username: state.credit_facade_credentials?.type !== "basic" ? undefined - : !state.credit_facade_credentials.username ? i18n.str`required` : undefined, + // @ts-expect-error "unedit" is fine since is part of the accountAuthType values + if (state.credit_facade_credentials?.type === "unedit") { + // we use this to set creds to undefined but server don't get this type + state.credit_facade_credentials = undefined; + } - password: state.credit_facade_credentials?.type !== "basic" ? undefined - : !state.credit_facade_credentials.password ? i18n.str`required` : undefined, + const facadeURL = safeConvertURL(state.credit_facade_url); + + const errors: FormErrors<TalerMerchantApi.AccountPatchDetails> = { + credit_facade_url: !state.credit_facade_url + ? undefined + : !facadeURL + ? i18n.str`Invalid url` + : !facadeURL.href.endsWith("/") + ? i18n.str`URL should end with a '/'` + : facadeURL.searchParams.size > 0 + ? i18n.str`URL should not contain params` + : facadeURL.hash + ? i18n.str`URL should not hash param` + : undefined, + credit_facade_credentials: undefinedIfEmpty({ + username: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !state.credit_facade_credentials.username + ? i18n.str`required` + : undefined, - repeatPassword: state.credit_facade_credentials?.type !== "basic" ? undefined - : !(state.credit_facade_credentials as any).repeatPassword ? i18n.str`required` : - (state.credit_facade_credentials as any).repeatPassword !== state.credit_facade_credentials.password ? i18n.str`doesnt match` + password: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !state.credit_facade_credentials.password + ? i18n.str`required` : undefined, + + repeatPassword: + state.credit_facade_credentials?.type !== "basic" + ? undefined + : !(state.credit_facade_credentials as any).repeatPassword + ? i18n.str`required` + : (state.credit_facade_credentials as any).repeatPassword !== + state.credit_facade_credentials.password + ? i18n.str`doesn't match` + : undefined, }), }; @@ -72,20 +101,27 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { const submitForm = () => { if (hasErrors) return Promise.reject(); - - const creds: typeof state.credit_facade_credentials = - state.credit_facade_credentials?.type === "basic" ? { - type: "basic", - password: state.credit_facade_credentials.password, - username: state.credit_facade_credentials.username, - } : state.credit_facade_credentials?.type === "none" ? { - type: "none" - } : undefined; - - return onUpdate({ - credit_facade_credentials: creds, - credit_facade_url: state.credit_facade_url, - }); + const credit_facade_url = !state.credit_facade_url + ? undefined + : facadeURL?.href; + + const credit_facade_credentials: + | TalerMerchantApi.FacadeCredentials + | undefined = + credit_facade_url == undefined || + state.credit_facade_credentials === undefined + ? undefined + : state.credit_facade_credentials.type === "basic" + ? { + type: "basic", + password: state.credit_facade_credentials.password, + username: state.credit_facade_credentials.username, + } + : { + type: "none", + }; + + return onUpdate({ credit_facade_credentials, credit_facade_url }); }; return ( @@ -134,7 +170,7 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { toStr={(str) => { if (str === "none") return "Without authentication"; if (str === "basic") return "With authentication"; - return "Do not change" + return "Do not change"; }} /> {state.credit_facade_credentials?.type === "basic" ? ( @@ -185,11 +221,12 @@ export function UpdatePage({ account, onUpdate, onBack }: Props): VNode { ); } -function isValidURL(s: string): boolean { +//TODO: move to utils +export function safeConvertURL(s?: string): URL | undefined { + if (!s) return undefined; try { - const u = new URL(s) - return true; + return new URL(s); } catch (e) { - return false; + return undefined; } } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx index 44dee7651..70942fd55 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,68 +19,125 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { useBankAccountAPI, useBankAccountDetails } from "../../../../hooks/bank.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useBankAccountDetails } from "../../../../hooks/bank.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { TestRevenueErrorType, testRevenueAPI } from "../create/index.js"; import { UpdatePage } from "./UpdatePage.js"; -export type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId; +export type Entity = TalerMerchantApi.AccountPatchDetails & WithId; interface Props { onBack?: () => void; onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; bid: string; } export default function UpdateValidator({ bid, onConfirm, onBack, - onUnauthorized, - onNotFound, - onLoadError, }: Props): VNode { - const { updateBankAccount } = useBankAccountAPI(); + const { lib: api } = useSessionContext(); + const { state } = useSessionContext(); const result = useBankAccountDetails(bid); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } } return ( <Fragment> <NotificationCard notification={notif} /> <UpdatePage - account={{ ...result.data, id: bid }} + account={{ ...result.body, id: bid }} onBack={onBack} - onUpdate={(data) => { - return updateBankAccount(bid, data) + onUpdate={async (request) => { + const revenueAPI = !request.credit_facade_url + ? undefined + : new URL("./", request.credit_facade_url); + + if (revenueAPI) { + const resp = await testRevenueAPI( + revenueAPI, + request.credit_facade_credentials, + ); + if (resp.type === "fail") { + switch (resp.case) { + case TestRevenueErrorType.NO_CONFIG: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`The endpoint doesn't seems to be a Taler Revenue API`, + }); + return; + } + case TestRevenueErrorType.CLIENT_BAD_REQUEST: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Server replied with "bad request".`, + }); + return; + } + case TestRevenueErrorType.UNAUTHORIZED: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Unauthorized, try with another credentials.`, + }); + return; + } + case TestRevenueErrorType.NOT_FOUND: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: i18n.str`Check facade URL, server replied with "not found".`, + }); + return; + } + case TestRevenueErrorType.GENERIC_ERROR: { + setNotif({ + message: i18n.str`Could not create account`, + type: "ERROR", + description: resp.detail.hint, + }); + return; + } + default: { + assertUnreachable(resp.case) + } + } + } + } + return api.instance.updateBankAccount(state.token, bid, request) .then(onConfirm) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx index 21dadb1e3..3168c7cc4 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -24,17 +24,17 @@ import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import { FormProvider } from "../../../components/form/FormProvider.js"; import { Input } from "../../../components/form/Input.js"; -import { MerchantBackend } from "../../../declaration.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; -type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage; +type Entity = TalerMerchantApi.InstanceReconfigurationMessage; interface Props { onUpdate: () => void; onDelete: () => void; - selected: MerchantBackend.Instances.QueryInstancesResponse; + selected: TalerMerchantApi.QueryInstancesResponse; } function convert( - from: MerchantBackend.Instances.QueryInstancesResponse, + from: TalerMerchantApi.QueryInstancesResponse, ): Entity { const defaults = { default_wire_fee_amortization: 1, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx index 9b393b818..e1a7f87f0 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,67 +13,70 @@ 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 { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../components/exception/loading.js"; import { DeleteModal } from "../../../components/modal/index.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { MerchantBackend } from "../../../declaration.js"; -import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; +import { useSessionContext } from "../../../context/session.js"; +import { useInstanceDetails } from "../../../hooks/instance.js"; +import { LoginPage } from "../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; import { DetailPage } from "./DetailPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; onUpdate: () => void; - onNotFound: () => VNode; onDelete: () => void; } export default function Detail({ onUpdate, - onLoadError, - onUnauthorized, onDelete, - onNotFound, }: Props): VNode { - const { id } = useInstanceContext(); + const { state } = useSessionContext(); const result = useInstanceDetails(); const [deleting, setDeleting] = useState<boolean>(false); - const { deleteInstance } = useInstanceAPI(); + // const { deleteInstance } = useInstanceAPI(); + const { lib } = useSessionContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(result) + } + } + } + return ( <Fragment> <DetailPage - selected={result.data} + selected={result.body} onUpdate={onUpdate} onDelete={() => setDeleting(true)} /> {deleting && ( <DeleteModal - element={{ name: result.data.name, id }} + element={{ name: result.body.name, id: state.instance }} onCancel={() => setDeleting(false)} onConfirm={async (): Promise<void> => { + if (state.status !== "loggedIn") { + return + } try { - await deleteInstance(); + await lib.instance.deleteCurrentInstance(state.token); onDelete(); } catch (error) { //FIXME: show message error diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx index 367fabce2..42cb1cb02 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,8 +19,8 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; -import { ConfigContextProvider } from "../../../context/config.js"; +import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser"; +import { FunctionalComponent, h } from "preact"; import { DetailPage as TestedComponent } from "./DetailPage.js"; export default { @@ -37,14 +37,33 @@ function createExample<Props>( props: Partial<Props>, ) { const component = (args: any) => ( - <ConfigContextProvider + <MerchantApiProviderTesting value={{ - currency: "TESTKUDOS", - version: "1", + cancelRequest: () => { }, + changeBackend: () => { }, + config: { + currency: "ARS", + version: "1", + currencies: { + "ASD": { + name: "testkudos", + alt_unit_names: {}, + num_fractional_input_digits: 1, + num_fractional_normal_digits: 1, + num_fractional_trailing_zero_digits: 1, + } + }, + exchanges: [], + name: "taler-merchant" + }, + hints: [], + lib: {} as any, + onActivity: (() => { }) as any, + url: new URL("asdasd"), }} > <Internal {...(props as any)} /> - </ConfigContextProvider> + </MerchantApiProviderTesting> ); return { component, props }; } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts b/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts index 1d8c76ff9..8f06937df 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts +++ b/packages/merchant-backoffice-ui/src/paths/instance/index.stories.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -16,4 +16,3 @@ export * as details from "./details/stories.js"; export * as kycList from "./kyc/list/ListPage.stories.js"; -export * as reserve from "./reserves/create/CreatedSuccessfully.stories.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx index d33f64ada..046636b4b 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,10 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; -import { ListPage as TestedComponent } from "./ListPage.js"; +import { PaytoString } from "@gnu-taler/taler-util"; import * as tests from "@gnu-taler/web-util/testing"; -import { MerchantBackend } from "../../../../declaration.js"; +import { ListPage as TestedComponent } from "./ListPage.js"; export default { title: "Pages/KYC/List", @@ -40,19 +39,19 @@ export const Example = tests.createExample(TestedComponent, { { aml_status: 0, exchange_url: "http://exchange.taler", - payto_uri: "payto://iban/de123123123", + payto_uri: "payto://iban/de123123123" as PaytoString, kyc_url: "http://exchange.taler/kyc", }, { aml_status: 1, exchange_url: "http://exchange.taler", - payto_uri: "payto://iban/de123123123", + payto_uri: "payto://iban/de123123123" as PaytoString, }, { aml_status: 2, exchange_url: "http://exchange.taler", - payto_uri: "payto://iban/de123123123", + payto_uri: "payto://iban/de123123123" as PaytoString, }, ], - } as MerchantBackend.KYC.AccountKycRedirects, + }, }); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx index 338081886..3eeed1d7b 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,12 +19,12 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; export interface Props { - status: MerchantBackend.KYC.AccountKycRedirects; + status: TalerMerchantApi.AccountKycRedirects; } export function ListPage({ status }: Props): VNode { @@ -85,11 +85,11 @@ export function ListPage({ status }: Props): VNode { ); } interface PendingTableProps { - entries: MerchantBackend.KYC.MerchantAccountKycRedirect[]; + entries: TalerMerchantApi.MerchantAccountKycRedirect[]; } interface TimedOutTableProps { - entries: MerchantBackend.KYC.ExchangeKycTimeout[]; + entries: TalerMerchantApi.ExchangeKycTimeout[]; } function PendingTable({ entries }: PendingTableProps): VNode { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx index 5b93ac169..ed0e1220f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,42 +19,55 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { useInstanceKYCDetails } from "../../../../hooks/instance.js"; import { ListPage } from "./ListPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; } -export default function ListKYC({ - onUnauthorized, - onLoadError, - onNotFound, -}: Props): VNode { +export default function ListKYC(_p: Props): VNode { const result = useInstanceKYCDetails(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> } + /** + * This component just render known kyc requirements. + * If query fail then is safe to hide errors. + */ + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.GatewayTimeout: { + return <div /> + } + case HttpStatusCode.BadGateway: { + const status = result.body; - const status = result.data.type === "ok" ? undefined : result.data.status; + if (!status) { + return <div>no kyc required</div>; + } + return <ListPage status={status} />; + + } + case HttpStatusCode.ServiceUnavailable: { + return <div /> + } + case HttpStatusCode.Unauthorized: { + return <div /> + } + case HttpStatusCode.NotFound: { + return <div />; + } + default: { + assertUnreachable(result) + } + } + } + const status = result.body; if (!status) { return <div>no kyc required</div>; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx index bd9f65718..fc814b68f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx index 52ee9d351..7be3d23f6 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,58 +19,62 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Amounts } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { add, isAfter, isBefore, isFuture } from "date-fns"; -import { Fragment, h, VNode } from "preact"; +import { + AbsoluteTime, + AmountString, + Amounts, + Duration, + TalerMerchantApi, + TalerProtocolDuration, +} from "@gnu-taler/taler-util"; +import { + 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"; import { FormErrors, FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { InputBoolean } from "../../../../components/form/InputBoolean.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDate } from "../../../../components/form/InputDate.js"; +import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputLocation } from "../../../../components/form/InputLocation.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js"; import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js"; import { ProductList } from "../../../../components/product/ProductList.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { Duration, MerchantBackend, WithId } from "../../../../declaration.js"; -import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { usePreference } from "../../../../hooks/preference.js"; import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { useSettings } from "../../../../hooks/useSettings.js"; -import { InputToggle } from "../../../../components/form/InputToggle.js"; interface Props { - onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; + onCreate: (d: TalerMerchantApi.PostOrderRequest) => void; onBack?: () => void; instanceConfig: InstanceConfig; - instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[]; + instanceInventory: (TalerMerchantApi.ProductDetail & WithId)[]; } interface InstanceConfig { use_stefan: boolean; - default_pay_delay: Duration; - default_wire_transfer_delay: Duration; + default_pay_delay: TalerProtocolDuration; + default_wire_transfer_delay: TalerProtocolDuration; } -function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> { - const defaultPayDeadline = - !config.default_pay_delay || config.default_pay_delay.d_us === "forever" - ? undefined - : add(new Date(), { - seconds: config.default_pay_delay.d_us / (1000 * 1000), - }); - const defaultWireDeadline = - !config.default_wire_transfer_delay || config.default_wire_transfer_delay.d_us === "forever" - ? undefined - : add(new Date(), { - seconds: config.default_wire_transfer_delay.d_us / (1000 * 1000), - }); +function with_defaults( + config: InstanceConfig, + _currency: string, +): Partial<Entity> { + const defaultPayDeadline = Duration.fromTalerProtocolDuration( + config.default_pay_delay, + ); + const defaultWireDeadline = Duration.fromTalerProtocolDuration( + config.default_wire_transfer_delay, + ); return { inventoryProducts: {}, @@ -78,9 +82,9 @@ function with_defaults(config: InstanceConfig, currency: string): Partial<Entity pricing: {}, payments: { max_fee: undefined, + createToken: true, pay_deadline: defaultPayDeadline, refund_deadline: defaultPayDeadline, - createToken: true, wire_transfer_deadline: defaultWireDeadline, }, shipping: {}, @@ -89,7 +93,7 @@ function with_defaults(config: InstanceConfig, currency: string): Partial<Entity } interface ProductAndQuantity { - product: MerchantBackend.Products.ProductDetail & WithId; + product: TalerMerchantApi.ProductDetail & WithId; quantity: number; } export interface ProductMap { @@ -103,47 +107,38 @@ interface Pricing { } interface Shipping { delivery_date?: Date; - delivery_location?: MerchantBackend.Location; + delivery_location?: TalerMerchantApi.Location; fullfilment_url?: string; } interface Payments { - refund_deadline?: Date; - pay_deadline?: Date; - wire_transfer_deadline?: Date; - auto_refund_deadline?: Date; + refund_deadline: Duration; + pay_deadline: Duration; + wire_transfer_deadline: Duration; + auto_refund_deadline: Duration; max_fee?: string; createToken: boolean; minimum_age?: number; } interface Entity { inventoryProducts: ProductMap; - products: MerchantBackend.Product[]; + products: TalerMerchantApi.Product[]; pricing: Partial<Pricing>; payments: Partial<Payments>; shipping: Partial<Shipping>; extra: Record<string, string>; } -const stringIsValidJSON = (value: string) => { - try { - JSON.parse(value.trim()); - return true; - } catch { - return false; - } -}; - export function CreatePage({ onCreate, onBack, instanceConfig, instanceInventory, }: Props): VNode { - const config = useConfigContext(); - const instance_default = with_defaults(instanceConfig, config.currency) + const { config } = useSessionContext(); + const instance_default = with_defaults(instanceConfig, config.currency); const [value, valueHandler] = useState(instance_default); const zero = Amounts.zeroOfCurrency(config.currency); - const [settings, updateSettings] = useSettings() + const [settings, updateSettings] = usePreference(); const inventoryList = Object.values(value.inventoryProducts || {}); const productList = Object.values(value.products || {}); @@ -164,54 +159,44 @@ export function CreatePage({ ? i18n.str`must be greater than 0` : undefined, }), - // extra: - // value.extra && !stringIsValidJSON(value.extra) - // ? i18n.str`not a valid json` - // : undefined, payments: undefinedIfEmpty({ refund_deadline: !value.payments?.refund_deadline ? undefined - : !isFuture(value.payments.refund_deadline) - ? i18n.str`should be in the future` - : value.payments.pay_deadline && - isBefore(value.payments.refund_deadline, value.payments.pay_deadline) - ? i18n.str`refund deadline cannot be before pay deadline` - : value.payments.wire_transfer_deadline && - isBefore( + : value.payments.pay_deadline && + Duration.cmp( + value.payments.refund_deadline, + value.payments.pay_deadline, + ) === -1 + ? i18n.str`refund deadline cannot be before pay deadline` + : value.payments.wire_transfer_deadline && + Duration.cmp( value.payments.wire_transfer_deadline, value.payments.refund_deadline, - ) - ? i18n.str`wire transfer deadline cannot be before refund deadline` - : undefined, + ) === -1 + ? i18n.str`wire transfer deadline cannot be before refund deadline` + : undefined, pay_deadline: !value.payments?.pay_deadline ? i18n.str`required` - : !isFuture(value.payments.pay_deadline) - ? i18n.str`should be in the future` - : value.payments.wire_transfer_deadline && - isBefore( + : value.payments.wire_transfer_deadline && + Duration.cmp( value.payments.wire_transfer_deadline, value.payments.pay_deadline, - ) - ? i18n.str`wire transfer deadline cannot be before pay deadline` - : undefined, + ) === -1 + ? i18n.str`wire transfer deadline cannot be before pay deadline` + : undefined, wire_transfer_deadline: !value.payments?.wire_transfer_deadline ? i18n.str`required` - : !isFuture(value.payments.wire_transfer_deadline) - ? i18n.str`should be in the future` - : undefined, + : undefined, auto_refund_deadline: !value.payments?.auto_refund_deadline ? undefined - : !isFuture(value.payments.auto_refund_deadline) - ? i18n.str`should be in the future` - : !value.payments?.refund_deadline - ? i18n.str`should have a refund deadline` - : !isAfter( - value.payments.refund_deadline, - value.payments.auto_refund_deadline, - ) - ? i18n.str`auto refund cannot be after refund deadline` - : undefined, - + : !value.payments?.refund_deadline + ? i18n.str`should have a refund deadline` + : Duration.cmp( + value.payments.refund_deadline, + value.payments.auto_refund_deadline, + ) == -1 + ? i18n.str`auto refund cannot be after refund deadline` + : undefined, }), shipping: undefinedIfEmpty({ delivery_date: !value.shipping?.delivery_date @@ -226,42 +211,40 @@ export function CreatePage({ ); const submit = (): void => { - const order = schema.cast(value); + const order = value as any; //schema.cast(value); if (!value.payments) return; if (!value.shipping) return; - const request: MerchantBackend.Orders.PostOrderRequest = { + const request: TalerMerchantApi.PostOrderRequest = { order: { amount: order.pricing.order_price, summary: order.pricing.summary, products: productList, - extra: JSON.stringify(value.extra), - pay_deadline: value.payments.pay_deadline - ? { - t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000), - } - : undefined, - wire_transfer_deadline: value.payments.wire_transfer_deadline - ? { - t_s: Math.floor( - value.payments.wire_transfer_deadline.getTime() / 1000, - ), - } - : undefined, - refund_deadline: value.payments.refund_deadline - ? { - t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000), - } - : undefined, + 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 - ? { - d_us: Math.floor( - value.payments.auto_refund_deadline.getTime() * 1000, - ), - } + ? Duration.toTalerProtocolDuration( + value.payments.auto_refund_deadline, + ) : undefined, - max_fee: value.payments.max_fee as string, - + max_fee: value.payments.max_fee as AmountString, delivery_date: value.shipping.delivery_date ? { t_s: value.shipping.delivery_date.getTime() / 1000 } : undefined, @@ -280,7 +263,7 @@ export function CreatePage({ }; const addProductToTheInventoryList = ( - product: MerchantBackend.Products.ProductDetail & WithId, + product: TalerMerchantApi.ProductDetail & WithId, quantity: number, ) => { valueHandler((v) => { @@ -298,7 +281,7 @@ export function CreatePage({ }); }; - const addNewProduct = async (product: MerchantBackend.Product) => { + const addNewProduct = async (product: TalerMerchantApi.Product) => { return valueHandler((v) => { const products = v.products ? [...v.products, product] : []; return { ...v, products }; @@ -314,7 +297,7 @@ export function CreatePage({ }; const [editingProduct, setEditingProduct] = useState< - MerchantBackend.Product | undefined + TalerMerchantApi.Product | undefined >(undefined); const totalPriceInventory = inventoryList.reduce((prev, cur) => { @@ -325,7 +308,7 @@ export function CreatePage({ const totalPriceProducts = productList.reduce((prev, cur) => { if (!cur.price) return zero; const p = Amounts.parseOrThrow(cur.price); - return Amounts.add(prev, Amounts.mult(p, cur.quantity).amount).amount; + return Amounts.add(prev, Amounts.mult(p, cur.quantity ?? 0).amount).amount; }, zero); const hasProducts = inventoryList.length > 0 || productList.length > 0; @@ -334,7 +317,7 @@ export function CreatePage({ const totalAsString = Amounts.stringify(totalPrice.amount); const allProducts = productList.concat(inventoryList.map(asProduct)); - const [newField, setNewField] = useState("") + const [newField, setNewField] = useState(""); useEffect(() => { valueHandler((v) => { @@ -354,40 +337,50 @@ export function CreatePage({ totalPrice.amount, ); - const minAgeByProducts = allProducts.reduce( + const minAgeByProducts = inventoryList.reduce( (cur, prev) => - !prev.minimum_age || cur > prev.minimum_age ? cur : prev.minimum_age, + !prev.product.minimum_age || cur > prev.product.minimum_age ? cur : prev.product.minimum_age, 0, ); - const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline - const noDefault_wireDeadline = !instance_default.payments || !instance_default.payments.wire_transfer_deadline - const requiresSomeTalerOptions = noDefault_payDeadline || noDefault_wireDeadline + // if there is no default pay deadline + const noDefault_payDeadline = + !instance_default.payments || !instance_default.payments.pay_deadline; + // and there is no default wire deadline + const noDefault_wireDeadline = + !instance_default.payments || + !instance_default.payments.wire_transfer_deadline; + // user required to set the taler options + const requiresSomeTalerOptions = + noDefault_payDeadline || noDefault_wireDeadline; return ( <div> - <section class="section is-main-section"> <div class="tabs is-toggle is-fullwidth is-small"> <ul> - <li class={!settings.advanceOrderMode ? "is-active" : ""} onClick={() => { - updateSettings({ - ...settings, - advanceOrderMode: false - }) - }}> - <a > - <span><i18n.Translate>Simple</i18n.Translate></span> + <li + class={!settings.advanceOrderMode ? "is-active" : ""} + onClick={() => { + updateSettings("advanceOrderMode", false); + }} + > + <a> + <span> + <i18n.Translate>Simple</i18n.Translate> + </span> </a> </li> - <li class={settings.advanceOrderMode ? "is-active" : ""} onClick={() => { - updateSettings({ - ...settings, - advanceOrderMode: true - }) - }}> - <a > - <span><i18n.Translate>Advanced</i18n.Translate></span> + <li + class={settings.advanceOrderMode ? "is-active" : ""} + onClick={() => { + updateSettings("advanceOrderMode", true); + }} + > + <a> + <span> + <i18n.Translate>Advanced</i18n.Translate> + </span> </a> </li> </ul> @@ -415,7 +408,7 @@ export function CreatePage({ inventory={instanceInventory} /> - {settings.advanceOrderMode && + {settings.advanceOrderMode && ( <NonInventoryProductFrom productToEdit={editingProduct} onAddProduct={(p) => { @@ -423,7 +416,7 @@ export function CreatePage({ return addNewProduct(p); }} /> - } + )} {allProducts.length > 0 && ( <ProductList @@ -466,8 +459,8 @@ export function CreatePage({ discountOrRise > 0 && (discountOrRise < 1 ? `discount of %${Math.round( - (1 - discountOrRise) * 100, - )}` + (1 - discountOrRise) * 100, + )}` : `rise of %${Math.round((discountOrRise - 1) * 100)}`) } tooltip={i18n.str`Amount to be paid by the customer`} @@ -488,7 +481,7 @@ export function CreatePage({ tooltip={i18n.str`Title of the order to be shown to the customer`} /> - {settings.advanceOrderMode && + {settings.advanceOrderMode && ( <InputGroup name="shipping" label={i18n.str`Shipping and Fulfillment`} @@ -514,135 +507,200 @@ export function CreatePage({ tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} /> </InputGroup> - } + )} - {(settings.advanceOrderMode || requiresSomeTalerOptions) && + {(settings.advanceOrderMode || requiresSomeTalerOptions) && ( <InputGroup name="payments" label={i18n.str`Taler payment options`} tooltip={i18n.str`Override default Taler payment settings for this order`} > - {(settings.advanceOrderMode || noDefault_payDeadline) && <InputDate - name="payments.pay_deadline" - label={i18n.str`Payment deadline`} - tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`} - 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> - } - />} - {settings.advanceOrderMode && <InputDate - name="payments.refund_deadline" - label={i18n.str`Refund deadline`} - tooltip={i18n.str`Time until which the order can be refunded by the merchant.`} - 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> - } - />} - {(settings.advanceOrderMode || noDefault_wireDeadline) && <InputDate - name="payments.wire_transfer_deadline" - label={i18n.str`Wire transfer deadline`} - tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`} - 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> - } - />} - {settings.advanceOrderMode && <InputDate - name="payments.auto_refund_deadline" - label={i18n.str`Auto-refund deadline`} - tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} - />} - - {settings.advanceOrderMode && <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.`} - />} - {settings.advanceOrderMode && <InputToggle - name="payments.createToken" - label={i18n.str`Create token`} - tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`} - />} - {settings.advanceOrderMode && <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`Min age defined by the producs is ${minAgeByProducts}` - : i18n.str`No product with age restriction in this order` - } - />} + {(settings.advanceOrderMode || noDefault_payDeadline) && ( + <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 for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run 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> + } + /> + )} + {settings.advanceOrderMode && ( + <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> + } + /> + )} + {(settings.advanceOrderMode || noDefault_wireDeadline) && ( + <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> + } + /> + )} + {settings.advanceOrderMode && ( + <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 + /> + )} + + {settings.advanceOrderMode && ( + <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.`} + /> + )} + {settings.advanceOrderMode && ( + <InputToggle + name="payments.createToken" + label={i18n.str`Create token`} + tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`} + /> + )} + {settings.advanceOrderMode && ( + <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`Min age defined by the producs is ${minAgeByProducts}` + : i18n.str`No product with age restriction in this order` + } + /> + )} </InputGroup> - } + )} - {settings.advanceOrderMode && + {settings.advanceOrderMode && ( <InputGroup name="extra" label={i18n.str`Additional information`} tooltip={i18n.str`Custom information to be included in the contract for this order.`} > - {Object.keys(value.extra ?? {}).map((key) => { - - return <Input - name={`extra.${key}`} - inputType="multiline" - label={key} - tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} - side={ - <button class="button" onClick={(e) => { - if (value.extra && value.extra[key] !== undefined) { - console.log(value.extra) - delete value.extra[key] - } - valueHandler({ - ...value, - }) - }}>remove</button> - } - /> + {Object.keys(value.extra ?? {}).map((key, idx) => { + return ( + <Input + name={`extra.${key}`} + key={String(idx)} + inputType="multiline" + label={key} + tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} + side={ + <button + class="button" + onClick={(e) => { + if ( + value.extra && + value.extra[key] !== undefined + ) { + delete value.extra[key]; + } + valueHandler({ + ...value, + }); + e.preventDefault(); + }} + > + remove + </button> + } + /> + ); })} <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label"> <i18n.Translate>Custom field name</i18n.Translate> - <span class="icon has-tooltip-right" data-tooltip={"new extra field"}> + <span + class="icon has-tooltip-right" + data-tooltip={"new extra field"} + > <i class="mdi mdi-information" /> </span> </label> @@ -650,23 +708,33 @@ export function CreatePage({ <div class="field-body is-flex-grow-3"> <div class="field"> <p class="control"> - <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} /> + <input + class="input " + value={newField} + onChange={(e) => setNewField(e.currentTarget.value)} + /> </p> </div> </div> - <button class="button" onClick={(e) => { - setNewField("") - valueHandler({ - ...value, - extra: { - ...(value.extra ?? {}), - [newField]: "" - } - }) - }}>add</button> + <button + class="button" + onClick={(e) => { + setNewField(""); + valueHandler({ + ...value, + extra: { + ...(value.extra ?? {}), + [newField]: "", + }, + }); + e.preventDefault(); + }} + > + add + </button> </div> </InputGroup> - } + )} </FormProvider> <div class="buttons is-right mt-5"> @@ -691,7 +759,7 @@ export function CreatePage({ ); } -function asProduct(p: ProductAndQuantity): MerchantBackend.Product { +function asProduct(p: ProductAndQuantity): TalerMerchantApi.Product { return { product_id: p.product.id, image: p.product.image, @@ -700,6 +768,27 @@ function asProduct(p: ProductAndQuantity): MerchantBackend.Product { quantity: p.quantity, description: p.product.description, taxes: p.product.taxes, - minimum_age: p.product.minimum_age, }; } + +function DeadlineHelp({ duration }: { duration?: Duration }): VNode { + const { i18n } = useTranslationContext(); + const [now, setNow] = useState(AbsoluteTime.now()); + useEffect(() => { + const iid = setInterval(() => { + setNow(AbsoluteTime.now()); + }, 60 * 1000); + return () => { + clearInterval(iid); + }; + }); + if (!duration) return <i18n.Translate>Disabled</i18n.Translate>; + const when = AbsoluteTime.addDuration(now, duration); + if (when.t_ms === "never") + return <i18n.Translate>No deadline</i18n.Translate>; + return ( + <i18n.Translate> + Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")} + </i18n.Translate> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx index 88a984c97..32f3f05c7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,12 +13,15 @@ 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 { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useEffect, useState } from "preact/hooks"; +import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util"; +import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { VNode, h } from "preact"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { useOrderAPI } from "../../../../hooks/order.js"; +import { useOrderDetails } from "../../../../hooks/order.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { Entity } from "./index.js"; +import { LoginPage } from "../../../login/index.js"; interface Props { entity: Entity; @@ -31,14 +34,38 @@ export function OrderCreatedSuccessfully({ onConfirm, onCreateAnother, }: Props): VNode { - const { getPaymentURL } = useOrderAPI(); - const [url, setURL] = useState<string | undefined>(undefined); + const result = useOrderDetails(entity.response.order_id) const { i18n } = useTranslationContext(); - useEffect(() => { - getPaymentURL(entity.response.order_id).then((response) => { - setURL(response.data); - }); - }, [getPaymentURL, entity.response.order_id]); + + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.BadGateway: { + return <div>Failed to obtain a response from the exchange</div>; + } + case HttpStatusCode.GatewayTimeout: { + return ( + <div>The merchant's interaction with the exchange took too long</div> + ); + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } + } + + const url = result.body.order_status === "unpaid" ? + result.body.taler_pay_uri : + result.body.contract_terms.fulfillment_url return ( <CreatedSuccessfully diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx index 2474fd042..861114014 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,72 +19,71 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; +import { useSessionContext } from "../../../../context/session.js"; import { useInstanceDetails } from "../../../../hooks/instance.js"; -import { useOrderAPI } from "../../../../hooks/order.js"; import { useInstanceProducts } from "../../../../hooks/product.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CreatePage } from "./CreatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; export type Entity = { - request: MerchantBackend.Orders.PostOrderRequest; - response: MerchantBackend.Orders.PostOrderResponse; + request: TalerMerchantApi.PostOrderRequest; + response: TalerMerchantApi.PostOrderResponse; }; interface Props { onBack?: () => void; onConfirm: (id: string) => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; } export default function OrderCreate({ onConfirm, onBack, - onLoadError, - onNotFound, - onUnauthorized, }: Props): VNode { - const { createOrder } = useOrderAPI(); + const { lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - + const { state } = useSessionContext(); const detailsResult = useInstanceDetails(); const inventoryResult = useInstanceProducts(); - if (detailsResult.loading) return <Loading />; - if (inventoryResult.loading) return <Loading />; - - if (!detailsResult.ok) { - if ( - detailsResult.type === ErrorType.CLIENT && - detailsResult.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - detailsResult.type === ErrorType.CLIENT && - detailsResult.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(detailsResult); + if (!detailsResult) return <Loading /> + if (detailsResult instanceof TalerError) { + return <ErrorLoadingMerchant error={detailsResult} /> } - - if (!inventoryResult.ok) { - if ( - inventoryResult.type === ErrorType.CLIENT && - inventoryResult.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - inventoryResult.type === ErrorType.CLIENT && - inventoryResult.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(inventoryResult); + if (detailsResult.type === "fail") { + switch (detailsResult.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(detailsResult); + } + } + } + if (!inventoryResult) return <Loading /> + if (inventoryResult instanceof TalerError) { + return <ErrorLoadingMerchant error={inventoryResult} /> + } + if (inventoryResult.type === "fail") { + switch (inventoryResult.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(inventoryResult); + } + } } return ( @@ -93,10 +92,17 @@ export default function OrderCreate({ <CreatePage onBack={onBack} - onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => { - createOrder(request) + onCreate={(request: TalerMerchantApi.PostOrderRequest) => { + lib.instance.createOrder(state.token, request) .then((r) => { - return onConfirm(r.data.order_id) + if (r.type === "ok") { + return onConfirm(r.body.order_id) + } else { + setNotif({ + message: "could not create order", + type: "ERROR", + }); + } }) .catch((error) => { setNotif({ @@ -106,8 +112,8 @@ export default function OrderCreate({ }); }); }} - instanceConfig={detailsResult.data} - instanceInventory={inventoryResult.data} + instanceConfig={detailsResult.body} + instanceInventory={inventoryResult.body} /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx index 6e73a01a5..7d4877db9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,9 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { addDays } from "date-fns"; -import { h, VNode, FunctionalComponent } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; +import { FunctionalComponent, h } from "preact"; import { DetailPage as TestedComponent } from "./DetailPage.js"; export default { @@ -42,14 +42,13 @@ function createExample<Props>( return r; } -const defaultContractTerm = { - amount: "TESTKUDOS:10", +const defaultContractTerm: TalerMerchantApi.ContractTerms = { + amount: "TESTKUDOS:10" as AmountString, timestamp: { t_s: new Date().getTime() / 1000, }, - auditors: [], exchanges: [], - max_fee: "TESTKUDOS:1", + max_fee: "TESTKUDOS:1" as AmountString, merchant: {} as any, merchant_base_url: "http://merchant.url/", order_id: "2021.165-03GDFC26Y1NNG", @@ -66,7 +65,7 @@ const defaultContractTerm = { }, wire_method: "x-taler-bank", h_wire: "asd", -} as MerchantBackend.ContractTerms; +}; // contract_terms: defaultContracTerm, export const Claimed = createExample(TestedComponent, { @@ -83,16 +82,16 @@ export const PaidNotRefundable = createExample(TestedComponent, { order_status: "paid", contract_terms: defaultContractTerm, refunded: false, - deposit_total: "TESTKUDOS:10", - exchange_ec: 0, + deposit_total: "TESTKUDOS:10" as AmountString, + exchange_code: 0, order_status_url: "http://merchant.backend/status", - exchange_hc: 0, - refund_amount: "TESTKUDOS:0", + exchange_http_status: 0, + refund_amount: "TESTKUDOS:0" as AmountString, refund_details: [], refund_pending: false, wire_details: [], - wire_reports: [], wired: false, + wire_reports: [], }, }); @@ -107,15 +106,15 @@ export const PaidRefundable = createExample(TestedComponent, { }, }, refunded: false, - deposit_total: "TESTKUDOS:10", - exchange_ec: 0, + deposit_total: "TESTKUDOS:10" as AmountString, + exchange_code: 0, order_status_url: "http://merchant.backend/status", - exchange_hc: 0, - refund_amount: "TESTKUDOS:0", + exchange_http_status: 0, + refund_amount: "TESTKUDOS:0" as AmountString, refund_details: [], + wire_reports: [], refund_pending: false, wire_details: [], - wire_reports: [], wired: false, }, }); @@ -130,6 +129,6 @@ export const Unpaid = createExample(TestedComponent, { }, summary: "text summary", taler_pay_uri: "pay uri", - total_amount: "TESTKUDOS:10", + total_amount: "TESTKUDOS:10" as AmountString, }, }); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx index 5ff76e37a..498ea83e3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,8 +19,15 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + AmountJson, + Amounts, + TalerMerchantApi, + stringifyRefundUri, +} from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { format, formatDistance } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -33,28 +40,30 @@ import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputLocation } from "../../../../components/form/InputLocation.js"; import { TextField } from "../../../../components/form/TextField.js"; import { ProductList } from "../../../../components/product/ProductList.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { + datetimeFormatForSettings, + usePreference, +} from "../../../../hooks/preference.js"; import { mergeRefunds } from "../../../../utils/amount.js"; import { RefundModal } from "../list/Table.js"; import { Event, Timeline } from "./Timeline.js"; -type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse; -type CT = MerchantBackend.ContractTerms; +type Entity = TalerMerchantApi.MerchantOrderStatusResponse; +type CT = TalerMerchantApi.ContractTerms; interface Props { onBack: () => void; selected: Entity; id: string; - onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void; + onRefund: (id: string, value: TalerMerchantApi.RefundRequest) => void; } -type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & { +type Paid = TalerMerchantApi.CheckPaymentPaidResponse & { refund_taken: string; }; -type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse; -type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse; +type Unpaid = TalerMerchantApi.CheckPaymentUnpaidResponse; +type Claimed = TalerMerchantApi.CheckPaymentClaimedResponse; function ContractTerms({ value }: { value: CT }) { const { i18n } = useTranslationContext(); @@ -149,8 +158,12 @@ function ClaimedPage({ order, }: { id: string; - order: MerchantBackend.Orders.CheckPaymentClaimedResponse; + order: TalerMerchantApi.CheckPaymentClaimedResponse; }) { + const now = new Date(); + const refundable = + order.contract_terms.refund_deadline.t_s !== "never" && + now.getTime() < order.contract_terms.refund_deadline.t_s * 1000; const events: Event[] = []; if (order.contract_terms.timestamp.t_s !== "never") { events.push({ @@ -166,20 +179,20 @@ function ClaimedPage({ type: "deadline", }); } - if (order.contract_terms.refund_deadline.t_s !== "never") { + if (order.contract_terms.refund_deadline.t_s !== "never" && refundable) { events.push({ when: new Date(order.contract_terms.refund_deadline.t_s * 1000), description: "refund deadline", type: "deadline", }); } - if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000), - description: "wire deadline", - type: "deadline", - }); - } + // if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { + // events.push({ + // when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000), + // description: "wire deadline", + // type: "deadline", + // }); + // } if ( order.contract_terms.delivery_date && order.contract_terms.delivery_date.t_s !== "never" @@ -193,7 +206,7 @@ function ClaimedPage({ const [value, valueHandler] = useState<Partial<Claimed>>(order); const { i18n } = useTranslationContext(); - const [settings] = useSettings() + const [settings] = usePreference(); return ( <div> @@ -237,10 +250,14 @@ function ClaimedPage({ <b> <i18n.Translate>claimed at</i18n.Translate>: </b>{" "} - {format( - new Date(order.contract_terms.timestamp.t_s * 1000), - datetimeFormatForSettings(settings) - )} + {order.contract_terms.timestamp.t_s === "never" + ? "never" + : format( + new Date( + order.contract_terms.timestamp.t_s * 1000, + ), + datetimeFormatForSettings(settings), + )} </p> </div> </div> @@ -311,25 +328,16 @@ function PaidPage({ onRefund, }: { id: string; - order: MerchantBackend.Orders.CheckPaymentPaidResponse; + order: TalerMerchantApi.CheckPaymentPaidResponse; onRefund: (id: string) => void; }) { + const now = new Date(); + const refundable = + order.contract_terms.refund_deadline.t_s !== "never" && + now.getTime() < order.contract_terms.refund_deadline.t_s * 1000; + const events: Event[] = []; - if (order.contract_terms.timestamp.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.timestamp.t_s * 1000), - description: "order created", - type: "start", - }); - } - if (order.contract_terms.pay_deadline.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.pay_deadline.t_s * 1000), - description: "pay deadline", - type: "deadline", - }); - } - if (order.contract_terms.refund_deadline.t_s !== "never") { + if (order.contract_terms.refund_deadline.t_s !== "never" && refundable) { events.push({ when: new Date(order.contract_terms.refund_deadline.t_s * 1000), description: "refund deadline", @@ -363,66 +371,71 @@ function PaidPage({ }); } }); - if (order.wire_details && order.wire_details.length) { - if (order.wire_details.length > 1) { - let last: MerchantBackend.Orders.TransactionWireTransfer | null = null; - let first: MerchantBackend.Orders.TransactionWireTransfer | null = null; - let total: AmountJson | null = null; - - order.wire_details.forEach((w) => { - if (last === null || last.execution_time.t_s < w.execution_time.t_s) { - last = w; - } - if (first === null || first.execution_time.t_s > w.execution_time.t_s) { - first = w; - } - total = - total === null - ? Amounts.parseOrThrow(w.amount) - : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount; - }); - const last_time = last!.execution_time.t_s; - if (last_time !== "never") { - events.push({ - when: new Date(last_time * 1000), - description: `wired ${Amounts.stringify(total!)}`, - type: "wired-range", + const ra = !order.refunded ? undefined : Amounts.parse(order.refund_amount); + const am = Amounts.parseOrThrow(order.contract_terms.amount); + if (ra && Amounts.cmp(ra, am) === 1) { + if (order.wire_details && order.wire_details.length) { + if (order.wire_details.length > 1) { + let last: TalerMerchantApi.TransactionWireTransfer | null = null; + let first: TalerMerchantApi.TransactionWireTransfer | null = null; + let total: AmountJson | null = null; + + order.wire_details.forEach((w) => { + if (last === null || last.execution_time.t_s < w.execution_time.t_s) { + last = w; + } + if ( + first === null || + first.execution_time.t_s > w.execution_time.t_s + ) { + first = w; + } + total = + total === null + ? Amounts.parseOrThrow(w.amount) + : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount; }); - } - const first_time = first!.execution_time.t_s; - if (first_time !== "never") { - events.push({ - when: new Date(first_time * 1000), - description: `wire transfer started...`, - type: "wired-range", - }); - } - } else { - order.wire_details.forEach((e) => { - if (e.execution_time.t_s !== "never") { + const last_time = last!.execution_time.t_s; + if (last_time !== "never") { events.push({ - when: new Date(e.execution_time.t_s * 1000), - description: `wired ${e.amount}`, - type: "wired", + when: new Date(last_time * 1000), + description: `wired ${Amounts.stringify(total!)}`, + type: "wired-range", }); } - }); + const first_time = first!.execution_time.t_s; + if (first_time !== "never") { + events.push({ + when: new Date(first_time * 1000), + description: `wire transfer started...`, + type: "wired-range", + }); + } + } else { + order.wire_details.forEach((e) => { + if (e.execution_time.t_s !== "never") { + events.push({ + when: new Date(e.execution_time.t_s * 1000), + description: `wired ${e.amount}`, + type: "wired", + }); + } + }); + } } } - const now = new Date() const nextEvent = events.find((e) => { - return e.when.getTime() > now.getTime() - }) + return e.when.getTime() > now.getTime(); + }); const [value, valueHandler] = useState<Partial<Paid>>(order); - const { url: backendURL } = useBackendContext() + const { state } = useSessionContext(); + const refundurl = stringifyRefundUri({ - merchantBaseUrl: backendURL, - orderId: order.contract_terms.order_id - }) - const refundable = - new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000; + merchantBaseUrl: state.backendUrl.href, + orderId: order.contract_terms.order_id, + }); const { i18n } = useTranslationContext(); const amount = Amounts.parseOrThrow(order.contract_terms.amount); @@ -503,15 +516,16 @@ function PaidPage({ textOverflow: "ellipsis", }} > - {nextEvent && + {nextEvent && ( <p> - <i18n.Translate>Next event in </i18n.Translate> {formatDistance( + <i18n.Translate>Next event in </i18n.Translate>{" "} + {formatDistance( nextEvent.when, new Date(), // "yyyy/MM/dd HH:mm:ss", )} </p> - } + )} </div> </div> </div> @@ -607,11 +621,11 @@ function UnpaidPage({ order, }: { id: string; - order: MerchantBackend.Orders.CheckPaymentUnpaidResponse; + order: TalerMerchantApi.CheckPaymentUnpaidResponse; }) { const [value, valueHandler] = useState<Partial<Unpaid>>(order); const { i18n } = useTranslationContext(); - const [settings] = useSettings() + const [settings] = usePreference(); return ( <div> <section class="hero is-hero-bar"> @@ -659,9 +673,9 @@ function UnpaidPage({ {order.creation_time.t_s === "never" ? "never" : format( - new Date(order.creation_time.t_s * 1000), - datetimeFormatForSettings(settings) - )} + new Date(order.creation_time.t_s * 1000), + datetimeFormatForSettings(settings), + )} </p> </div> </div> @@ -764,7 +778,3 @@ export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode { </Fragment> ); } - -async function copyToClipboard(text: string) { - return navigator.clipboard.writeText(text); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx index 8c863f386..2d62e2252 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -16,7 +16,7 @@ import { format } from "date-fns"; import { h } from "preact"; import { useEffect, useState } from "preact/hooks"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; +import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js"; interface Props { events: Event[]; @@ -31,7 +31,7 @@ export function Timeline({ events: e }: Props) { }); events.sort((a, b) => a.when.getTime() - b.when.getTime()); - const [settings] = useSettings(); + const [settings] = usePreference(); const [state, setState] = useState(events); useEffect(() => { const handle = setTimeout(() => { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx index 1517a3c42..b28e59b29 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -14,55 +14,62 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - useTranslationContext, - HttpError, - ErrorType, + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useOrderDetails } from "../../../../hooks/order.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { DetailPage } from "./DetailPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; export interface Props { oid: string; - onBack: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; } -export default function Update({ - oid, - onBack, - onLoadError, - onNotFound, - onUnauthorized, -}: Props): VNode { - const { refundOrder } = useOrderAPI(); +export default function Update({ oid, onBack }: Props): VNode { const result = useOrderDetails(oid); const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { lib: api } = useSessionContext(); + const { state } = useSessionContext(); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.BadGateway: { + return <div>Failed to obtain a response from the exchange</div>; + } + case HttpStatusCode.GatewayTimeout: { + return ( + <div>The merchant's interaction with the exchange took too long</div> + ); + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } } return ( @@ -72,8 +79,12 @@ export default function Update({ <DetailPage onBack={onBack} id={oid} - onRefund={(id, value) => - refundOrder(id, value) + onRefund={(id, value) => { + if (state.status !== "loggedIn") { + return; + } + api.instance + .addRefund(state.token, id, value) .then(() => setNotif({ message: i18n.str`refund created successfully`, @@ -86,9 +97,9 @@ export default function Update({ type: "ERROR", description: error.message, }), - ) - } - selected={result.data} + ); + }} + selected={result.body} /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx index 156c577f4..5c9969689 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/List.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,8 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; +import { FunctionalComponent, h } from "preact"; import { ListPage as TestedComponent } from "./ListPage.js"; +import { AmountString } from "@gnu-taler/taler-util"; export default { title: "Pages/Order/List", @@ -54,7 +55,7 @@ export const Example = createExample(TestedComponent, { orders: [ { id: "123", - amount: "TESTKUDOS:10", + amount: "TESTKUDOS:10" as AmountString, paid: false, refundable: true, row_id: 1, @@ -66,7 +67,7 @@ export const Example = createExample(TestedComponent, { }, { id: "234", - amount: "TESTKUDOS:12", + amount: "TESTKUDOS:12" as AmountString, paid: true, refundable: true, row_id: 2, @@ -79,7 +80,7 @@ export const Example = createExample(TestedComponent, { }, { id: "456", - amount: "TESTKUDOS:1", + amount: "TESTKUDOS:1" as AmountString, paid: false, refundable: false, row_id: 3, @@ -92,7 +93,7 @@ export const Example = createExample(TestedComponent, { }, { id: "234", - amount: "TESTKUDOS:12", + amount: "TESTKUDOS:12" as AmountString, paid: false, refundable: false, row_id: 4, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx index 9f80719a1..408bc0c0a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,14 +19,14 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AbsoluteTime, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { h, VNode, Fragment } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { DatePicker } from "../../../../components/picker/DatePicker.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { dateFormatForSettings, usePreference } from "../../../../hooks/preference.js"; import { CardTable } from "./Table.js"; -import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; export interface ListPageProps { onShowAll: () => void; @@ -43,23 +43,19 @@ export interface ListPageProps { isNotWiredActive: string; isWiredActive: string; - jumpToDate?: Date; - onSelectDate: (date?: Date) => void; + jumpToDate?: AbsoluteTime; + onSelectDate: (date?: AbsoluteTime) => void; - orders: (MerchantBackend.Orders.OrderHistoryEntry & WithId)[]; + orders: (TalerMerchantApi.OrderHistoryEntry & WithId)[]; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; - onSelectOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; - onRefundOrder: (o: MerchantBackend.Orders.OrderHistoryEntry & WithId) => void; + onSelectOrder: (o: TalerMerchantApi.OrderHistoryEntry & WithId) => void; + onRefundOrder: (o: TalerMerchantApi.OrderHistoryEntry & WithId) => void; onCreate: () => void; } export function ListPage({ - hasMoreAfter, - hasMoreBefore, onLoadMoreAfter, onLoadMoreBefore, orders, @@ -85,7 +81,7 @@ export function ListPage({ const { i18n } = useTranslationContext(); const dateTooltip = i18n.str`select date to show nearby orders`; const [pickDate, setPickDate] = useState(false); - const [settings] = useSettings(); + const [settings] = usePreference(); return ( <Fragment> @@ -177,7 +173,7 @@ export function ListPage({ class="input" type="text" readonly - value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))} + value={!jumpToDate || jumpToDate.t_ms === "never" ? "" : format(jumpToDate.t_ms, dateFormatForSettings(settings))} placeholder={i18n.str`date (${dateFormatForSettings(settings)})`} onClick={() => { setPickDate(true); @@ -207,7 +203,9 @@ export function ListPage({ <DatePicker opened={pickDate} closeFunction={() => setPickDate(false)} - dateReceiver={onSelectDate} + dateReceiver={(d) => { + onSelectDate(AbsoluteTime.fromMilliseconds(d.getTime())) + }} /> <CardTable @@ -216,8 +214,6 @@ export function ListPage({ onCopyURL={onCopyURL} onSelect={onSelectOrder} onRefund={onRefundOrder} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx index b2806bb79..5ece34409 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,10 +19,12 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Amounts } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { h, VNode } from "preact"; +import { VNode, h } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import { FormErrors, @@ -33,12 +35,14 @@ import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { ConfirmModal } from "../../../../components/modal/index.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { + datetimeFormatForSettings, + usePreference, +} from "../../../../hooks/preference.js"; import { mergeRefunds } from "../../../../utils/amount.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; -type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId; +type Entity = TalerMerchantApi.OrderHistoryEntry & WithId; interface Props { orders: Entity[]; onRefund: (value: Entity) => void; @@ -46,8 +50,6 @@ interface Props { onCreate: () => void; onSelect: (order: Entity) => void; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } @@ -59,8 +61,6 @@ export function CardTable({ onSelect, onLoadMoreAfter, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -101,8 +101,6 @@ export function CardTable({ rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} /> ) : ( <EmptyTable /> @@ -121,8 +119,6 @@ interface TableProps { onSelect: (id: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } @@ -133,19 +129,14 @@ function Table({ onCopyURL, onLoadMoreAfter, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); - const [settings] = useSettings(); + const [settings] = usePreference(); return ( <div class="table-container"> - {hasMoreBefore && ( - <button - class="button is-fullwidth" - onClick={onLoadMoreBefore} - > - <i18n.Translate>load newer orders</i18n.Translate> + {onLoadMoreBefore && ( + <button class="button is-fullwidth" onClick={onLoadMoreBefore}> + <i18n.Translate>load first page</i18n.Translate> </button> )} <table class="table is-striped is-hoverable is-fullwidth"> @@ -174,9 +165,9 @@ function Table({ {i.timestamp.t_s === "never" ? "never" : format( - new Date(i.timestamp.t_s * 1000), - datetimeFormatForSettings(settings), - )} + new Date(i.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} </td> <td onClick={(): void => onSelect(i)} @@ -217,12 +208,11 @@ function Table({ })} </tbody> </table> - {hasMoreAfter && ( - <button - class="button is-fullwidth" - onClick={onLoadMoreAfter} - > - <i18n.Translate>load older orders</i18n.Translate> + {onLoadMoreAfter && ( + <button class="button is-fullwidth" + data-tooltip={i18n.str`load more orders after the last one`} + onClick={onLoadMoreAfter}> + <i18n.Translate>load next page</i18n.Translate> </button> )} </div> @@ -235,7 +225,7 @@ function EmptyTable(): VNode { <div class="content has-text-grey has-text-centered"> <p> <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> + <i class="mdi mdi-magnify mdi-48px" /> </span> </p> <p> @@ -249,8 +239,8 @@ function EmptyTable(): VNode { interface RefundModalProps { onCancel: () => void; - onConfirm: (value: MerchantBackend.Orders.RefundRequest) => void; - order: MerchantBackend.Orders.MerchantOrderStatusResponse; + onConfirm: (value: TalerMerchantApi.RefundRequest) => void; + order: TalerMerchantApi.MerchantOrderStatusResponse; } export function RefundModal({ @@ -260,7 +250,7 @@ export function RefundModal({ }: RefundModalProps): VNode { type State = { mainReason?: string; description?: string; refund?: string }; const [form, setValue] = useState<State>({}); - const [settings] = useSettings(); + const [settings] = usePreference(); const { i18n } = useTranslationContext(); // const [errors, setErrors] = useState<FormErrors<State>>({}); @@ -268,7 +258,7 @@ export function RefundModal({ order.order_status === "paid" ? order.refund_details : [] ).reduce(mergeRefunds, []); - const config = useConfigContext(); + const { config } = useSessionContext(); const totalRefunded = refunds .map((r) => r.amount) .reduce( @@ -303,7 +293,7 @@ export function RefundModal({ : undefined, }; const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); const validateAndConfirm = () => { @@ -362,9 +352,9 @@ export function RefundModal({ {r.timestamp.t_s === "never" ? "never" : format( - new Date(r.timestamp.t_s * 1000), - datetimeFormatForSettings(settings), - )} + new Date(r.timestamp.t_s * 1000), + datetimeFormatForSettings(settings), + )} </td> <td>{r.amount}</td> <td>{r.reason}</td> @@ -382,7 +372,7 @@ export function RefundModal({ <FormProvider<State> errors={errors} object={form} - valueHandler={(d) => setValue(d as any)} + valueHandler={(d) => setValue(d)} > <InputCurrency<State> name="refund" diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx index 34c7d348a..8a1f85b1c 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -20,81 +20,86 @@ */ import { - ErrorType, - HttpError, - useTranslationContext, + AbsoluteTime, + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; +import { useSessionContext } from "../../../../context/session.js"; import { InstanceOrderFilter, useInstanceOrders, - useOrderAPI, useOrderDetails, } from "../../../../hooks/order.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; import { RefundModal } from "./Table.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; onSelect: (id: string) => void; onCreate: () => void; } -export default function OrderList({ - onUnauthorized, - onLoadError, - onCreate, - onSelect, - onNotFound, -}: Props): VNode { - const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" }); +export default function OrderList({ onCreate, onSelect }: Props): VNode { + const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: false }); const [orderToBeRefunded, setOrderToBeRefunded] = useState< - MerchantBackend.Orders.OrderHistoryEntry | undefined + TalerMerchantApi.OrderHistoryEntry | undefined >(undefined); - const setNewDate = (date?: Date): void => + const setNewDate = (date?: AbsoluteTime): void => setFilter((prev) => ({ ...prev, date })); - const result = useInstanceOrders(filter, setNewDate); - const { refundOrder, getPaymentURL } = useOrderAPI(); + const result = useInstanceOrders(filter, (d) => + setFilter({ ...filter, position: d }), + ); + const { lib } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); + const { state } = useSessionContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } } - const isNotPaidActive = filter.paid === "no" ? "is-active" : ""; - const isPaidActive = filter.paid === "yes" && filter.wired === undefined ? "is-active" : ""; - const isRefundedActive = filter.refunded === "yes" ? "is-active" : ""; - const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : ""; - const isWiredActive = filter.wired === "yes" ? "is-active" : ""; + const isNotPaidActive = filter.paid === false ? "is-active" : ""; + const isPaidActive = + filter.paid === true && filter.wired === undefined ? "is-active" : ""; + const isRefundedActive = filter.refunded === true ? "is-active" : ""; + const isNotWiredActive = + filter.wired === false && filter.paid === true ? "is-active" : ""; + const isWiredActive = filter.wired === true ? "is-active" : ""; const isAllActive = filter.paid === undefined && - filter.refunded === undefined && - filter.wired === undefined + filter.refunded === undefined && + filter.wired === undefined ? "is-active" : ""; @@ -103,18 +108,19 @@ export default function OrderList({ <NotificationCard notification={notif} /> <JumpToElementById - testIfExist={getPaymentURL} + testIfExist={async (order) => { + const resp = await lib.instance.getOrderDetails(state.token, order); + return resp.type === "ok"; + }} onSelect={onSelect} description={i18n.str`jump to order with the given product ID`} - palceholder={i18n.str`order id`} + placeholder={i18n.str`order id`} /> <ListPage - orders={result.data.orders.map((o) => ({ ...o, id: o.order_id }))} - onLoadMoreBefore={result.loadMorePrev} - hasMoreBefore={!result.isReachingStart} - onLoadMoreAfter={result.loadMore} - hasMoreAfter={!result.isReachingEnd} + orders={result.body.map((o) => ({ ...o, id: o.order_id }))} + onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst} + onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} onSelectOrder={(order) => onSelect(order.id)} onRefundOrder={(value) => setOrderToBeRefunded(value)} isAllActive={isAllActive} @@ -124,25 +130,36 @@ export default function OrderList({ isNotPaidActive={isNotPaidActive} isRefundedActive={isRefundedActive} jumpToDate={filter.date} - onCopyURL={(id) => - getPaymentURL(id).then((resp) => copyToClipboard(resp.data)) - } - onCreate={onCreate} onSelectDate={setNewDate} + onCopyURL={async (id) => { + const resp = await lib.instance.getOrderDetails(state.token, id); + if (resp.type === "ok") { + if (resp.body.order_status === "unpaid") { + copyToClipboard(resp.body.taler_pay_uri); + } else { + if (resp.body.contract_terms.fulfillment_url) { + copyToClipboard(resp.body.contract_terms.fulfillment_url); + } + } + copyToClipboard(resp.body.order_status); + } + }} + onCreate={onCreate} onShowAll={() => setFilter({})} - onShowNotPaid={() => setFilter({ paid: "no" })} - onShowPaid={() => setFilter({ paid: "yes" })} - onShowRefunded={() => setFilter({ refunded: "yes" })} - onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })} - onShowWired={() => setFilter({ wired: "yes" })} + onShowNotPaid={() => setFilter({ paid: false })} + onShowPaid={() => setFilter({ paid: true })} + onShowRefunded={() => setFilter({ refunded: true })} + onShowNotWired={() => setFilter({ wired: false, paid: true })} + onShowWired={() => setFilter({ wired: true })} /> {orderToBeRefunded && ( <RefundModalForTable id={orderToBeRefunded.order_id} onCancel={() => setOrderToBeRefunded(undefined)} - onConfirm={(value) => - refundOrder(orderToBeRefunded.order_id, value) + onConfirm={(value) => { + lib.instance + .addRefund(state.token, orderToBeRefunded.order_id, value) .then(() => setNotif({ message: i18n.str`refund created successfully`, @@ -156,26 +173,7 @@ export default function OrderList({ description: error.message, }), ) - .then(() => setOrderToBeRefunded(undefined)) - } - onLoadError={(error) => { - setNotif({ - message: i18n.str`could not create the refund`, - type: "ERROR", - description: error.message, - }); - setOrderToBeRefunded(undefined); - return <div />; - }} - onUnauthorized={onUnauthorized} - onNotFound={() => { - setNotif({ - message: i18n.str`could not get the order to refund`, - type: "ERROR", - // description: error.message - }); - setOrderToBeRefunded(undefined); - return <div />; + .then(() => setOrderToBeRefunded(undefined)); }} /> )} @@ -185,41 +183,42 @@ export default function OrderList({ interface RefundProps { id: string; - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; onCancel: () => void; - onConfirm: (m: MerchantBackend.Orders.RefundRequest) => void; + onConfirm: (m: TalerMerchantApi.RefundRequest) => void; } -function RefundModalForTable({ - id, - onUnauthorized, - onLoadError, - onNotFound, - onConfirm, - onCancel, -}: RefundProps): VNode { +function RefundModalForTable({ id, onConfirm, onCancel }: RefundProps): VNode { const result = useOrderDetails(id); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.BadGateway: { + return <div>Failed to obtain a response from the exchange</div>; + } + case HttpStatusCode.GatewayTimeout: { + return ( + <div>The merchant's interaction with the exchange took too long</div> + ); + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } } return ( <RefundModal - order={result.data} + order={result.body} onCancel={onCancel} onConfirm={onConfirm} /> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx index 26f851cc8..36b31ebe8 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/Create.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx index 5f1ae26a3..d5522c2d4 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,8 +19,13 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { + TalerMerchantApi, + isRfc3548Base32Charset, + randomRfc3548Base32Key, +} from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -28,18 +33,10 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { isBase32RFC3548Charset, randomBase32Key } from "../../../../utils/crypto.js"; -import { QR } from "../../../../components/exception/QR.js"; -import { useInstanceContext } from "../../../../context/instance.js"; -type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; +type Entity = TalerMerchantApi.OtpDeviceAddDetails; interface Props { onCreate: (d: Entity) => Promise<void>; @@ -49,32 +46,32 @@ interface Props { const algorithms = [0, 1, 2]; const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"]; - export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const backend = useBackendContext(); const [state, setState] = useState<Partial<Entity>>({}); const [showKey, setShowKey] = useState(false); const errors: FormErrors<Entity> = { - otp_device_id: !state.otp_device_id ? i18n.str`required` + otp_device_id: !state.otp_device_id + ? i18n.str`required` : !/[a-zA-Z0-9]*/.test(state.otp_device_id) ? i18n.str`no valid. only characters and numbers` : undefined, otp_algorithm: !state.otp_algorithm ? i18n.str`required` : undefined, - otp_key: !state.otp_key ? i18n.str`required` : - !isBase32RFC3548Charset(state.otp_key) + otp_key: !state.otp_key + ? i18n.str`required` + : !isRfc3548Base32Charset(state.otp_key) ? i18n.str`just letters and numbers from 2 to 7` : state.otp_key.length !== 32 ? i18n.str`size of the key should be 32` : undefined, - otp_device_description: !state.otp_device_description ? i18n.str`required` + otp_device_description: !state.otp_device_description + ? i18n.str`required` : !/[a-zA-Z0-9]*/.test(state.otp_device_description) ? i18n.str`no valid. only characters and numbers` : undefined, - }; const hasErrors = Object.keys(errors).some( @@ -115,7 +112,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { toStr={(v) => algorithmsNames[v]} fromStr={(v) => Number(v)} /> - {state.otp_algorithm && state.otp_algorithm > 0 ? ( + {state.otp_algorithm ? ( <Fragment> <InputWithAddon<Entity> expand @@ -129,7 +126,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { setShowKey(!showKey); }} addonAfter={ - <span class="icon" > + <span class="icon"> {showKey ? ( <i class="mdi mdi-eye" /> ) : ( @@ -142,7 +139,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { data-tooltip={i18n.str`generate random secret key`} class="button is-info mr-3" onClick={(e) => { - setState((s) => ({ ...s, otp_key: randomBase32Key() })); + setState((s) => ({ + ...s, + otp_key: randomRfc3548Base32Key(), + })); + e.preventDefault(); }} > <i18n.Translate>random</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx index db3842711..7723bec81 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -14,41 +14,35 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; +import { VNode, h } from "preact"; import { QR } from "../../../../components/exception/QR.js"; import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { useInstanceContext } from "../../../../context/instance.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useBackendContext } from "../../../../context/backend.js"; +import { useSessionContext } from "../../../../context/session.js"; -type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; +type Entity = TalerMerchantApi.OtpDeviceAddDetails; interface Props { entity: Entity; onConfirm: () => void; } -function isNotUndefined<X>(x: X | undefined): x is X { - return !!x; -} - export function CreatedSuccessfully({ entity, onConfirm, }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const { id: instanceId } = useInstanceContext(); - const issuer = new URL(backendURL).hostname; - const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; - const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`; + const { state } = useSessionContext(); + const issuer = state.backendUrl.href; + const qrText = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`; + const qrTextSafe = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`; return ( <Template onConfirm={onConfirm} > <p class="is-size-5"> <i18n.Translate> - You can scan the next QR code with your device or safe the key before continue. + You can scan the next QR code with your device or save the key before continuing. </i18n.Translate> </p> <div class="field is-horizontal"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx index 648846793..8ab0e1f26 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,28 +19,28 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useWebhookAPI } from "../../../../hooks/webhooks.js"; +import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -import { useOtpDeviceAPI } from "../../../../hooks/otp.js"; import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; -export type Entity = MerchantBackend.OTP.OtpDeviceAddDetails; +export type Entity = TalerMerchantApi.OtpDeviceAddDetails; interface Props { onBack?: () => void; onConfirm: () => void; } export default function CreateValidator({ onConfirm, onBack }: Props): VNode { - const { createOtpDevice } = useOtpDeviceAPI(); + const { lib: api } = useSessionContext(); + const { state } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - const [created, setCreated] = useState<MerchantBackend.OTP.OtpDeviceAddDetails | null>(null) + const [created, setCreated] = useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null) if (created) { return <CreatedSuccessfully entity={created} onConfirm={onConfirm} /> @@ -52,7 +52,7 @@ export default function CreateValidator({ onConfirm, onBack }: Props): VNode { <CreatePage onBack={onBack} onCreate={(request: Entity) => { - return createOtpDevice(request) + return api.instance.addOtpDevice(state.token, request) .then((d) => { setCreated(request) }) diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx index b18049674..49032c80e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/List.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx index 4efee9781..8ca0a9c58 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/ListPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,18 +19,17 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { CardTable } from "./Table.js"; export interface Props { - devices: MerchantBackend.OTP.OtpDeviceEntry[]; + devices: TalerMerchantApi.OtpDeviceEntry[]; onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; onCreate: () => void; - onDelete: (e: MerchantBackend.OTP.OtpDeviceEntry) => void; - onSelect: (e: MerchantBackend.OTP.OtpDeviceEntry) => void; + onDelete: (e: TalerMerchantApi.OtpDeviceEntry) => void; + onSelect: (e: TalerMerchantApi.OtpDeviceEntry) => void; } export function ListPage({ @@ -41,9 +40,7 @@ export function ListPage({ onLoadMoreBefore, onLoadMoreAfter, }: Props): VNode { - const form = { payto_uri: "" }; - const { i18n } = useTranslationContext(); return ( <section class="section is-main-section"> <CardTable @@ -55,9 +52,7 @@ export function ListPage({ onDelete={onDelete} onSelect={onSelect} onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} /> </section> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx index 0c28027fe..afe3c98e2 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/Table.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,12 +19,12 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../../declaration.js"; -type Entity = MerchantBackend.OTP.OtpDeviceEntry; +type Entity = TalerMerchantApi.OtpDeviceEntry; interface Props { devices: Entity[]; @@ -32,8 +32,6 @@ interface Props { onSelect: (e: Entity) => void; onCreate: () => void; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } @@ -44,8 +42,6 @@ export function CardTable({ onSelect, onLoadMoreAfter, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -85,8 +81,6 @@ export function CardTable({ rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} /> ) : ( <EmptyTable /> @@ -104,35 +98,26 @@ interface TableProps { onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - function Table({ instances, onLoadMoreAfter, onDelete, onSelect, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class="table-container"> - {hasMoreBefore && ( + {onLoadMoreBefore && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more devices before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load newer devices</i18n.Translate> + <i18n.Translate>load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -161,7 +146,7 @@ function Table({ onClick={(): void => onSelect(i)} style={{ cursor: "pointer" }} > - {i.otp_device_id} + {i.device_description} </td> <td class="is-actions-cell right-sticky"> <div class="buttons is-right"> @@ -179,13 +164,13 @@ function Table({ })} </tbody> </table> - {hasMoreAfter && ( + {onLoadMoreAfter && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more devices after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load older devices</i18n.Translate> + <i18n.Translate>load next page</i18n.Translate> </button> )} </div> @@ -198,7 +183,7 @@ function EmptyTable(): VNode { <div class="content has-text-grey has-text-centered"> <p> <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> + <i class="mdi mdi-magnify mdi-48px" /> </span> </p> <p> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx index 2aae8738a..b6a077863 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,55 +19,56 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { HttpStatusCode } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable +} from "@gnu-taler/taler-util"; +import { + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; onCreate: () => void; onSelect: (id: string) => void; } -export default function ListOtpDevices({ - onUnauthorized, - onLoadError, - onCreate, - onSelect, - onNotFound, -}: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); +export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode { + // const [position, setPosition] = useState<string | undefined>(undefined); const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteOtpDevice } = useOtpDeviceAPI(); - const result = useInstanceOtpDevices({ position }, (id) => setPosition(id)); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); + const result = useInstanceOtpDevices(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } } return ( @@ -75,17 +76,16 @@ export default function ListOtpDevices({ <NotificationCard notification={notif} /> <ListPage - devices={result.data.otp_devices} - onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined - } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + devices={result.body.otp_devices} + onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} + onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} onCreate={onCreate} onSelect={(e) => { onSelect(e.otp_device_id); }} - onDelete={(e: MerchantBackend.OTP.OtpDeviceEntry) => - deleteOtpDevice(e.otp_device_id) + onDelete={(e: TalerMerchantApi.OtpDeviceEntry) => { + return lib.instance + .deleteOtpDevice(state.token, e.otp_device_id) .then(() => setNotif({ message: i18n.str`validator delete successfully`, @@ -98,8 +98,8 @@ export default function ListOtpDevices({ type: "ERROR", description: error.message, }), - ) - } + ); + }} /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx index d6b1d65e0..06ea9d07a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/Update.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx index b82807cc7..35d67cbc6 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/UpdatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { randomRfc3548Base32Key, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -28,12 +29,10 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { randomBase32Key } from "../../../../utils/crypto.js"; -type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId; +type Entity = TalerMerchantApi.OtpDevicePatchDetails & WithId; interface Props { onUpdate: (d: Entity) => Promise<void>; @@ -48,8 +47,7 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { const [state, setState] = useState<Partial<Entity>>(device); const [showKey, setShowKey] = useState(false); - const errors: FormErrors<Entity> = { - }; + const errors: FormErrors<Entity> = {}; const hasErrors = Object.keys(errors).some( (k) => (errors as any)[k] !== undefined, @@ -106,16 +104,23 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { label={i18n.str`Device key`} readonly={state.otp_key === undefined} inputType={showKey ? "text" : "password"} - help={state.otp_key === undefined ? "Not modified" : "Be sure to be very hard to guess or use the random generator"} + help={ + state.otp_key === undefined + ? "Not modified" + : "Be sure to be very hard to guess or use the random generator" + } tooltip={i18n.str`Your device need to have exactly the same value`} fromStr={(v) => v.toUpperCase()} addonAfterAction={() => { setShowKey(!showKey); }} addonAfter={ - <span class="icon" onClick={() => { - setShowKey(!showKey); - }}> + <span + class="icon" + onClick={() => { + setShowKey(!showKey); + }} + > {showKey ? ( <i class="mdi mdi-eye" /> ) : ( @@ -124,25 +129,34 @@ export function UpdatePage({ device, onUpdate, onBack }: Props): VNode { </span> } side={ - state.otp_key === undefined ? <button - - onClick={(e) => { - setState((s) => ({ ...s, otp_key: "" })); - }} - class="button">change key</button> : + state.otp_key === undefined ? ( + <button + onClick={(e) => { + setState((s) => ({ ...s, otp_key: "" })); + }} + class="button" + > + change key + </button> + ) : ( <button data-tooltip={i18n.str`generate random secret key`} class="button is-info mr-3" onClick={(e) => { - setState((s) => ({ ...s, otp_key: randomBase32Key() })); + setState((s) => ({ + ...s, + otp_key: randomRfc3548Base32Key(), + })); }} > <i18n.Translate>random</i18n.Translate> </button> + ) } /> </Fragment> - ) : undefined} </FormProvider> + ) : undefined}{" "} + </FormProvider> <div class="buttons is-right mt-5"> {onBack && ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx index 52f6c6c29..99edb95c3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/update/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -20,57 +20,68 @@ */ import { - ErrorType, - HttpError, - useTranslationContext, + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable +} from "@gnu-taler/taler-util"; +import { + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useOtpDeviceDetails } from "../../../../hooks/otp.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { CreatedSuccessfully } from "../create/CreatedSuccessfully.js"; import { UpdatePage } from "./UpdatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { useOtpDeviceAPI, useOtpDeviceDetails } from "../../../../hooks/otp.js"; -export type Entity = MerchantBackend.OTP.OtpDevicePatchDetails & WithId; +export type Entity = TalerMerchantApi.OtpDevicePatchDetails & WithId; interface Props { onBack?: () => void; onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; vid: string; } export default function UpdateValidator({ vid, onConfirm, onBack, - onUnauthorized, - onNotFound, - onLoadError, }: Props): VNode { - const { updateOtpDevice } = useOtpDeviceAPI(); const result = useOtpDeviceDetails(vid); const [notif, setNotif] = useState<Notification | undefined>(undefined); + const [keyUpdated, setKeyUpdated] = + useState<TalerMerchantApi.OtpDeviceAddDetails | null>(null); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } + } + + if (keyUpdated) { + return <CreatedSuccessfully entity={keyUpdated} onConfirm={onConfirm} />; } return ( @@ -79,15 +90,49 @@ export default function UpdateValidator({ <UpdatePage device={{ id: vid, - otp_algorithm: result.data.otp_algorithm, - otp_device_description: result.data.device_description, - otp_key: undefined, - otp_ctr: result.data.otp_ctr + otp_algorithm: result.body.otp_algorithm, + otp_device_description: result.body.device_description, + otp_key: "", + otp_ctr: result.body.otp_ctr, }} onBack={onBack} - onUpdate={(data) => { - return updateOtpDevice(vid, data) - .then(onConfirm) + onUpdate={async (newInfo) => { + return lib.instance + .updateOtpDevice(state.token, vid, newInfo) + .then((d) => { + if (d.type === "ok") { + if (newInfo.otp_key) { + setKeyUpdated({ + otp_algorithm: newInfo.otp_algorithm, + otp_device_description: newInfo.otp_device_description, + otp_device_id: newInfo.id, + otp_key: newInfo.otp_key, + otp_ctr: newInfo.otp_ctr, + }); + } else { + onConfirm(); + } + } else { + switch(d.case) { + case HttpStatusCode.NotFound: { + setNotif({ + message: i18n.str`Could not update template`, + type: "ERROR", + description: i18n.str`Template id is unknown`, + }); + break; + } + case HttpStatusCode.Conflict: { + setNotif({ + message: i18n.str`Could not update template`, + type: "ERROR", + description: i18n.str`The provided information is inconsistent with the current state of the template`, + }); + break; + } + } + } + }) .catch((error) => { setNotif({ message: i18n.str`could not update template`, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx index 2fc0819bb..22bbfe28a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/Create.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx index becaf8f3a..64b174f64 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,14 +19,14 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { ProductForm } from "../../../../components/product/ProductForm.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { useListener } from "../../../../hooks/listener.js"; -type Entity = MerchantBackend.Products.ProductAddDetail & { +type Entity = TalerMerchantApi.ProductAddDetail & { product_id: string; }; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx index 6b02430cc..2b6ebed45 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/CreatedSuccessfully.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx index 775690bd1..9de5cae78 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/create/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,22 +19,23 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useProductAPI } from "../../../../hooks/product.js"; +import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -export type Entity = MerchantBackend.Products.ProductAddDetail; +export type Entity = TalerMerchantApi.ProductAddDetail; interface Props { onBack?: () => void; onConfirm: () => void; } export default function CreateProduct({ onConfirm, onBack }: Props): VNode { - const { createProduct } = useProductAPI(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); @@ -43,8 +44,8 @@ export default function CreateProduct({ onConfirm, onBack }: Props): VNode { <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: MerchantBackend.Products.ProductAddDetail) => { - return createProduct(request) + onCreate={(request: TalerMerchantApi.ProductAddDetail) => { + return lib.instance.addProduct(state.token, request) .then(() => onConfirm()) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx index c2c4d548c..580a92cdc 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/List.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,7 +19,8 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; +import { AmountString } from "@gnu-taler/taler-util"; +import { FunctionalComponent, h } from "preact"; import { CardTable as TestedComponent } from "./Table.js"; export default { @@ -49,7 +50,7 @@ export const Example = createExample(TestedComponent, { description: "description1", description_i18n: {} as any, image: "", - price: "TESTKUDOS:10", + price: "TESTKUDOS:10" as AmountString, taxes: [], total_lost: 10, total_sold: 5, 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 index 275f855cb..9d5701fa7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,10 +19,10 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { Amounts } from "@gnu-taler/taler-util"; +import { AmountString, Amounts, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { ComponentChildren, Fragment, h, VNode } from "preact"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; import { StateUpdater, useState } from "preact/hooks"; import emptyImage from "../../../../assets/empty.png"; import { @@ -31,10 +31,9 @@ import { } from "../../../../components/form/FormProvider.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; +import { dateFormatForSettings, usePreference } from "../../../../hooks/preference.js"; -type Entity = MerchantBackend.Products.ProductDetail & WithId; +type Entity = TalerMerchantApi.ProductDetail & WithId; interface Props { instances: Entity[]; @@ -42,10 +41,12 @@ interface Props { onSelect: (product: Entity) => void; onUpdate: ( id: string, - data: MerchantBackend.Products.ProductPatchDetail, + data: TalerMerchantApi.ProductPatchDetail, ) => Promise<void>; onCreate: () => void; selected?: boolean; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; } export function CardTable({ @@ -54,6 +55,8 @@ export function CardTable({ onSelect, onUpdate, onDelete, + onLoadMoreAfter, + onLoadMoreBefore }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string | undefined>( undefined, @@ -90,6 +93,8 @@ export function CardTable({ onSelect={onSelect} onDelete={onDelete} onUpdate={onUpdate} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} rowSelection={rowSelection} rowSelectionHandler={rowSelectionHandler} /> @@ -108,10 +113,12 @@ interface TableProps { onSelect: (id: Entity) => void; onUpdate: ( id: string, - data: MerchantBackend.Products.ProductPatchDetail, + data: TalerMerchantApi.ProductPatchDetail, ) => Promise<void>; onDelete: (id: Entity) => void; rowSelectionHandler: StateUpdater<string | undefined>; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; } function Table({ @@ -121,11 +128,18 @@ function Table({ onSelect, onUpdate, onDelete, + onLoadMoreAfter, + onLoadMoreBefore }: TableProps): VNode { const { i18n } = useTranslationContext(); - const [settings] = useSettings(); + const [settings] = usePreference(); return ( <div class="table-container"> + {onLoadMoreBefore && ( + <button class="button is-fullwidth" onClick={onLoadMoreBefore}> + <i18n.Translate>load first page</i18n.Translate> + </button> + )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> <thead> <tr> @@ -197,14 +211,14 @@ function Table({ /> </td> <td - class="has-tooltip-right" + class="has-tooltip-right" data-tooltip={i.description} onClick={() => rowSelection !== i.id && rowSelectionHandler(i.id) } style={{ cursor: "pointer" }} > - {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description} + {i.description.length > 30 ? i.description.substring(0, 30) + "..." : i.description} </td> <td onClick={() => @@ -244,9 +258,9 @@ function Table({ } style={{ cursor: "pointer" }} > - <span style={{"whiteSpace":"nowrap"}}> + <span style={{ "whiteSpace": "nowrap" }}> - {i.total_sold} {i.unit} + {i.total_sold} {i.unit} </span> </td> <td class="is-actions-cell right-sticky"> @@ -284,7 +298,7 @@ function Table({ <FastProductUpdateForm product={i} onUpdate={(prod) => - onUpdate(i.id, prod).then((r) => + onUpdate(i.id, prod).then(() => rowSelectionHandler(undefined), ) } @@ -298,6 +312,13 @@ function Table({ })} </tbody> </table> + {onLoadMoreAfter && ( + <button class="button is-fullwidth" + data-tooltip={i18n.str`load more products after the last one`} + onClick={onLoadMoreAfter}> + <i18n.Translate>load next page</i18n.Translate> + </button> + )} </div> ); } @@ -305,7 +326,7 @@ function Table({ interface FastProductUpdateFormProps { product: Entity; onUpdate: ( - data: MerchantBackend.Products.ProductPatchDetail, + data: TalerMerchantApi.ProductPatchDetail, ) => Promise<void>; onCancel: () => void; } @@ -342,12 +363,6 @@ function FastProductWithInfiniteStockUpdateForm({ <div class="buttons is-expanded"> - <div class="buttons mt-5"> - - <button class="button mt-5" onClick={onCancel}> - <i18n.Translate>Clone</i18n.Translate> - </button> - </div> <div class="buttons is-right mt-5"> <button class="button" onClick={onCancel}> <i18n.Translate>Cancel</i18n.Translate> @@ -361,7 +376,7 @@ function FastProductWithInfiniteStockUpdateForm({ onClick={() => onUpdate({ ...product, - price: value.price, + price: value.price as AmountString, }) } > @@ -397,7 +412,7 @@ function FastProductWithManagedStockUpdateForm({ }; const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string,unknown>)[k] !== undefined, ); const { i18n } = useTranslationContext(); @@ -446,7 +461,7 @@ function FastProductWithManagedStockUpdateForm({ ...product, total_stock: product.total_stock + value.incoming, total_lost: product.total_lost + value.lost, - price: value.price, + price: value.price as AmountString, }) } > @@ -472,7 +487,7 @@ function EmptyTable(): VNode { <div class="content has-text-grey has-text-centered"> <p> <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> + <i class="mdi mdi-magnify mdi-48px" /> </span> </p> <p> @@ -491,6 +506,6 @@ function difference(price: string, tax: number) { ps[1] = `${p - tax}`; return ps.join(":"); } -function sum(taxes: MerchantBackend.Tax[]) { +function sum(taxes: TalerMerchantApi.Tax[]) { return taxes.reduce((p, c) => p + parseInt(c.tax.split(":")[1], 10), 0); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx index 942b5d0ac..6ad0d4598 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,61 +19,59 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { ConfirmModal } from "../../../../components/modal/index.js"; +import { useSessionContext } from "../../../../context/session.js"; import { - useInstanceProducts, - useProductAPI, + useInstanceProducts } from "../../../../hooks/product.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { CardTable } from "./Table.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js"; -import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; interface Props { - onUnauthorized: () => VNode; - onNotFound: () => VNode; onCreate: () => void; onSelect: (id: string) => void; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; } export default function ProductList({ - onUnauthorized, - onLoadError, onCreate, onSelect, - onNotFound, }: Props): VNode { const result = useInstanceProducts(); - const { deleteProduct, updateProduct, getProduct } = useProductAPI(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const [deleting, setDeleting] = - useState<MerchantBackend.Products.ProductDetail & WithId | null>(null); + useState<TalerMerchantApi.ProductDetail & WithId | null>(null); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } } return ( @@ -81,33 +79,38 @@ export default function ProductList({ <NotificationCard notification={notif} /> <JumpToElementById - testIfExist={getProduct} + testIfExist={async (id) => { + const resp = await lib.instance.getProductDetails(state.token, id); + return resp.type === "ok"; + }} onSelect={onSelect} description={i18n.str`jump to product with the given product ID`} - palceholder={i18n.str`product id`} + placeholder={i18n.str`product id`} /> <CardTable - instances={result.data} + instances={result.body} + onLoadMoreBefore={result.isFirstPage ? undefined : result.loadFirst} + onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} onCreate={onCreate} - onUpdate={(id, prod) => - updateProduct(id, prod) - .then(() => - setNotif({ - message: i18n.str`product updated successfully`, - type: "SUCCESS", - }), - ) - .catch((error) => - setNotif({ - message: i18n.str`could not update the product`, - type: "ERROR", - description: error.message, - }), - ) - } + onUpdate={async (id, prod) => { + try { + await lib.instance.updateProduct(state.token, id, prod); + setNotif({ + message: i18n.str`product updated successfully`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n.str`could not update the product`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + return + }} onSelect={(product) => onSelect(product.id)} - onDelete={(prod: MerchantBackend.Products.ProductDetail & WithId) => + onDelete={(prod: TalerMerchantApi.ProductDetail & WithId) => setDeleting(prod) } /> @@ -121,7 +124,7 @@ export default function ProductList({ onCancel={() => setDeleting(null)} onConfirm={async (): Promise<void> => { try { - await deleteProduct(deleting.id); + await lib.instance.deleteProduct(state.token, deleting.id); setNotif({ message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`, type: "SUCCESS", diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx index a85b13b8b..7aa93b186 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/Update.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,7 +19,8 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; +import { AmountString } from "@gnu-taler/taler-util"; +import { FunctionalComponent, h } from "preact"; import { UpdatePage as TestedComponent } from "./UpdatePage.js"; export default { @@ -46,7 +47,7 @@ export const WithManagedStock = createExample(TestedComponent, { description: "description1", description_i18n: {} as any, image: "", - price: "TESTKUDOS:10", + price: "TESTKUDOS:10" as AmountString, taxes: [], total_lost: 10, total_sold: 5, @@ -62,7 +63,7 @@ export const WithInfiniteStock = createExample(TestedComponent, { description: "description1", description_i18n: {} as any, image: "", - price: "TESTKUDOS:10", + price: "TESTKUDOS:10" as AmountString, taxes: [], total_lost: 10, total_sold: 5, 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 index 97715171e..5395ae40f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/UpdatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,14 +19,14 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { ProductForm } from "../../../../components/product/ProductForm.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { useListener } from "../../../../hooks/listener.js"; -type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }; +type Entity = TalerMerchantApi.ProductDetail & { product_id: string }; interface Props { onUpdate: (d: Entity) => Promise<void>; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx index 8e0f7647f..5e3e58d80 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,67 +19,66 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useProductAPI, useProductDetails } from "../../../../hooks/product.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useProductDetails } from "../../../../hooks/product.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -export type Entity = MerchantBackend.Products.ProductAddDetail; +export type Entity = TalerMerchantApi.ProductAddDetail; interface Props { onBack?: () => void; onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; pid: string; } export default function UpdateProduct({ pid, onConfirm, onBack, - onUnauthorized, - onNotFound, - onLoadError, }: Props): VNode { - const { updateProduct } = useProductAPI(); const result = useProductDetails(pid); const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } } return ( <Fragment> <NotificationCard notification={notif} /> <UpdatePage - product={{ ...result.data, product_id: pid }} + product={{ ...result.body, product_id: pid }} onBack={onBack} onUpdate={(data) => { - return updateProduct(pid, data) + return lib.instance.updateProduct(state.token, pid, data) .then(onConfirm) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx deleted file mode 100644 index 5542c028a..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/Create.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Reserve/Create", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, {}); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx deleted file mode 100644 index e46941b6d..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx +++ /dev/null @@ -1,277 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { HttpError, RequestError, useApiContext, useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { StateUpdater, useEffect, useState } from "preact/hooks"; -import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { - PAYTO_WIRE_METHOD_LOOKUP, - URL_REGEX, -} from "../../../../utils/constants.js"; -import { useBackendBaseRequest } from "../../../../hooks/backend.js"; -import { parsePaytoUri } from "@gnu-taler/taler-util"; - -type Entity = MerchantBackend.Rewards.ReserveCreateRequest; - -interface Props { - onCreate: (d: Entity) => Promise<void>; - onBack?: () => void; -} - -enum Steps { - EXCHANGE, - WIRE_METHOD, -} - -interface ViewProps { - step: Steps; - setCurrentStep: (s: Steps) => void; - reserve: Partial<Entity>; - onBack?: () => void; - submitForm: () => Promise<void>; - setReserve: StateUpdater<Partial<Entity>>; -} -function ViewStep({ - step, - setCurrentStep, - reserve, - onBack, - submitForm, - setReserve, -}: ViewProps): VNode { - const { i18n } = useTranslationContext(); - const {request} = useApiContext() - const [wireMethods, setWireMethods] = useState<Array<string>>([]); - const [exchangeQueryError, setExchangeQueryError] = useState< - string | undefined - >(undefined); - - useEffect(() => { - setExchangeQueryError(undefined); - }, [reserve.exchange_url]); - - switch (step) { - case Steps.EXCHANGE: { - const errors: FormErrors<Entity> = { - initial_balance: !reserve.initial_balance - ? "cannot be empty" - : !(parseInt(reserve.initial_balance.split(":")[1], 10) > 0) - ? i18n.str`it should be greater than 0` - : undefined, - exchange_url: !reserve.exchange_url - ? i18n.str`cannot be empty` - : !URL_REGEX.test(reserve.exchange_url) - ? i18n.str`must be a valid URL` - : !exchangeQueryError - ? undefined - : exchangeQueryError, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - return ( - <Fragment> - <FormProvider<Entity> - object={reserve} - errors={errors} - valueHandler={setReserve} - > - <InputCurrency<Entity> - name="initial_balance" - label={i18n.str`Initial balance`} - tooltip={i18n.str`balance prior to deposit`} - /> - <Input<Entity> - name="exchange_url" - label={i18n.str`Exchange URL`} - tooltip={i18n.str`URL of exchange`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - class="has-tooltip-left" - onClick={() => { - if (!reserve.exchange_url) { - return Promise.resolve(); - } - - return request<any>(reserve.exchange_url, "keys") - .then((r) => { - console.log(r) - if (r.loading) return; - if (r.ok) { - const wireMethods = r.data.accounts.map((a: any) => { - const p = parsePaytoUri(a.payto_uri); - const r = p?.targetType - return r - }).filter((x:any) => !!x); - setWireMethods(Array.from(new Set(wireMethods))); - } - setCurrentStep(Steps.WIRE_METHOD); - return; - }) - .catch((r: RequestError<{}>) => { - console.log(r.cause) - setExchangeQueryError(r.cause.message); - }); - }} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - disabled={hasErrors} - > - <i18n.Translate>Next</i18n.Translate> - </AsyncButton> - </div> - </Fragment> - ); - } - - case Steps.WIRE_METHOD: { - const errors: FormErrors<Entity> = { - wire_method: !reserve.wire_method - ? i18n.str`cannot be empty` - : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - return ( - <Fragment> - <FormProvider<Entity> - object={reserve} - errors={errors} - valueHandler={setReserve} - > - <InputCurrency<Entity> - name="initial_balance" - label={i18n.str`Initial balance`} - tooltip={i18n.str`balance prior to deposit`} - readonly - /> - <Input<Entity> - name="exchange_url" - label={i18n.str`Exchange URL`} - tooltip={i18n.str`URL of exchange`} - readonly - /> - <InputSelector<Entity> - name="wire_method" - label={i18n.str`Wire method`} - tooltip={i18n.str`method to use for wire transfer`} - values={wireMethods} - placeholder={i18n.str`Select one wire method`} - /> - </FormProvider> - <div class="buttons is-right mt-5"> - {onBack && ( - <button - class="button" - onClick={() => setCurrentStep(Steps.EXCHANGE)} - > - <i18n.Translate>Back</i18n.Translate> - </button> - )} - <AsyncButton - onClick={submitForm} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - disabled={hasErrors} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </div> - </Fragment> - ); - } - } -} - -export function CreatePage({ onCreate, onBack }: Props): VNode { - const [reserve, setReserve] = useState<Partial<Entity>>({}); - - const submitForm = () => { - return onCreate(reserve as Entity); - }; - - const [currentStep, setCurrentStep] = useState(Steps.EXCHANGE); - - return ( - <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <div class="tabs is-toggle is-fullwidth is-small"> - <ul> - <li class={currentStep === Steps.EXCHANGE ? "is-active" : ""}> - <a style={{ cursor: "initial" }}> - <span>Step 1: Specify exchange</span> - </a> - </li> - <li - class={currentStep === Steps.WIRE_METHOD ? "is-active" : ""} - > - <a style={{ cursor: "initial" }}> - <span>Step 2: Select wire method</span> - </a> - </li> - </ul> - </div> - - <ViewStep - step={currentStep} - reserve={reserve} - setCurrentStep={setCurrentStep} - setReserve={setReserve} - submitForm={submitForm} - onBack={onBack} - /> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx deleted file mode 100644 index 445ca3ef0..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.stories.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatedSuccessfully as TestedComponent } from "./CreatedSuccessfully.js"; -import * as tests from "@gnu-taler/web-util/testing"; - -export default { - title: "Pages/Reserve/CreatedSuccessfully", - component: TestedComponent, - argTypes: { - onCreate: { action: "onCreate" }, - onBack: { action: "onBack" }, - }, -}; - -export const OneBankAccount = tests.createExample(TestedComponent, { - entity: { - request: { - exchange_url: "http://exchange.taler/", - initial_balance: "TESTKUDOS:1", - wire_method: "x-taler-bank", - }, - response: { - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - ], - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - }, -}); - -export const ThreeBankAccount = tests.createExample(TestedComponent, { - entity: { - request: { - exchange_url: "http://exchange.taler/", - initial_balance: "TESTKUDOS:1", - wire_method: "x-taler-bank", - }, - response: { - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/exchange_account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - { - payto_uri: "payto://x-taler-bank/bank1.taler:8080/asd", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - { - payto_uri: "payto://x-taler-bank/bank2.taler:8080/qwe", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - ], - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - }, -}); - -export const NoBankAccount = tests.createExample(TestedComponent, { - entity: { - request: { - exchange_url: "http://exchange.taler/", - initial_balance: "TESTKUDOS:1", - wire_method: "x-taler-bank", - }, - response: { - accounts: [ - { - payto_uri: "payo://x-talr-bank/bank.taler:8080/exchange_account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - { - payto_uri: "payto://x-taler-bank", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "asd", - conversion_url: "", - }, - ], - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - }, -}); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx deleted file mode 100644 index 1d512c843..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx +++ /dev/null @@ -1,190 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 { parsePaytoUri, stringifyPaytoUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, VNode, h } from "preact"; -import { QR } from "../../../../components/exception/QR.js"; -import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { MerchantBackend, WireAccount } from "../../../../declaration.js"; - -type Entity = { - request: MerchantBackend.Rewards.ReserveCreateRequest; - response: MerchantBackend.Rewards.ReserveCreateConfirmation; -}; - -interface Props { - entity: Entity; - onConfirm: () => void; - onCreateAnother?: () => void; -} - -function isNotUndefined<X>(x: X | undefined): x is X { - return !!x; -} - -export function CreatedSuccessfully({ - entity, - onConfirm, - onCreateAnother, -}: Props): VNode { - const { i18n } = useTranslationContext(); - return ( - <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Amount</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - readonly - class="input" - value={entity.request.initial_balance} - /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Wire transfer subject</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - class="input" - readonly - value={entity.response.reserve_pub} - /> - </p> - </div> - </div> - </div> - <ShowAccountsOfReserveAsQRWithLink - accounts={entity.response.accounts ?? []} - message={entity.response.reserve_pub} - amount={entity.request.initial_balance} - /> - </Template> - ); -} - -export function ShowAccountsOfReserveAsQRWithLink({ - accounts, - message, - amount, -}: { - accounts: WireAccount[]; - message: string; - amount: string; -}): VNode { - const { i18n } = useTranslationContext(); - const accountsInfo = !accounts - ? [] - : accounts - .map((acc) => { - const p = parsePaytoUri(acc.payto_uri); - if (p) { - p.params["message"] = message; - p.params["amount"] = amount; - } - return p; - }) - .filter(isNotUndefined); - - const links = accountsInfo.map((a) => stringifyPaytoUri(a)); - - if (links.length === 0) { - return ( - <Fragment> - <p class="is-size-5"> - The reserve have invalid accounts. List of invalid payto URIs below: - </p> - <ul> - {accounts.map((a, idx) => { - return <li key={idx}>{a.payto_uri}</li>; - })} - </ul> - </Fragment> - ); - } - - if (links.length === 1) { - return ( - <Fragment> - <p class="is-size-5"> - <i18n.Translate> - To complete the setup of the reserve, you must now initiate a wire - transfer using the given wire transfer subject and crediting the - specified amount to the indicated account of the exchange. - </i18n.Translate> - </p> - <p style={{ margin: 10 }}> - <b>Exchange bank account</b> - </p> - <QR text={links[0]} /> - <p class="is-size-5"> - <i18n.Translate> - If your system supports RFC 8905, you can do this by opening this - URI: - </i18n.Translate> - </p> - <pre> - <a target="_blank" rel="noreferrer" href={links[0]}> - {links[0]} - </a> - </pre> - </Fragment> - ); - } - - return ( - <div> - <p class="is-size-5"> - <i18n.Translate> - To complete the setup of the reserve, you must now initiate a wire - transfer using the given wire transfer subject and crediting the - specified amount to one of the indicated account of the exchange. - </i18n.Translate> - </p> - - <p style={{ margin: 10 }}> - <b>Exchange bank accounts</b> - </p> - <p class="is-size-5"> - <i18n.Translate> - If your system supports RFC 8905, you can do this by clicking on the - URI below the QR code: - </i18n.Translate> - </p> - {links.map((link) => { - return ( - <Fragment> - <QR text={link} /> - <pre> - <a target="_blank" rel="noreferrer" href={link}> - {link} - </a> - </pre> - </Fragment> - ); - })} - </div> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx deleted file mode 100644 index 4bbaf1459..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useReservesAPI } from "../../../../hooks/reserves.js"; -import { Notification } from "../../../../utils/types.js"; -import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; -import { CreatePage } from "./CreatePage.js"; -interface Props { - onBack: () => void; - onConfirm: () => void; -} -export default function CreateReserve({ onBack, onConfirm }: Props): VNode { - const { createReserve } = useReservesAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - - const [createdOk, setCreatedOk] = useState< - | { - request: MerchantBackend.Rewards.ReserveCreateRequest; - response: MerchantBackend.Rewards.ReserveCreateConfirmation; - } - | undefined - >(undefined); - - if (createdOk) { - return <CreatedSuccessfully entity={createdOk} onConfirm={onConfirm} />; - } - - return ( - <Fragment> - <NotificationCard notification={notif} /> - <CreatePage - onBack={onBack} - onCreate={(request: MerchantBackend.Rewards.ReserveCreateRequest) => { - return createReserve(request) - .then((r) => setCreatedOk({ request, response: r.data })) - .catch((error) => { - setNotif({ - message: i18n.str`could not create reserve`, - type: "ERROR", - description: error.message, - }); - }); - }} - /> - </Fragment> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx deleted file mode 100644 index d8840eeac..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx +++ /dev/null @@ -1,266 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { - Amounts, - parsePaytoUri, - stringifyPaytoUri, -} from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { QR } from "../../../../components/exception/QR.js"; -import { FormProvider } from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDate } from "../../../../components/form/InputDate.js"; -import { TextField } from "../../../../components/form/TextField.js"; -import { SimpleModal } from "../../../../components/modal/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useRewardDetails } from "../../../../hooks/reserves.js"; -import { RewardInfo } from "./RewardInfo.js"; -import { ShowAccountsOfReserveAsQRWithLink } from "../create/CreatedSuccessfully.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Rewards.ReserveDetail; -type CT = MerchantBackend.ContractTerms; - -interface Props { - onBack: () => void; - selected: Entity; - id: string; -} - -export function DetailPage({ id, selected, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - const didExchangeAckTransfer = Amounts.isNonZero( - Amounts.parseOrThrow(selected.exchange_initial_amount), - ); - - return ( - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <div class="section main-section"> - <FormProvider object={{ ...selected, id }} valueHandler={null}> - <InputDate<Entity> - name="creation_time" - label={i18n.str`Created at`} - readonly - /> - <InputDate<Entity> - name="expiration_time" - label={i18n.str`Valid until`} - readonly - /> - <InputCurrency<Entity> - name="merchant_initial_amount" - label={i18n.str`Created balance`} - readonly - /> - <TextField<Entity> - name="exchange_url" - label={i18n.str`Exchange URL`} - readonly - > - <a target="_blank" rel="noreferrer" href={selected.exchange_url}> - {selected.exchange_url} - </a> - </TextField> - - {didExchangeAckTransfer && ( - <Fragment> - <InputCurrency<Entity> - name="exchange_initial_amount" - label={i18n.str`Exchange balance`} - readonly - /> - <InputCurrency<Entity> - name="pickup_amount" - label={i18n.str`Picked up`} - readonly - /> - <InputCurrency<Entity> - name="committed_amount" - label={i18n.str`Committed`} - readonly - /> - </Fragment> - )} - <Input name="id" label={i18n.str`Subject`} readonly /> - </FormProvider> - - {didExchangeAckTransfer ? ( - <Fragment> - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-cash-register" /> - </span> - <i18n.Translate>Rewards</i18n.Translate> - </p> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {selected.rewards && selected.rewards.length > 0 ? ( - <Table rewards={selected.rewards} /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - </Fragment> - ) : selected.accounts ? ( - <ShowAccountsOfReserveAsQRWithLink - accounts={selected.accounts} - amount={selected.merchant_initial_amount} - message={id} - /> - ) : undefined} - - <div class="buttons is-right mt-5"> - <button class="button" onClick={onBack}> - <i18n.Translate>Back</i18n.Translate> - </button> - </div> - </div> - </div> - <div class="column" /> - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - No reward has been authorized from this reserve - </i18n.Translate> - </p> - </div> - ); -} - -interface TableProps { - rewards: MerchantBackend.Rewards.RewardStatusEntry[]; -} - -function Table({ rewards }: TableProps): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Authorized</i18n.Translate> - </th> - <th> - <i18n.Translate>Picked up</i18n.Translate> - </th> - <th> - <i18n.Translate>Reason</i18n.Translate> - </th> - <th> - <i18n.Translate>Expiration</i18n.Translate> - </th> - </tr> - </thead> - <tbody> - {rewards.map((t, i) => { - return <RewardRow id={t.reward_id} key={i} entry={t} />; - })} - </tbody> - </table> - </div> - ); -} - -function RewardRow({ - id, - entry, -}: { - id: string; - entry: MerchantBackend.Rewards.RewardStatusEntry; -}) { - const [selected, setSelected] = useState(false); - const result = useRewardDetails(id); - const [settings] = useSettings(); - if (result.loading) { - return ( - <tr> - <td>...</td> - <td>...</td> - <td>...</td> - <td>...</td> - </tr> - ); - } - if (!result.ok) { - return ( - <tr> - <td>...</td> {/* authorized */} - <td>{entry.total_amount}</td> - <td>{entry.reason}</td> - <td>...</td> {/* expired */} - </tr> - ); - } - const info = result.data; - function onSelect() { - setSelected(true); - } - return ( - <Fragment> - {selected && ( - <SimpleModal - description="reward" - active - onCancel={() => setSelected(false)} - > - <RewardInfo id={id} amount={info.total_authorized} entity={info} /> - </SimpleModal> - )} - <tr> - <td onClick={onSelect}>{info.total_authorized}</td> - <td onClick={onSelect}>{info.total_picked_up}</td> - <td onClick={onSelect}>{info.reason}</td> - <td onClick={onSelect}> - {info.expiration.t_s === "never" - ? "never" - : format(info.expiration.t_s * 1000, datetimeFormatForSettings(settings))} - </td> - </tr> - </Fragment> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx deleted file mode 100644 index 41c715f20..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { DetailPage as TestedComponent } from "./DetailPage.js"; - -export default { - title: "Pages/Reserve/Detail", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Funded = createExample(TestedComponent, { - id: "THISISTHERESERVEID", - selected: { - active: true, - committed_amount: "TESTKUDOS:10", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:10", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "", - }, - ], - exchange_url: "http://exchange.taler/", - }, -}); - -export const NotYetFunded = createExample(TestedComponent, { - id: "THISISTHERESERVEID", - selected: { - active: true, - committed_amount: "TESTKUDOS:10", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:0", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "", - }, - ], - exchange_url: "http://exchange.taler/", - }, -}); - -export const FundedWithEmptyRewards = createExample(TestedComponent, { - id: "THISISTHERESERVEID", - selected: { - active: true, - committed_amount: "TESTKUDOS:10", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:10", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - accounts: [ - { - payto_uri: "payto://x-taler-bank/bank.taler:8080/account", - credit_restrictions: [], - debit_restrictions: [], - master_sig: "", - }, - ], - exchange_url: "http://exchange.taler/", - rewards: [ - { - reason: "asdasd", - reward_id: "123", - total_amount: "TESTKUDOS:1", - }, - ], - }, -}); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx deleted file mode 100644 index 780068a91..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 { stringifyRewardUri } from "@gnu-taler/taler-util"; -import { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Rewards.RewardDetails; - -interface Props { - id: string; - entity: Entity; - amount: string; -} - -export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode { - const { url: backendURL } = useBackendContext() - const [settings] = useSettings(); - const rewardURL = stringifyRewardUri({ merchantBaseUrl: backendURL, merchantRewardId }) - return ( - <Fragment> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Amount</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={amount} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">URL</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field" style={{ overflowWrap: "anywhere" }}> - <p class="control"> - <a target="_blank" rel="noreferrer" href={rewardURL}> - {rewardURL} - </a> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Valid until</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - class="input" - readonly - value={ - !entity.expiration || entity.expiration.t_s === "never" - ? "never" - : format( - entity.expiration.t_s * 1000, - datetimeFormatForSettings(settings), - ) - } - /> - </p> - </div> - </div> - </div> - </Fragment> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx deleted file mode 100644 index 8e2a74529..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { Loading } from "../../../../components/exception/loading.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useReserveDetails } from "../../../../hooks/reserves.js"; -import { DetailPage } from "./DetailPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; - -interface Props { - rid: string; - - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; - onDelete: () => void; - onBack: () => void; -} -export default function DetailReserve({ - rid, - onUnauthorized, - onLoadError, - onNotFound, - onBack, - onDelete, -}: Props): VNode { - const result = useReserveDetails(rid); - - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); - } - return ( - <Fragment> - <DetailPage selected={result.data} onBack={onBack} id={rid} /> - </Fragment> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx deleted file mode 100644 index e205ee621..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import * as yup from "yup"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { - ConfirmModal, - ContinueModal, -} from "../../../../components/modal/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { AuthorizeRewardSchema } from "../../../../schemas/index.js"; -import { CreatedSuccessfully } from "./CreatedSuccessfully.js"; - -interface AuthorizeRewardModalProps { - onCancel: () => void; - onConfirm: (value: MerchantBackend.Rewards.RewardCreateRequest) => void; - rewardAuthorized?: { - response: MerchantBackend.Rewards.RewardCreateConfirmation; - request: MerchantBackend.Rewards.RewardCreateRequest; - }; -} - -export function AuthorizeRewardModal({ - onCancel, - onConfirm, - rewardAuthorized, -}: AuthorizeRewardModalProps): VNode { - // const result = useOrderDetails(id) - type State = MerchantBackend.Rewards.RewardCreateRequest; - const [form, setValue] = useState<Partial<State>>({}); - const { i18n } = useTranslationContext(); - - // const [errors, setErrors] = useState<FormErrors<State>>({}) - let errors: FormErrors<State> = {}; - try { - AuthorizeRewardSchema.validateSync(form, { abortEarly: false }); - } catch (err) { - if (err instanceof yup.ValidationError) { - const yupErrors = err.inner as any[]; - errors = yupErrors.reduce( - (prev, cur) => - !cur.path ? prev : { ...prev, [cur.path]: cur.message }, - {}, - ); - } - } - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); - - const validateAndConfirm = () => { - onConfirm(form as State); - }; - if (rewardAuthorized) { - return ( - <ContinueModal description="reward" active onConfirm={onCancel}> - <CreatedSuccessfully - entity={rewardAuthorized.response} - request={rewardAuthorized.request} - onConfirm={onCancel} - /> - </ContinueModal> - ); - } - - return ( - <ConfirmModal - description="New reward" - active - onCancel={onCancel} - disabled={hasErrors} - onConfirm={validateAndConfirm} - > - <FormProvider<State> - errors={errors} - object={form} - valueHandler={setValue} - > - <InputCurrency<State> - name="amount" - label={i18n.str`Amount`} - tooltip={i18n.str`amount of reward`} - /> - <Input<State> - name="justification" - label={i18n.str`Justification`} - inputType="multiline" - tooltip={i18n.str`reason for the reward`} - /> - <Input<State> - name="next_url" - label={i18n.str`URL after reward`} - tooltip={i18n.str`URL to visit after reward payment`} - /> - </FormProvider> - </ConfirmModal> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx deleted file mode 100644 index b78236bc7..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; -import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Rewards.RewardCreateConfirmation; - -interface Props { - entity: Entity; - request: MerchantBackend.Rewards.RewardCreateRequest; - onConfirm: () => void; - onCreateAnother?: () => void; -} - -export function CreatedSuccessfully({ - request, - entity, - onConfirm, - onCreateAnother, -}: Props): VNode { - const [settings] = useSettings(); - return ( - <Fragment> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Amount</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={request.amount} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Justification</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={request.justification} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">URL</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input readonly class="input" value={entity.reward_status_url} /> - </p> - </div> - </div> - </div> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label">Valid until</label> - </div> - <div class="field-body is-flex-grow-3"> - <div class="field"> - <p class="control"> - <input - class="input" - readonly - value={ - !entity.reward_expiration || - entity.reward_expiration.t_s === "never" - ? "never" - : format( - entity.reward_expiration.t_s * 1000, - datetimeFormatForSettings(settings), - ) - } - /> - </p> - </div> - </div> - </div> - </Fragment> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx deleted file mode 100644 index b070bbde3..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CardTable as TestedComponent } from "./Table.js"; - -export default { - title: "Pages/Reserve/List", - component: TestedComponent, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const AllFunded = createExample(TestedComponent, { - instances: [ - { - id: "reseverId", - active: true, - committed_amount: "TESTKUDOS:10", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:10", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - { - id: "reseverId2", - active: true, - committed_amount: "TESTKUDOS:13", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:10", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - ], -}); - -export const Empty = createExample(TestedComponent, { - instances: [], -}); - -export const OneNotYetFunded = createExample(TestedComponent, { - instances: [ - { - id: "reseverId", - active: true, - committed_amount: "TESTKUDOS:0", - creation_time: { - t_s: new Date().getTime() / 1000, - }, - exchange_initial_amount: "TESTKUDOS:0", - expiration_time: { - t_s: new Date().getTime() / 1000, - }, - merchant_initial_amount: "TESTKUDOS:10", - pickup_amount: "TESTKUDOS:10", - reserve_pub: "WEQWDASDQWEASDADASDQWEQWEASDAS", - }, - ], -}); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx deleted file mode 100644 index 795e7ec82..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx +++ /dev/null @@ -1,320 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { format } from "date-fns"; -import { Fragment, h, VNode } from "preact"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; - -type Entity = MerchantBackend.Rewards.ReserveStatusEntry & WithId; - -interface Props { - instances: Entity[]; - onNewReward: (id: Entity) => void; - onSelect: (id: Entity) => void; - onDelete: (id: Entity) => void; - onCreate: () => void; -} - -export function CardTable({ - instances, - onCreate, - onSelect, - onNewReward, - onDelete, -}: Props): VNode { - const [withoutFunds, withFunds] = instances.reduce((prev, current) => { - const amount = current.exchange_initial_amount; - if (amount.endsWith(":0")) { - prev[0] = prev[0].concat(current); - } else { - prev[1] = prev[1].concat(current); - } - return prev; - }, new Array<Array<Entity>>([], [])); - - const { i18n } = useTranslationContext(); - - return ( - <Fragment> - {withoutFunds.length > 0 && ( - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-cash" /> - </span> - <i18n.Translate>Reserves not yet funded</i18n.Translate> - </p> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - <TableWithoutFund - instances={withoutFunds} - onNewReward={onNewReward} - onSelect={onSelect} - onDelete={onDelete} - /> - </div> - </div> - </div> - </div> - )} - - <div class="card has-table"> - <header class="card-header"> - <p class="card-header-title"> - <span class="icon"> - <i class="mdi mdi-cash" /> - </span> - <i18n.Translate>Reserves ready</i18n.Translate> - </p> - <div class="card-header-icon" aria-label="more options" /> - <div class="card-header-icon" aria-label="more options"> - <span - class="has-tooltip-left" - data-tooltip={i18n.str`add new reserve`} - > - <button class="button is-info" type="button" onClick={onCreate}> - <span class="icon is-small"> - <i class="mdi mdi-plus mdi-36px" /> - </span> - </button> - </span> - </div> - </header> - <div class="card-content"> - <div class="b-table has-pagination"> - <div class="table-wrapper has-mobile-cards"> - {withFunds.length > 0 ? ( - <Table - instances={withFunds} - onNewReward={onNewReward} - onSelect={onSelect} - onDelete={onDelete} - /> - ) : ( - <EmptyTable /> - )} - </div> - </div> - </div> - </div> - </Fragment> - ); -} -interface TableProps { - instances: Entity[]; - onNewReward: (id: Entity) => void; - onDelete: (id: Entity) => void; - onSelect: (id: Entity) => void; -} - -function Table({ instances, onNewReward, onSelect, onDelete }: TableProps): VNode { - const { i18n } = useTranslationContext(); - const [settings] = useSettings(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Created at</i18n.Translate> - </th> - <th> - <i18n.Translate>Expires at</i18n.Translate> - </th> - <th> - <i18n.Translate>Initial</i18n.Translate> - </th> - <th> - <i18n.Translate>Picked up</i18n.Translate> - </th> - <th> - <i18n.Translate>Committed</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.id}> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.creation_time.t_s === "never" - ? "never" - : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.expiration_time.t_s === "never" - ? "never" - : format( - i.expiration_time.t_s * 1000, - datetimeFormatForSettings(settings), - )} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.exchange_initial_amount} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.pickup_amount} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.committed_amount} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-small is-danger has-tooltip-left" - data-tooltip={i18n.str`delete selected reserve from the database`} - type="button" - onClick={(): void => onDelete(i)} - > - Delete - </button> - <button - class="button is-small is-info has-tooltip-left" - data-tooltip={i18n.str`authorize new reward from selected reserve`} - type="button" - onClick={(): void => onNewReward(i)} - > - New Reward - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - ); -} - -function EmptyTable(): VNode { - const { i18n } = useTranslationContext(); - return ( - <div class="content has-text-grey has-text-centered"> - <p> - <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> - </span> - </p> - <p> - <i18n.Translate> - There is no ready reserves yet, add more pressing the + sign or fund - them - </i18n.Translate> - </p> - </div> - ); -} - -function TableWithoutFund({ - instances, - onSelect, - onDelete, -}: TableProps): VNode { - const { i18n } = useTranslationContext(); - const [settings] = useSettings(); - return ( - <div class="table-container"> - <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> - <thead> - <tr> - <th> - <i18n.Translate>Created at</i18n.Translate> - </th> - <th> - <i18n.Translate>Expires at</i18n.Translate> - </th> - <th> - <i18n.Translate>Expected Balance</i18n.Translate> - </th> - <th /> - </tr> - </thead> - <tbody> - {instances.map((i) => { - return ( - <tr key={i.id}> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.creation_time.t_s === "never" - ? "never" - : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.expiration_time.t_s === "never" - ? "never" - : format( - i.expiration_time.t_s * 1000, - datetimeFormatForSettings(settings), - )} - </td> - <td - onClick={(): void => onSelect(i)} - style={{ cursor: "pointer" }} - > - {i.merchant_initial_amount} - </td> - <td class="is-actions-cell right-sticky"> - <div class="buttons is-right"> - <button - class="button is-small is-danger jb-modal has-tooltip-left" - type="button" - data-tooltip={i18n.str`delete selected reserve from the database`} - onClick={(): void => onDelete(i)} - > - Delete - </button> - </div> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx deleted file mode 100644 index b26ff0000..000000000 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx +++ /dev/null @@ -1,171 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { - ErrorType, - HttpError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; -import { useState } from "preact/hooks"; -import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { - useInstanceReserves, - useReservesAPI, -} from "../../../../hooks/reserves.js"; -import { Notification } from "../../../../utils/types.js"; -import { AuthorizeRewardModal } from "./AutorizeRewardModal.js"; -import { CardTable } from "./Table.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { ConfirmModal } from "../../../../components/modal/index.js"; - -interface Props { - onUnauthorized: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onSelect: (id: string) => void; - onNotFound: () => VNode; - onCreate: () => void; -} - -interface RewardConfirmation { - response: MerchantBackend.Rewards.RewardCreateConfirmation; - request: MerchantBackend.Rewards.RewardCreateRequest; -} - -export default function ListRewards({ - onUnauthorized, - onLoadError, - onNotFound, - onSelect, - onCreate, -}: Props): VNode { - const result = useInstanceReserves(); - const { deleteReserve, authorizeRewardReserve } = useReservesAPI(); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { i18n } = useTranslationContext(); - const [reserveForReward, setReserveForReward] = useState<string | undefined>( - undefined, - ); - const [deleting, setDeleting] = - useState<MerchantBackend.Rewards.ReserveStatusEntry | null>(null); - const [rewardAuthorized, setRewardAuthorized] = useState< - RewardConfirmation | undefined - >(undefined); - - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); - } - - return ( - <section class="section is-main-section"> - <NotificationCard notification={notif} /> - - {reserveForReward && ( - <AuthorizeRewardModal - onCancel={() => { - setReserveForReward(undefined); - setRewardAuthorized(undefined); - }} - rewardAuthorized={rewardAuthorized} - onConfirm={async (request) => { - try { - const response = await authorizeRewardReserve( - reserveForReward, - request, - ); - setRewardAuthorized({ - request, - response: response.data, - }); - } catch (error) { - setNotif({ - message: i18n.str`could not create the reward`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - setReserveForReward(undefined); - } - }} - /> - )} - - <CardTable - instances={result.data.reserves - .filter((r) => r.active) - .map((o) => ({ ...o, id: o.reserve_pub }))} - onCreate={onCreate} - onDelete={(reserve) => { - setDeleting(reserve) - }} - onSelect={(reserve) => onSelect(reserve.id)} - onNewReward={(reserve) => setReserveForReward(reserve.id)} - /> - - {deleting && ( - <ConfirmModal - label={`Delete reserve`} - description={`Delete the reserve`} - danger - active - onCancel={() => setDeleting(null)} - onConfirm={async (): Promise<void> => { - try { - await deleteReserve(deleting.reserve_pub); - setNotif({ - message: i18n.str`Reserve for "${deleting.merchant_initial_amount}" (ID: ${deleting.reserve_pub}) has been deleted`, - type: "SUCCESS", - }); - } catch (error) { - setNotif({ - message: i18n.str`Failed to delete reserve`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined, - }); - } - setDeleting(null); - }} - > - <p> - If you delete the reserve for <b>"{deleting.merchant_initial_amount}"</b> you won't be able to create more rewards. <br /> - Reserve ID: <b>{deleting.reserve_pub}</b> - </p> - <p class="warning"> - Deleting an template <b>cannot be undone</b>. - </p> - </ConfirmModal> - )} - - </section> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx index c9d17ea3b..53025f153 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/Create.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx index 947f3572c..78d7c83ac 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -20,10 +20,16 @@ */ import { + AmountString, Amounts, - MerchantTemplateContractDetails, + Duration, + TalerError, + TalerMerchantApi, + TranslatedString, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; @@ -35,109 +41,127 @@ import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; +import { TextField } from "../../../../components/form/TextField.js"; +import { useSessionContext } from "../../../../context/session.js"; import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { InputTab } from "../../../../components/form/InputTab.js"; - -enum Steps { - BOTH_FIXED, - FIXED_PRICE, - FIXED_SUMMARY, - NON_FIXED, -} -type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps }; +// type Entity = TalerMerchantApi.TemplateAddDetails & { type: Steps }; +type Entity = { + id?: string; + description?: string; + otpId?: string; + summary?: string; + amount?: AmountString; + minimum_age?: number; + pay_duration?: Duration; + summary_editable?: boolean; + amount_editable?: boolean; + currency_editable?: boolean; +}; interface Props { - onCreate: (d: Entity) => Promise<void>; + onCreate: (d: TalerMerchantApi.TemplateAddDetails) => Promise<void>; onBack?: () => void; } export function CreatePage({ onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const devices = useInstanceOtpDevices() + const { config } = useSessionContext(); + const {state:session} = useSessionContext(); + const devices = useInstanceOtpDevices(); const [state, setState] = useState<Partial<Entity>>({ - template_contract: { - minimum_age: 0, - pay_duration: { - d_us: 1000 * 1000 * 60 * 30, //30 min - }, + minimum_age: 0, + pay_duration: { + d_ms: 1000 * 60 * 30, //30 min }, - type: Steps.NON_FIXED, }); - const parsedPrice = !state.template_contract?.amount - ? undefined - : Amounts.parse(state.template_contract?.amount); + function updateState(up: (s: Partial<Entity>) => Partial<Entity>) { + setState((old) => { + const newState = up(old); + if (!newState.amount_editable) { + newState.currency_editable = false; + } + return newState; + }); + } + + const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount); const errors: FormErrors<Entity> = { - template_id: !state.template_id + id: !state.id ? i18n.str`should not be empty` - : !/[a-zA-Z0-9]*/.test(state.template_id) + : !/[a-zA-Z0-9]*/.test(state.id) ? i18n.str`no valid. only characters and numbers` : undefined, - template_description: !state.template_description - ? i18n.str`should not be empty` - : undefined, - template_contract: !state.template_contract + description: !state.description ? i18n.str`should not be empty` : undefined, + amount: !state.amount ? undefined - : undefinedIfEmpty({ - amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.amount - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.summary - ? i18n.str`required` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` - : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" - ? undefined - : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second - ? i18n.str`to short` - : undefined, - } as Partial<MerchantTemplateContractDetails>), + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + minimum_age: + state.minimum_age && state.minimum_age < 0 + ? i18n.str`should be greater that 0` + : undefined, + pay_duration: !state.pay_duration + ? i18n.str`can't be empty` + : state.pay_duration.d_ms === "forever" + ? undefined + : state.pay_duration.d_ms < 1000 //less than one second + ? i18n.str`to short` + : undefined, }; + const cList = Object.values(config.currencies).map((d) => d.name); + const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); const submitForm = () => { if (hasErrors) return Promise.reject(); - if (state.template_contract) { - if (state.type === Steps.NON_FIXED) { - delete state.template_contract.amount; - delete state.template_contract.summary; - } else if (state.type === Steps.FIXED_SUMMARY) { - delete state.template_contract.amount; - } else if (state.type === Steps.FIXED_PRICE) { - delete state.template_contract.summary; - } - } - delete state.type - return onCreate(state as any); + return onCreate({ + template_id: state.id!, + template_description: state.description!, + template_contract: { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: state.amount_editable ? undefined : state.amount, + summary: state.summary_editable ? undefined : state.summary, + currency: + cList.length > 1 && state.currency_editable + ? undefined + : config.currency, + }, + editable_defaults: { + amount: !state.amount_editable ? undefined : state.amount, + summary: !state.summary_editable ? undefined : state.summary, + currency: + cList.length === 1 || !state.currency_editable + ? undefined + : config.currency, + }, + otp_id: state.otpId!, + }); }; - - const deviceList = !devices.ok ? [] : devices.data.otp_devices - + const deviceList = + !devices || devices instanceof TalerError || devices.type === "fail" + ? [] + : devices.body.otp_devices; + const deviceMap = deviceList.reduce( + (prev, cur) => { + prev[cur.otp_device_id] = cur.device_description as TranslatedString; + return prev; + }, + {} as Record<string, TranslatedString>, + ); return ( <div> <section class="section is-main-section"> @@ -146,90 +170,99 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <div class="column is-four-fifths"> <FormProvider object={state} - valueHandler={setState} + valueHandler={updateState} errors={errors} > <InputWithAddon<Entity> - name="template_id" - help={`${backendURL}/templates/${state.template_id ?? ""}`} + name="id" + help={ + new URL(`templates/${state.id ?? ""}`, session.backendUrl.href).href + } label={i18n.str`Identifier`} tooltip={i18n.str`Name of the template in URLs.`} /> <Input<Entity> - name="template_description" + name="description" label={i18n.str`Description`} help="" tooltip={i18n.str`Describe what this template stands for`} /> - <InputTab - name="type" - label={i18n.str`Type`} - help={(() => { - switch (state.type) { - case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` - case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` - case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` - case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` - } - })()} - tooltip={i18n.str`Define what the user be allowed to modify`} - values={[ - Steps.NON_FIXED, - Steps.FIXED_PRICE, - Steps.FIXED_SUMMARY, - Steps.BOTH_FIXED, - ]} - toStr={(v: Steps): string => { - switch (v) { - case Steps.NON_FIXED: return i18n.str`Simple` - case Steps.FIXED_PRICE: return i18n.str`With price` - case Steps.FIXED_SUMMARY: return i18n.str`With summary` - case Steps.BOTH_FIXED: return i18n.str`With price and summary` - } - }} + + <Input<Entity> + name="summary" + inputType="multiline" + label={i18n.str`Summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} /> - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? - <Input - name="template_contract.summary" - inputType="multiline" - label={i18n.str`Fixed summary`} - tooltip={i18n.str`If specified, this template will create order with the same summary`} - /> - : undefined} - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? - <InputCurrency - name="template_contract.amount" - label={i18n.str`Fixed price`} - tooltip={i18n.str`If specified, this template will create order with the same price`} - /> - : undefined} - <InputNumber - name="template_contract.minimum_age" + <InputToggle<Entity> + name="summary_editable" + label={i18n.str`Summary is editable`} + tooltip={i18n.str`Allow the user to change the summary.`} + /> + + <InputCurrency<Entity> + name="amount" + label={i18n.str`Amount`} + tooltip={i18n.str`If specified, this template will create order with the same price`} + /> + <InputToggle<Entity> + name="amount_editable" + label={i18n.str`Amount is editable`} + tooltip={i18n.str`Allow the user to select the amount to pay.`} + /> + {cList.length > 1 && ( + <Fragment> + <InputToggle<Entity> + name="currency_editable" + readonly={!state.amount_editable} + label={i18n.str`Currency is editable`} + tooltip={i18n.str`Allow the user to change currency.`} + /> + <TextField name="sc" label={i18n.str`Supported currencies`}> + <i18n.Translate>supported currencies: {cList.join(", ")}</i18n.Translate> + </TextField> + </Fragment> + )} + <InputNumber<Entity> + name="minimum_age" label={i18n.str`Minimum age`} help="" tooltip={i18n.str`Is this contract restricted to some age?`} /> - <InputDuration - name="template_contract.pay_duration" + <InputDuration<Entity> + name="pay_duration" label={i18n.str`Payment timeout`} help="" tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} /> - <Input<Entity> - name="otp_id" - label={i18n.str`OTP device`} - readonly - tooltip={i18n.str`Use to verify transaction in offline mode.`} - /> - <InputSearchOnList - label={i18n.str`Search device`} - onChange={(p) => setState((v) => ({ ...v, otp_id: p?.id }))} - list={deviceList.map(e => ({ - description: e.device_description, - id: e.otp_device_id - }))} - /> - + {!deviceList.length ? ( + <TextField + name="otpId" + label={i18n.str`OTP device`} + tooltip={i18n.str`Use to verify transaction while offline.`} + > + <i18n.Translate>No OTP device.</i18n.Translate> + <a href="/otp-devices/new"> + <i18n.Translate>Add one first</i18n.Translate> + </a> + </TextField> + ) : ( + <InputSelector<Entity> + name="otpId" + label={i18n.str`OTP device`} + values={[ + undefined, + ...deviceList.map((e) => e.otp_device_id), + ]} + toStr={(v?: string) => { + if (!v) { + return i18n.str`No device`; + } + return deviceMap[v]; + }} + tooltip={i18n.str`Use to verify transaction in offline mode.`} + /> + )} </FormProvider> <div class="buttons is-right mt-5"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx index a29ee53b6..499c7c859 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,23 +19,24 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTemplateAPI } from "../../../../hooks/templates.js"; +import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -export type Entity = MerchantBackend.Transfers.TransferInformation; +export type Entity = TalerMerchantApi.TransferInformation; interface Props { onBack?: () => void; onConfirm: () => void; } export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { - const { createTemplate } = useTemplateAPI(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); @@ -44,8 +45,8 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: MerchantBackend.Template.TemplateAddDetails) => { - return createTemplate(request) + onCreate={(request: TalerMerchantApi.TemplateAddDetails) => { + return lib.instance.addTemplate(state.token, request) .then(() => onConfirm()) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx index 702e9ba4a..707324d40 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/List.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx index bf6062c34..66d8a2f7e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/ListPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,20 +19,19 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { CardTable } from "./Table.js"; export interface Props { - templates: MerchantBackend.Template.TemplateEntry[]; + templates: TalerMerchantApi.TemplateEntry[]; onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; onCreate: () => void; - onDelete: (e: MerchantBackend.Template.TemplateEntry) => void; - onSelect: (e: MerchantBackend.Template.TemplateEntry) => void; - onNewOrder: (e: MerchantBackend.Template.TemplateEntry) => void; - onQR: (e: MerchantBackend.Template.TemplateEntry) => void; + onDelete: (e: TalerMerchantApi.TemplateEntry) => void; + onSelect: (e: TalerMerchantApi.TemplateEntry) => void; + onNewOrder: (e: TalerMerchantApi.TemplateEntry) => void; + onQR: (e: TalerMerchantApi.TemplateEntry) => void; } export function ListPage({ @@ -45,9 +44,7 @@ export function ListPage({ onLoadMoreBefore, onLoadMoreAfter, }: Props): VNode { - const form = { payto_uri: "" }; - const { i18n } = useTranslationContext(); return ( <CardTable templates={templates.map((o) => ({ @@ -60,9 +57,7 @@ export function ListPage({ onSelect={onSelect} onNewOrder={onNewOrder} onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} /> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx index 9fdf4ead9..082e622e3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/Table.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,12 +19,12 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../../declaration.js"; -type Entity = MerchantBackend.Template.TemplateEntry; +type Entity = TalerMerchantApi.TemplateEntry; interface Props { templates: Entity[]; @@ -34,8 +34,6 @@ interface Props { onQR: (e: Entity) => void; onCreate: () => void; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } @@ -48,8 +46,6 @@ export function CardTable({ onNewOrder, onLoadMoreAfter, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -91,8 +87,6 @@ export function CardTable({ rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} /> ) : ( <EmptyTable /> @@ -112,16 +106,9 @@ interface TableProps { onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - function Table({ instances, onLoadMoreAfter, @@ -130,19 +117,17 @@ function Table({ onQR, onSelect, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class="table-container"> - {hasMoreBefore && ( + {onLoadMoreBefore && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more templates before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load newer templates</i18n.Translate> + <i18n.Translate>load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -203,13 +188,13 @@ function Table({ })} </tbody> </table> - {hasMoreAfter && ( + {onLoadMoreAfter && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more templates after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load older templates</i18n.Translate> + <i18n.Translate>load next page</i18n.Translate> </button> )} </div> @@ -222,7 +207,7 @@ function EmptyTable(): VNode { <div class="content has-text-grey has-text-centered"> <p> <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> + <i class="mdi mdi-magnify mdi-48px" /> </span> </p> <p> 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 index b9767442f..9e59609c7 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,30 +19,27 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; +import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; +import { ConfirmModal } from "../../../../components/modal/index.js"; +import { useSessionContext } from "../../../../context/session.js"; import { - useInstanceTemplates, - useTemplateAPI, + useInstanceTemplates } from "../../../../hooks/templates.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; -import { HttpStatusCode, TranslatedString } from "@gnu-taler/taler-util"; -import { ConfirmModal } from "../../../../components/modal/index.js"; -import { JumpToElementById } from "../../../../components/form/JumpToElementById.js"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; onCreate: () => void; onSelect: (id: string) => void; onNewOrder: (id: string) => void; @@ -50,35 +47,35 @@ interface Props { } export default function ListTemplates({ - onUnauthorized, - onLoadError, onCreate, onQR, onSelect, onNewOrder, - onNotFound, }: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteTemplate, testTemplateExist } = useTemplateAPI(); - const result = useInstanceTemplates({ position }, (id) => setPosition(id)); + const { lib } = useSessionContext(); + const result = useInstanceTemplates(); const [deleting, setDeleting] = - useState<MerchantBackend.Template.TemplateEntry | null>(null); + useState<TalerMerchantApi.TemplateEntry | null>(null); + const { state } = useSessionContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } } return ( @@ -86,18 +83,21 @@ export default function ListTemplates({ <NotificationCard notification={notif} /> <JumpToElementById - testIfExist={testTemplateExist} + testIfExist={async (id) => { + const resp = await lib.instance.getTemplateDetails(state.token, id) + return resp.type === "ok" + }} onSelect={onSelect} description={i18n.str`jump to template with the given template ID`} - palceholder={i18n.str`template id`} + placeholder={i18n.str`template id`} /> <ListPage - templates={result.data.templates} + templates={result.body} onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined + result.isFirstPage ? undefined: result.loadFirst } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} onCreate={onCreate} onSelect={(e) => { onSelect(e.template_id); @@ -108,7 +108,7 @@ export default function ListTemplates({ onQR={(e) => { onQR(e.template_id); }} - onDelete={(e: MerchantBackend.Template.TemplateEntry) => { + onDelete={(e: TalerMerchantApi.TemplateEntry) => { setDeleting(e) } } @@ -123,7 +123,7 @@ export default function ListTemplates({ onCancel={() => setDeleting(null)} onConfirm={async (): Promise<void> => { try { - await deleteTemplate(deleting.template_id); + await lib.instance.deleteTemplate(state.token, deleting.template_id); setNotif({ message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`, type: "SUCCESS", diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx index eb853c8ff..c0059c7bc 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/Qr.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx index 5140aae3a..7322ca169 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,113 +19,101 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { stringifyPayTemplateUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + TalerMerchantApi, + stringifyPayTemplateUri +} from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { useState } from "preact/hooks"; import { QR } from "../../../../components/exception/QR.js"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { useInstanceContext } from "../../../../context/instance.js"; -import { MerchantBackend } from "../../../../declaration.js"; - -type Entity = MerchantBackend.Template.UsingTemplateDetails; +import { useSessionContext } from "../../../../context/session.js"; + +// type Entity = TalerMerchantApi.UsingTemplateDetails; interface Props { - contract: MerchantBackend.Template.TemplateContractDetails; + contract: TalerMerchantApi.TemplateContractDetails; id: string; onBack?: () => void; } -export function QrPage({ contract, id: templateId, onBack }: Props): VNode { +export function QrPage({ id: templateId, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() - const { id: instanceId } = useInstanceContext(); - const config = useConfigContext(); + const { state } = useSessionContext(); - const [state, setState] = useState<Partial<Entity>>({ - amount: contract.amount, - summary: contract.summary, - }); + // const [state, setState] = useState<Partial<Entity>>({ + // amount: contract.amount, + // summary: contract.summary, + // }); - const errors: FormErrors<Entity> = {}; + // const errors: FormErrors<Entity> = {}; - const fixedAmount = !!contract.amount; - const fixedSummary = !!contract.summary; + // const fixedAmount = !!contract.amount; + // const fixedSummary = !!contract.summary; - const templateParams: Record<string, string> = {} - if (!fixedAmount) { - if (state.amount) { - templateParams.amount = state.amount - } else { - templateParams.amount = config.currency - } - } + // const templateParams: Record<string, string> = {}; + // if (!fixedAmount) { + // if (state.amount) { + // templateParams.amount = state.amount; + // } else { + // templateParams.amount = config.currency; + // } + // } - if (!fixedSummary) { - templateParams.summary = state.summary ?? "" - } + // if (!fixedSummary) { + // templateParams.summary = state.summary ?? ""; + // } - const merchantBaseUrl = new URL(backendURL).href; + const merchantBaseUrl = state.backendUrl.href; const payTemplateUri = stringifyPayTemplateUri({ merchantBaseUrl, templateId, - templateParams - }) - - const issuer = encodeURIComponent( - `${new URL(backendURL).host}/${instanceId}`, - ); + templateParams: {}, + }); return ( <div> + <section id="printThis"> + <QR text={payTemplateUri} /> + <pre style={{ textAlign: "center" }}> + <a href={payTemplateUri}>{payTemplateUri}</a> + </pre> + </section> + <section class="section is-main-section"> <div class="columns"> <div class="column" /> <div class="column is-four-fifths"> - <p class="is-size-5 mt-5 mb-5"> + {/* <p class="is-size-5 mt-5 mb-5"> <i18n.Translate> Here you can specify a default value for fields that are not fixed. Default values can be edited by the customer before the payment. </i18n.Translate> - </p> + </p> */} <p></p> - <FormProvider + {/* <FormProvider object={state} valueHandler={setState} errors={errors} > <InputCurrency<Entity> name="amount" - label={ - fixedAmount - ? i18n.str`Fixed amount` - : i18n.str`Default amount` - } - readonly={fixedAmount} + label={i18n.str`Amount`} + readonly tooltip={i18n.str`Amount of the order`} /> <Input<Entity> name="summary" inputType="multiline" - readonly={fixedSummary} - label={ - fixedSummary - ? i18n.str`Fixed summary` - : i18n.str`Default summary` - } + readonly + label={i18n.str`Summary`} tooltip={i18n.str`Title of the order to be shown to the customer`} /> - </FormProvider> + </FormProvider> */} <div class="buttons is-right mt-5"> {onBack && ( @@ -144,12 +132,6 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode { <div class="column" /> </div> </section> - <section id="printThis"> - <QR text={payTemplateUri} /> - <pre style={{ textAlign: "center" }}> - <a href={payTemplateUri}>{payTemplateUri}</a> - </pre> - </section> </div> ); } @@ -167,6 +149,6 @@ function saveAsPDF(name: string): void { printWindow.document.body.appendChild(divContents.cloneNode(true)); printWindow.addEventListener("load", () => { printWindow.print(); - printWindow.close(); + // printWindow.close(); }); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx index 7db7478f7..ed809c7b3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,62 +19,48 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { - ErrorType, - HttpError, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; -import { useState } from "preact/hooks"; +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { - useTemplateAPI, - useTemplateDetails, + useTemplateDetails } from "../../../../hooks/templates.js"; -import { Notification } from "../../../../utils/types.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { QrPage } from "./QrPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { LoginPage } from "../../../login/index.js"; -export type Entity = MerchantBackend.Transfers.TransferInformation; +export type Entity = TalerMerchantApi.TransferInformation; interface Props { onBack?: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; tid: string; } export default function TemplateQrPage({ tid, onBack, - onLoadError, - onNotFound, - onUnauthorized, }: Props): VNode { const result = useTemplateDetails(tid); - const [notif, setNotif] = useState<Notification | undefined>(undefined); - - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } + } + return ( - <> - <NotificationCard notification={notif} /> - <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} /> - </> + <QrPage contract={result.body.template_contract} id={tid} onBack={onBack} /> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx index 8d07cb31f..303d17b72 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/Update.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx index b578d4664..eedb77f28 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -20,10 +20,16 @@ */ import { + AmountString, Amounts, - MerchantTemplateContractDetails, + Duration, + TalerError, + TalerMerchantApi, + TranslatedString, } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; @@ -35,102 +41,133 @@ import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { InputWithAddon } from "../../../../components/form/InputWithAddon.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { InputTab } from "../../../../components/form/InputTab.js"; - -enum Steps { - BOTH_FIXED, - FIXED_PRICE, - FIXED_SUMMARY, - NON_FIXED, -} +import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; +import { TextField } from "../../../../components/form/TextField.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useInstanceOtpDevices } from "../../../../hooks/otp.js"; -type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; +type Entity = { + description?: string; + otpId?: string | null; + summary?: string; + amount?: AmountString; + minimum_age?: number; + pay_duration?: Duration; + summary_editable?: boolean; + amount_editable?: boolean; + currency_editable?: boolean; +}; interface Props { - onUpdate: (d: Entity) => Promise<void>; + onUpdate: (d: TalerMerchantApi.TemplatePatchDetails) => Promise<void>; onBack?: () => void; - template: Entity; + template: TalerMerchantApi.TemplateDetails & WithId; } export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { url: backendURL } = useBackendContext() + const { config } = useSessionContext(); + const {state:session} = useSessionContext(); + + const [state, setState] = useState<Partial<Entity>>({ + description: template.template_description, + minimum_age: template.template_contract.minimum_age, + otpId: template.otp_id, + pay_duration: template.template_contract.pay_duration + ? Duration.fromTalerProtocolDuration( + template.template_contract.pay_duration, + ) + : undefined, + summary: + template.editable_defaults?.summary ?? template.template_contract.summary, + amount: + template.editable_defaults?.amount ?? + (template.template_contract.amount as AmountString | undefined), + currency_editable: !!template.editable_defaults?.currency, + summary_editable: !!template.editable_defaults?.summary, + amount_editable: !!template.editable_defaults?.amount, + }); - const intialStep = - template.template_contract?.amount === undefined && template.template_contract?.summary === undefined - ? Steps.NON_FIXED - : template.template_contract?.summary === undefined - ? Steps.FIXED_PRICE - : template.template_contract?.amount === undefined - ? Steps.FIXED_SUMMARY - : Steps.BOTH_FIXED; + function updateState(up: (s: Partial<Entity>) => Partial<Entity>) { + setState((old) => { + const newState = up(old); + if (!newState.amount_editable) { + newState.currency_editable = false; + } + return newState; + }); + } - const [state, setState] = useState<Partial<Entity & { type: Steps }>>({ ...template, type: intialStep }); + const devices = useInstanceOtpDevices(); + const deviceList = + !devices || devices instanceof TalerError || devices.type === "fail" + ? [] + : devices.body.otp_devices; + const deviceMap = deviceList.reduce( + (prev, cur) => { + prev[cur.otp_device_id] = cur.device_description as TranslatedString; + return prev; + }, + {} as Record<string, TranslatedString>, + ); - const parsedPrice = !state.template_contract?.amount - ? undefined - : Amounts.parse(state.template_contract?.amount); + const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount); const errors: FormErrors<Entity> = { - template_description: !state.template_description - ? i18n.str`should not be empty` - : undefined, - template_contract: !state.template_contract + description: !state.description ? i18n.str`should not be empty` : undefined, + amount: !state.amount ? undefined - : undefinedIfEmpty({ - amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.amount - ? i18n.str`required` - : !parsedPrice - ? i18n.str`not valid` - : Amounts.isZero(parsedPrice) - ? i18n.str`must be greater than 0` - : undefined, - summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED) - ? undefined - : !state.template_contract?.summary - ? i18n.str`required` - : undefined, - minimum_age: - state.template_contract.minimum_age < 0 - ? i18n.str`should be greater that 0` - : undefined, - pay_duration: !state.template_contract.pay_duration - ? i18n.str`can't be empty` - : state.template_contract.pay_duration.d_us === "forever" - ? undefined - : state.template_contract.pay_duration.d_us < 1000 * 1000 // less than one second - ? i18n.str`to short` - : undefined, - } as Partial<MerchantTemplateContractDetails>), + : !parsedPrice + ? i18n.str`not valid` + : Amounts.isZero(parsedPrice) + ? i18n.str`must be greater than 0` + : undefined, + minimum_age: + state.minimum_age && state.minimum_age < 0 + ? i18n.str`should be greater that 0` + : undefined, + pay_duration: !state.pay_duration + ? i18n.str`can't be empty` + : state.pay_duration.d_ms === "forever" + ? undefined + : state.pay_duration.d_ms < 1000 // less than one second + ? i18n.str`to short` + : undefined, }; + const cList = Object.values(config.currencies).map((d) => d.name); + const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); const submitForm = () => { if (hasErrors) return Promise.reject(); - if (state.template_contract) { - if (state.type === Steps.NON_FIXED) { - delete state.template_contract.amount; - delete state.template_contract.summary; - } else if (state.type === Steps.FIXED_SUMMARY) { - delete state.template_contract.amount; - } else if (state.type === Steps.FIXED_PRICE) { - delete state.template_contract.summary; - } - } - delete state.type - return onUpdate(state as any); + return onUpdate({ + template_description: state.description!, + template_contract: { + minimum_age: state.minimum_age!, + pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!), + amount: state.amount_editable ? undefined : state.amount, + summary: state.summary_editable ? undefined : state.summary, + currency: + cList.length > 1 && state.currency_editable + ? undefined + : config.currency, + }, + editable_defaults: { + amount: !state.amount_editable ? undefined : state.amount, + summary: !state.summary_editable ? undefined : state.summary, + currency: + cList.length === 1 || !state.currency_editable + ? undefined + : config.currency, + }, + otp_id: state.otpId!, + }); }; - return ( <div> <section class="section"> @@ -140,7 +177,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - {backendURL}/templates/{template.id} + {new URL(`templates/${template.id}`, session.backendUrl.href).href} </span> </div> </div> @@ -154,77 +191,91 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode { <div class="column is-four-fifths"> <FormProvider object={state} - valueHandler={setState} + valueHandler={updateState} errors={errors} > - <InputWithAddon<Entity> - name="id" - addonBefore={`templates/`} - readonly - label={i18n.str`Identifier`} - tooltip={i18n.str`Name of the template in URLs.`} - /> - <Input<Entity> - name="template_description" + name="description" label={i18n.str`Description`} help="" tooltip={i18n.str`Describe what this template stands for`} /> - <InputTab - name="type" - label={i18n.str`Type`} - help={(() => { - switch (state.type) { - case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.` - case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.` - case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.` - case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.` - } - })()} - tooltip={i18n.str`Define what the user be allowed to modify`} - values={[ - Steps.NON_FIXED, - Steps.FIXED_PRICE, - Steps.FIXED_SUMMARY, - Steps.BOTH_FIXED, - ]} - toStr={(v: Steps): string => { - switch (v) { - case Steps.NON_FIXED: return i18n.str`Simple` - case Steps.FIXED_PRICE: return i18n.str`With price` - case Steps.FIXED_SUMMARY: return i18n.str`With summary` - case Steps.BOTH_FIXED: return i18n.str`With price and summary` - } - }} + <Input<Entity> + name="summary" + inputType="multiline" + label={i18n.str`Summary`} + tooltip={i18n.str`If specified, this template will create order with the same summary`} /> - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ? - <Input - name="template_contract.summary" - inputType="multiline" - label={i18n.str`Fixed summary`} - tooltip={i18n.str`If specified, this template will create order with the same summary`} - /> - : undefined} - {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ? - <InputCurrency - name="template_contract.amount" - label={i18n.str`Fixed price`} - tooltip={i18n.str`If specified, this template will create order with the same price`} - /> - : undefined} - <InputNumber - name="template_contract.minimum_age" + <InputToggle<Entity> + name="summary_editable" + label={i18n.str`Summary is editable`} + tooltip={i18n.str`Allow the user to change the summary.`} + /> + <InputCurrency<Entity> + name="amount" + label={i18n.str`Amount`} + tooltip={i18n.str`If specified, this template will create order with the same price`} + /> + <InputToggle<Entity> + name="amount_editable" + label={i18n.str`Amount is editable`} + tooltip={i18n.str`Allow the user to select the amount to pay.`} + /> + {cList.length > 1 && ( + <Fragment> + <InputToggle<Entity> + name="currency_editable" + readonly={!state.amount_editable} + label={i18n.str`Currency is editable`} + tooltip={i18n.str`Allow the user to change currency.`} + /> + <TextField name="sc" label={i18n.str`Supported currencies`}> + <i18n.Translate> + supported currencies: {cList.join(", ")} + </i18n.Translate> + </TextField> + </Fragment> + )} + <InputNumber<Entity> + name="minimum_age" label={i18n.str`Minimum age`} help="" tooltip={i18n.str`Is this contract restricted to some age?`} /> - <InputDuration - name="template_contract.pay_duration" + <InputDuration<Entity> + name="pay_duration" label={i18n.str`Payment timeout`} help="" tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} /> + {!deviceList.length ? ( + <TextField + name="otpId" + label={i18n.str`OTP device`} + tooltip={i18n.str`Use to verify transaction while offline.`} + > + <i18n.Translate>No OTP device.</i18n.Translate> + <a href="/otp-devices/new"> + <i18n.Translate>Add one first</i18n.Translate> + </a> + </TextField> + ) : ( + <InputSelector<Entity> + name="otpId" + label={i18n.str`OTP device`} + values={[ + undefined, + ...deviceList.map((e) => e.otp_device_id), + ]} + toStr={(v?: string) => { + if (!v) { + return i18n.str`No device`; + } + return deviceMap[v]; + }} + tooltip={i18n.str`Use to verify transaction in offline mode.`} + /> + )} </FormProvider> <div class="buttons is-right mt-5"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx index 3adca45db..6185bd2a9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,71 +19,69 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { useSessionContext } from "../../../../context/session.js"; import { - useTemplateAPI, useTemplateDetails, } from "../../../../hooks/templates.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -export type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId; +export type Entity = TalerMerchantApi.TemplatePatchDetails & WithId; interface Props { onBack?: () => void; onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; tid: string; } export default function UpdateTemplate({ tid, onConfirm, onBack, - onUnauthorized, - onNotFound, - onLoadError, }: Props): VNode { - const { updateTemplate } = useTemplateAPI(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const result = useTemplateDetails(tid); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } } return ( <Fragment> <NotificationCard notification={notif} /> <UpdatePage - template={{ ...result.data, id: tid }} + template={{...result.body, id: tid}} onBack={onBack} onUpdate={(data) => { - return updateTemplate(tid, data) + return lib.instance.updateTemplate(state.token, tid, data) .then(onConfirm) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx index 13576d94d..d91888b97 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/Use.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 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 index 983804d3e..360c9d373 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; @@ -29,13 +30,12 @@ import { } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { MerchantBackend } from "../../../../declaration.js"; -type Entity = MerchantBackend.Template.UsingTemplateDetails; +type Entity = TalerMerchantApi.TemplateContractDetails; interface Props { id: string; - template: MerchantBackend.Template.TemplateDetails; + template: TalerMerchantApi.TemplateDetails; onCreateOrder: (d: Entity) => Promise<void>; onBack?: () => void; } @@ -44,17 +44,18 @@ export function UsePage({ id, template, onCreateOrder, onBack }: Props): VNode { const { i18n } = useTranslationContext(); const [state, setState] = useState<Partial<Entity>>({ - amount: template.template_contract.amount, - summary: template.template_contract.summary, + currency: template.editable_defaults?.currency ?? template.template_contract.currency, + amount: template.editable_defaults?.amount ?? template.template_contract.amount, + summary: template.editable_defaults?.summary ?? template.template_contract.summary, }); const errors: FormErrors<Entity> = { amount: - !template.template_contract.amount && !state.amount + !state.amount ? i18n.str`Amount is required` : undefined, summary: - !template.template_contract.summary && !state.summary + !state.summary ? i18n.str`Order summary is required` : undefined, }; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx index ed1242ef5..00cb2b827 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,31 +19,28 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { - useTemplateAPI, - useTemplateDetails, + useTemplateDetails } from "../../../../hooks/templates.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UsePage } from "./UsePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; +import { useSessionContext } from "../../../../context/session.js"; -export type Entity = MerchantBackend.Transfers.TransferInformation; +export type Entity = TalerMerchantApi.TransferInformation; interface Props { onBack?: () => void; onOrderCreated: (id: string) => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; tid: string; } @@ -51,42 +48,52 @@ export default function TemplateUsePage({ tid, onOrderCreated, onBack, - onLoadError, - onNotFound, - onUnauthorized, }: Props): VNode { - const { createOrderFromTemplate } = useTemplateAPI(); + const { lib } = useSessionContext(); const result = useTemplateDetails(tid); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate /> + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result) + } + } } return ( <> <NotificationCard notification={notif} /> <UsePage - template={result.data} + template={result.body} id={tid} onBack={onBack} onCreateOrder={( - request: MerchantBackend.Template.UsingTemplateDetails, + request: TalerMerchantApi.UsingTemplateDetails, ) => { - return createOrderFromTemplate(tid, request) - .then((res) => onOrderCreated(res.data.order_id)) + + return lib.instance.useTemplateCreateOrder(tid, request) + .then((res) => { + if (res.type === "ok") { + onOrderCreated(res.body.order_id) + } else { + setNotif({ + message: i18n.str`could not create order from template`, + type: "ERROR", + }); + } + }) .catch((error) => { setNotif({ message: i18n.str`could not create order from template`, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx index d22a9e4d4..f75ee89b8 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -25,19 +25,23 @@ import { useState } from "preact/hooks"; import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { FormProvider } from "../../../components/form/FormProvider.js"; import { Input } from "../../../components/form/Input.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { AccessToken } from "../../../declaration.js"; import { NotificationCard } from "../../../components/menu/index.js"; +import { useSessionContext } from "../../../context/session.js"; +import { AccessToken, createAccessToken } from "@gnu-taler/taler-util"; interface Props { - instanceId: string; hasToken: boolean | undefined; onClearToken: (c: AccessToken | undefined) => void; onNewToken: (c: AccessToken | undefined, s: AccessToken) => void; onBack?: () => void; } -export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearToken }: Props): VNode { +export function DetailPage({ + hasToken, + onBack, + onNewToken, + onClearToken, +}: Props): VNode { type State = { old_token: string; new_token: string; repeat_token: string }; const [form, setValue] = useState<Partial<State>>({ old_token: "", @@ -47,9 +51,10 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo const { i18n } = useTranslationContext(); const errors = { - old_token: hasToken && !form.old_token - ? i18n.str`you need your access token to perform the operation` - : undefined, + old_token: + hasToken && !form.old_token + ? i18n.str`you need your access token to perform the operation` + : undefined, new_token: !form.new_token ? i18n.str`cannot be empty` : form.new_token === form.old_token @@ -62,18 +67,21 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo }; const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, + (k) => (errors as Record<string, unknown>)[k] !== undefined, ); - const instance = useInstanceContext(); + const { state } = useSessionContext(); - const text = i18n.str`You are updating the access token from instance with id "${instance.id}"`; + const text = i18n.str`You are updating the access token from instance with id "${state.instance}"`; async function submitForm() { if (hasErrors) return; - const ot = hasToken ? `secret-token:${form.old_token}` as AccessToken : undefined; - const nt = `secret-token:${form.new_token}` as AccessToken; - onNewToken(ot, nt) + const oldToken = + form.old_token !== undefined && hasToken + ? createAccessToken(form.old_token) + : undefined; + const newToken = createAccessToken(form.new_token!); + onNewToken(oldToken, newToken); } return ( @@ -84,17 +92,15 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo <div class="level"> <div class="level-left"> <div class="level-item"> - <span class="is-size-4"> - {text} - </span> + <span class="is-size-4">{text}</span> </div> </div> </div> </div> </section> <hr /> - - {!hasToken && + + {!hasToken && ( <NotificationCard notification={{ message: i18n.str`This instance doesn't have authentication token.`, @@ -102,7 +108,7 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo type: "WARN", }} /> - } + )} <div class="columns"> <div class="column" /> @@ -119,7 +125,8 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo /> <p> <i18n.Translate> - Clearing the access token will mean public access to the instance. + Clearing the access token will mean public access to the + instance. </i18n.Translate> </p> <div class="buttons is-right mt-5"> @@ -127,10 +134,9 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo class="button" onClick={() => { if (hasToken) { - const ot = `secret-token:${form.old_token}` as AccessToken; - onClearToken(ot) + onClearToken(form.old_token ? createAccessToken(form.old_token) : undefined); } else { - onClearToken(undefined) + onClearToken(undefined); } }} > @@ -140,7 +146,6 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo </Fragment> )} - <Input<State> name="new_token" label={i18n.str`New access token`} @@ -154,29 +159,29 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo inputType="password" /> </Fragment> + <div class="buttons is-right mt-5"> + {onBack && ( + <a class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </a> + )} + <AsyncButton + type="submit" + disabled={hasErrors} + data-tooltip={ + hasErrors + ? i18n.str`Need to complete marked fields` + : "confirm operation" + } + onClick={submitForm} + > + <i18n.Translate>Confirm change</i18n.Translate> + </AsyncButton> + </div> </FormProvider> - <div class="buttons is-right mt-5"> - {onBack && ( - <button class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - <AsyncButton - disabled={hasErrors} - data-tooltip={ - hasErrors - ? i18n.str`Need to complete marked fields` - : "confirm operation" - } - onClick={submitForm} - > - <i18n.Translate>Confirm change</i18n.Translate> - </AsyncButton> - </div> </div> <div class="column" /> </div> - </section> </div> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx index 22365c9e1..c23e5be17 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,72 +13,84 @@ 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 { HttpStatusCode } from "@gnu-taler/taler-util"; -import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; -import { Loading } from "../../../components/exception/loading.js"; -import { AccessToken, MerchantBackend } from "../../../declaration.js"; -import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js"; -import { DetailPage } from "./DetailPage.js"; -import { useInstanceContext } from "../../../context/instance.js"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../components/exception/loading.js"; import { NotificationCard } from "../../../components/menu/index.js"; +import { useSessionContext } from "../../../context/session.js"; +import { useInstanceDetails } from "../../../hooks/instance.js"; import { Notification } from "../../../utils/types.js"; -import { useBackendContext } from "../../../context/backend.js"; +import { LoginPage } from "../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; +import { DetailPage } from "./DetailPage.js"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; onChange: () => void; - onNotFound: () => VNode; onCancel: () => void; } -export default function Token({ - onLoadError, - onChange, - onUnauthorized, - onNotFound, - onCancel, -}: Props): VNode { +export default function Token({ onChange, onCancel }: Props): VNode { const { i18n } = useTranslationContext(); - + const { lib } = useSessionContext(); + const { logIn } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { clearAccessToken, setNewAccessToken } = useInstanceAPI(); - const { id } = useInstanceContext(); - const result = useInstanceDetails() + const result = useInstanceDetails(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage />; + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(result); + } + } } - const hasToken = result.data.auth.method === "token" + const hasToken = result.body.auth.method === "token"; return ( <Fragment> <NotificationCard notification={notif} /> <DetailPage - instanceId={id} onBack={onCancel} hasToken={hasToken} onClearToken={async (currentToken): Promise<void> => { try { - await clearAccessToken(currentToken); - onChange(); + const resp = await lib.instance.updateCurrentInstanceAuthentication( + currentToken, + { + method: "external", + }, + ); + if (resp.type === "ok") { + onChange(); + } else { + return setNotif({ + message: i18n.str`Failed to clear token`, + type: "ERROR", + description: resp.detail.hint, + }); + } } catch (error) { if (error instanceof Error) { - setNotif({ + return setNotif({ message: i18n.str`Failed to clear token`, type: "ERROR", description: error.message, @@ -88,11 +100,45 @@ export default function Token({ }} onNewToken={async (currentToken, newToken): Promise<void> => { try { - await setNewAccessToken(currentToken, newToken); - onChange(); + { + const resp = + await lib.instance.updateCurrentInstanceAuthentication( + currentToken, + { + token: newToken, + method: "token", + }, + ); + if (resp.type === "fail") { + return setNotif({ + message: i18n.str`Failed to set new token`, + type: "ERROR", + description: resp.detail.hint, + }); + } + } + const resp = await lib.authenticate.createAccessTokenBearer( + newToken, + { + scope: "write", + duration: { + d_us: "forever", + }, + refreshable: true, + }, + ); + if (resp.type === "ok") { + logIn(resp.body.token); + return onChange(); + } else { + return setNotif({ + message: i18n.str`Failed to set new token`, + type: "ERROR", + }); + } } catch (error) { if (error instanceof Error) { - setNotif({ + return setNotif({ message: i18n.str`Failed to set new token`, type: "ERROR", description: error.message, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx index 5f0f56f2d..581828657 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx index 64b67335c..ca38defc3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 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 index 13f5f3c12..91aabe58e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,8 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../../components/exception/AsyncButton.js"; import { @@ -30,14 +31,12 @@ import { import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { useConfigContext } from "../../../../context/config.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { CROCKFORD_BASE32_REGEX, URL_REGEX, } from "../../../../utils/constants.js"; -type Entity = MerchantBackend.Transfers.TransferInformation; +type Entity = TalerMerchantApi.TransferInformation; interface Props { onCreate: (d: Entity) => Promise<void>; @@ -47,13 +46,12 @@ interface Props { export function CreatePage({ accounts, onCreate, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const { currency } = useConfigContext(); const [state, setState] = useState<Partial<Entity>>({ wtid: "", // payto_uri: , // exchange_url: 'http://exchange.taler:8081/', - credit_amount: ``, + credit_amount: `` as AmountString, }); const errors: FormErrors<Entity> = { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx index 25551a031..428476337 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,31 +19,34 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { TalerError, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceDetails } from "../../../../hooks/instance.js"; -import { useTransferAPI } from "../../../../hooks/transfer.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -import { useBankAccountDetails, useInstanceBankAccounts } from "../../../../hooks/bank.js"; -export type Entity = MerchantBackend.Transfers.TransferInformation; +export type Entity = TalerMerchantApi.TransferInformation; interface Props { onBack?: () => void; onConfirm: () => void; } export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { - const { informTransfer } = useTransferAPI(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); const instance = useInstanceBankAccounts(); - const accounts = !instance.ok - ? [] - : instance.data.accounts.map((a) => a.payto_uri); + const accounts = + !instance || instance instanceof TalerError || instance.type === "fail" + ? [] + : instance.body.accounts.map((a) => a.payto_uri); return ( <> @@ -51,8 +54,9 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { <CreatePage onBack={onBack} accounts={accounts} - onCreate={(request: MerchantBackend.Transfers.TransferInformation) => { - return informTransfer(request) + onCreate={(request: TalerMerchantApi.TransferInformation) => { + return lib.instance + .informWireTransfer(state.token, request) .then(() => onConfirm()) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx index 92b3f9853..def03fe27 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/List.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,7 +19,8 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; +import { AmountString, PaytoString } from "@gnu-taler/taler-util"; +import { FunctionalComponent, h } from "preact"; import { ListPage as TestedComponent } from "./ListPage.js"; export default { @@ -50,8 +51,8 @@ export const Example = createExample(TestedComponent, { transfers: [ { exchange_url: "http://exchange.url/", - credit_amount: "TESTKUDOS:10", - payto_uri: "payto//x-taler-bank/bank:8080/account", + credit_amount: "TESTKUDOS:10" as AmountString, + payto_uri: "payto//x-taler-bank/bank:8080/account" as PaytoString, transfer_serial_id: 123123123, wtid: "!@KJELQKWEJ!L@K#!J@", confirmed: true, @@ -62,8 +63,8 @@ export const Example = createExample(TestedComponent, { }, { exchange_url: "http://exchange.url/", - credit_amount: "TESTKUDOS:10", - payto_uri: "payto//x-taler-bank/bank:8080/account", + credit_amount: "TESTKUDOS:10" as AmountString, + payto_uri: "payto//x-taler-bank/bank:8080/account" as PaytoString, transfer_serial_id: 123123123, wtid: "!@KJELQKWEJ!L@K#!J@", confirmed: true, @@ -74,8 +75,8 @@ export const Example = createExample(TestedComponent, { }, { exchange_url: "http://exchange.url/", - credit_amount: "TESTKUDOS:10", - payto_uri: "payto//x-taler-bank/bank:8080/account", + credit_amount: "TESTKUDOS:10" as AmountString, + payto_uri: "payto//x-taler-bank/bank:8080/account" as PaytoString, transfer_serial_id: 123123123, wtid: "!@KJELQKWEJ!L@K#!J@", confirmed: true, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx index 02b12c4c2..22ad0b8d8 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -23,11 +23,11 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { FormProvider } from "../../../../components/form/FormProvider.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { CardTable } from "./Table.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; export interface Props { - transfers: MerchantBackend.Transfers.TransferDetails[]; + transfers: TalerMerchantApi.TransferDetails[]; onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; onShowAll: () => void; @@ -75,6 +75,11 @@ export function ListPage({ name="payto_uri" label={i18n.str`Account URI`} values={accounts} + fromStr={(d) => { + const idx = accounts.indexOf(d) + if (idx === -1) return undefined; + return d + }} placeholder={i18n.str`Select one account`} tooltip={i18n.str`filter by account address`} /> @@ -125,9 +130,7 @@ export function ListPage({ onCreate={onCreate} onDelete={onDelete} onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} /> </section> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx index b6b1cf328..b9235c669 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,14 +19,14 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; +import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js"; -type Entity = MerchantBackend.Transfers.TransferDetails & WithId; +type Entity = TalerMerchantApi.TransferDetails & WithId; interface Props { transfers: Entity[]; @@ -34,8 +34,6 @@ interface Props { onCreate: () => void; accounts: string[]; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } @@ -45,8 +43,6 @@ export function CardTable({ onDelete, onLoadMoreAfter, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -85,8 +81,6 @@ export function CardTable({ rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} /> ) : ( <EmptyTable /> @@ -103,35 +97,26 @@ interface TableProps { onDelete: (id: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - function Table({ instances, onLoadMoreAfter, onDelete, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); - const [settings] = useSettings(); + const [settings] = usePreference(); return ( <div class="table-container"> - {hasMoreBefore && ( + {onLoadMoreBefore && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more transfers before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load newer transfers</i18n.Translate> + <i18n.Translate>load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -197,13 +182,13 @@ function Table({ })} </tbody> </table> - {hasMoreAfter && ( + {onLoadMoreAfter && ( <button class="button is-fullwidth" - data-tooltip={i18n.str`load more transfer after the last one`} + data-tooltip={i18n.str`load more transfers after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load older transfers</i18n.Translate> + <i18n.Translate>load next page</i18n.Translate> </button> )} </div> @@ -216,7 +201,7 @@ function EmptyTable(): VNode { <div class="content has-text-grey has-text-centered"> <p> <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> + <i class="mdi mdi-magnify mdi-48px" /> </span> </p> <p> 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 index 0fdbb9bc3..8b4d1f3cb 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,42 +19,36 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { ErrorType, HttpError } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { HttpStatusCode, TalerError, assertUnreachable } from "@gnu-taler/taler-util"; +import { VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useInstanceDetails } from "../../../../hooks/instance.js"; +import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; import { useInstanceTransfers } from "../../../../hooks/transfer.js"; +import { LoginPage } from "../../../login/index.js"; import { ListPage } from "./ListPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; onCreate: () => void; } interface Form { - verified?: "yes" | "no"; + verified?: boolean; payto_uri?: string; } export default function ListTransfer({ - onUnauthorized, - onLoadError, onCreate, - onNotFound, }: Props): VNode { - const setFilter = (s?: "yes" | "no") => setForm({ ...form, verified: s }); + const setFilter = (s?: boolean) => setForm({ ...form, verified: s }); const [position, setPosition] = useState<string | undefined>(undefined); const instance = useInstanceBankAccounts(); - const accounts = !instance.ok + const accounts = !instance || (instance instanceof TalerError) || instance.type === "fail" ? [] - : instance.data.accounts.map((a) => a.payto_uri); + : instance.body.accounts.map((a) => a.payto_uri); const [form, setForm] = useState<Form>({ payto_uri: "" }); const shoulUseDefaultAccount = accounts.length === 1 @@ -64,8 +58,8 @@ export default function ListTransfer({ } }, [shoulUseDefaultAccount]) - const isVerifiedTransfers = form.verified === "yes"; - const isNonVerifiedTransfers = form.verified === "no"; + const isVerifiedTransfers = form.verified === true; + const isNonVerifiedTransfers = form.verified === false; const isAllTransfers = form.verified === undefined; const result = useInstanceTransfers( @@ -77,37 +71,37 @@ export default function ListTransfer({ (id) => setPosition(id), ); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(result); + } + } } return ( <ListPage accounts={accounts} - transfers={result.data.transfers} - onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined - } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + transfers={result.body} + onLoadMoreBefore={result.isFirstPage ? undefined: result.loadFirst } + onLoadMoreAfter={result.isLastPage ? undefined : result.loadNext} onCreate={onCreate} onDelete={() => { null; }} - // position={position} setPosition={setPosition} onShowAll={() => setFilter(undefined)} - onShowUnverified={() => setFilter("no")} - onShowVerified={() => setFilter("yes")} + onShowUnverified={() => setFilter(false)} + onShowVerified={() => setFilter(true)} isAllTransfers={isAllTransfers} isVerifiedTransfers={isVerifiedTransfers} isNonVerifiedTransfers={isNonVerifiedTransfers} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx index 84cc95e72..719f99209 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/update/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx index 817a7025c..5bd12e4e9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode, FunctionalComponent } from "preact"; +import { FunctionalComponent, h } from "preact"; import { UpdatePage as TestedComponent } from "./UpdatePage.js"; export default { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx index 5b88b550f..cde58967f 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,8 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { Duration, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { h, VNode } from "preact"; +import { VNode, h } from "preact"; import { useState } from "preact/hooks"; import { AsyncButton } from "../../../components/exception/AsyncButton.js"; import { @@ -28,40 +29,31 @@ import { FormProvider, } from "../../../components/form/FormProvider.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { MerchantBackend } from "../../../declaration.js"; +import { useSessionContext } from "../../../context/session.js"; import { undefinedIfEmpty } from "../../../utils/table.js"; -type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & { - auth_token?: string; +export type Entity = Omit<Omit<TalerMerchantApi.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & { + default_pay_delay: Duration, + default_wire_transfer_delay: Duration, }; -//MerchantBackend.Instances.InstanceAuthConfigurationMessage +//TalerMerchantApi.InstanceAuthConfigurationMessage interface Props { - onUpdate: (d: Entity) => void; - selected: MerchantBackend.Instances.QueryInstancesResponse; + onUpdate: (d: TalerMerchantApi.InstanceReconfigurationMessage) => void; + selected: TalerMerchantApi.QueryInstancesResponse; isLoading: boolean; onBack: () => void; } function convert( - from: MerchantBackend.Instances.QueryInstancesResponse, + from: TalerMerchantApi.QueryInstancesResponse, ): Entity { - const { ...rest } = from; - // const accounts = qAccounts - // .filter((a) => a.active) - // .map( - // (a) => - // ({ - // payto_uri: a.payto_uri, - // credit_facade_url: a.credit_facade_url, - // credit_facade_credentials: a.credit_facade_credentials, - // } as MerchantBackend.Instances.MerchantBankAccount), - // ); + const { default_pay_delay, default_wire_transfer_delay, ...rest } = from; + const defaults = { use_stefan: false, - default_pay_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 }, //two hours - default_wire_transfer_delay: { d_us: 2 * 1000 * 1000 * 60 * 60 * 2 }, //two hours + default_pay_delay: Duration.fromTalerProtocolDuration(default_pay_delay), + default_wire_transfer_delay: Duration.fromTalerProtocolDuration(default_wire_transfer_delay), }; return { ...defaults, ...rest }; } @@ -71,21 +63,7 @@ export function UpdatePage({ selected, onBack, }: Props): VNode { - const { id } = useInstanceContext(); - // const currentTokenValue = getTokenValuePart(token); - - // function updateToken(token: string | undefined | null) { - // const value = - // token && token.startsWith("secret-token:") - // ? token.substring("secret-token:".length) - // : token; - - // if (!token) { - // onChangeAuth({ method: "external" }); - // } else { - // onChangeAuth({ method: "token", token: `secret-token:${value}` }); - // } - // } + const { state } = useSessionContext(); const [value, valueHandler] = useState<Partial<Entity>>(convert(selected)); @@ -101,9 +79,9 @@ export function UpdatePage({ default_pay_delay: !value.default_pay_delay ? i18n.str`required` : !!value.default_wire_transfer_delay && - value.default_wire_transfer_delay.d_us !== "forever" && - value.default_pay_delay.d_us !== "forever" && - value.default_pay_delay.d_us > value.default_wire_transfer_delay.d_us ? + value.default_wire_transfer_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms !== "forever" && + value.default_pay_delay.d_ms > value.default_wire_transfer_delay.d_ms ? i18n.str`pay delay can't be greater than wire transfer delay` : undefined, default_wire_transfer_delay: !value.default_wire_transfer_delay ? i18n.str`required` @@ -127,7 +105,13 @@ export function UpdatePage({ ); const submit = async (): Promise<void> => { - await onUpdate(value as Entity); + const { default_pay_delay, default_wire_transfer_delay, ...rest } = value as Required<Entity>; + const result: TalerMerchantApi.InstanceReconfigurationMessage = { + default_pay_delay: Duration.toTalerProtocolDuration(default_pay_delay), + default_wire_transfer_delay: Duration.toTalerProtocolDuration(default_wire_transfer_delay), + ...rest, + } + await onUpdate(result); }; // const [active, setActive] = useState(false); @@ -140,30 +124,10 @@ export function UpdatePage({ <div class="level-left"> <div class="level-item"> <span class="is-size-4"> - <i18n.Translate>Instance id</i18n.Translate>: <b>{id}</b> + <i18n.Translate>Instance id</i18n.Translate>: <b>{state.instance}</b> </span> </div> </div> - {/* <div class="level-right"> - <div class="level-item"> - <h1 class="title"> - <button - class="button is-danger" - data-tooltip={i18n.str`Change the authorization method use for this instance.`} - onClick={(): void => { - setActive(!active); - }} - > - <div class="icon is-left"> - <i class="mdi mdi-lock-reset" /> - </div> - <span> - <i18n.Translate>Manage access token</i18n.Translate> - </span> - </button> - </h1> - </div> - </div> */} </div> </div> </section> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx index e44cf5c0f..9da7f7efb 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -13,83 +13,79 @@ 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 { HttpStatusCode } from "@gnu-taler/taler-util"; +import { HttpStatusCode, TalerError, TalerMerchantApi, TalerMerchantInstanceHttpClient, TalerMerchantManagementResultByMethod, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - HttpResponse, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../components/exception/loading.js"; import { NotificationCard } from "../../../components/menu/index.js"; -import { useInstanceContext } from "../../../context/instance.js"; -import { AccessToken, MerchantBackend } from "../../../declaration.js"; +import { useSessionContext } from "../../../context/session.js"; import { - useInstanceAPI, useInstanceDetails, useManagedInstanceDetails, - useManagementAPI, } from "../../../hooks/instance.js"; import { Notification } from "../../../utils/types.js"; +import { LoginPage } from "../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; export interface Props { onBack: () => void; onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onUpdateError: (e: HttpError<MerchantBackend.ErrorDetail>) => void; + // onUnauthorized: () => VNode; + // onNotFound: () => VNode; + // onLoadError: (e: HttpError<TalerErrorDetail>) => VNode; + // onUpdateError: (e: HttpError<TalerErrorDetail>) => void; } export default function Update(props: Props): VNode { - const { updateInstance } = useInstanceAPI(); + const { lib } = useSessionContext(); + const updateInstance = lib.instance.updateCurrentInstance.bind(lib.instance) const result = useInstanceDetails(); - return CommonUpdate(props, result, updateInstance, ); + return CommonUpdate(props, result, updateInstance,); } export function AdminUpdate(props: Props & { instanceId: string }): VNode { - const { updateInstance } = useManagementAPI( - props.instanceId, - ); + const { lib } = useSessionContext(); + const t = lib.subInstanceApi(props.instanceId).instance; + const updateInstance = t.updateCurrentInstance.bind(t) const result = useManagedInstanceDetails(props.instanceId); - return CommonUpdate(props, result, updateInstance, ); + return CommonUpdate(props, result, updateInstance,); } + function CommonUpdate( { onBack, onConfirm, - onLoadError, - onNotFound, - onUpdateError, - onUnauthorized, }: Props, - result: HttpResponse< - MerchantBackend.Instances.QueryInstancesResponse, - MerchantBackend.ErrorDetail - >, - updateInstance: any, + result: TalerMerchantManagementResultByMethod<"getInstanceDetails"> | TalerError | undefined, + updateInstance: typeof TalerMerchantInstanceHttpClient.prototype.updateCurrentInstance, ): VNode { const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); + const { state } = useSessionContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading /> + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} /> + } + if (result.type === "fail") { + switch(result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(result) + } + } } return ( @@ -98,15 +94,18 @@ function CommonUpdate( <UpdatePage onBack={onBack} isLoading={false} - selected={result.data} + selected={result.body} onUpdate={( - d: MerchantBackend.Instances.InstanceReconfigurationMessage, + d: TalerMerchantApi.InstanceReconfigurationMessage, ): Promise<void> => { - return updateInstance(d) + if (state.status !== "loggedIn") { + return Promise.resolve(); + } + return updateInstance(state.token, d) .then(onConfirm) .catch((error: Error) => setNotif({ - message: i18n.str`Failed to create instance`, + message: i18n.str`Failed to update instance`, type: "ERROR", description: error.message, }), diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx index 4857ede97..2e2a58b33 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/Create.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 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 index 434d69412..8792aabea 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -28,14 +28,10 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputDuration } from "../../../../components/form/InputDuration.js"; -import { InputNumber } from "../../../../components/form/InputNumber.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; -type Entity = MerchantBackend.Webhooks.WebhookAddDetails; +type Entity = TalerMerchantApi.WebhookAddDetails; interface Props { onCreate: (d: Entity) => Promise<void>; @@ -120,7 +116,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode { <p> The text below support <a target="_blank" rel="noreferrer" href="https://mustache.github.io/mustache.5.html">mustache</a> template engine. Any string between <pre style={{ display: "inline", padding: 0 }}>{{</pre> and <pre style={{ display: "inline", padding: 0 }}>}}</pre> will - be replaced with replaced with the value of the correspoding variable. + be replaced with replaced with the value of the corresponding variable. </p> <p> For example <pre style={{ display: "inline", padding: 0 }}>{{contract_terms.amount}}</pre> will be replaced diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx index 924e6d9b8..70f246ff1 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,33 +19,34 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useWebhookAPI } from "../../../../hooks/webhooks.js"; +import { useSessionContext } from "../../../../context/session.js"; import { Notification } from "../../../../utils/types.js"; import { CreatePage } from "./CreatePage.js"; -export type Entity = MerchantBackend.Webhooks.WebhookAddDetails; +export type Entity = TalerMerchantApi.WebhookAddDetails; interface Props { onBack?: () => void; onConfirm: () => void; } export default function CreateWebhook({ onConfirm, onBack }: Props): VNode { - const { createWebhook } = useWebhookAPI(); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); return ( <> <NotificationCard notification={notif} /> <CreatePage onBack={onBack} - onCreate={(request: MerchantBackend.Webhooks.WebhookAddDetails) => { - return createWebhook(request) + onCreate={(request: TalerMerchantApi.WebhookAddDetails) => { + return lib.instance.addWebhook(state.token, request) .then(() => onConfirm()) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx index 702e9ba4a..707324d40 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/List.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx index 87e221e3c..3f1feb8e9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/ListPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -20,17 +20,17 @@ */ import { h, VNode } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { CardTable } from "./Table.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; export interface Props { - webhooks: MerchantBackend.Webhooks.WebhookEntry[]; + webhooks: TalerMerchantApi.WebhookEntry[]; onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; onCreate: () => void; - onDelete: (e: MerchantBackend.Webhooks.WebhookEntry) => void; - onSelect: (e: MerchantBackend.Webhooks.WebhookEntry) => void; + onDelete: (e: TalerMerchantApi.WebhookEntry) => void; + onSelect: (e: TalerMerchantApi.WebhookEntry) => void; } export function ListPage({ @@ -55,9 +55,7 @@ export function ListPage({ onDelete={onDelete} onSelect={onSelect} onLoadMoreBefore={onLoadMoreBefore} - hasMoreBefore={!onLoadMoreBefore} onLoadMoreAfter={onLoadMoreAfter} - hasMoreAfter={!onLoadMoreAfter} /> </section> ); 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 index 42a179d2c..919285e78 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,12 +19,12 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { TalerMerchantApi } from "@gnu-taler/taler-util"; import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { StateUpdater, useState } from "preact/hooks"; -import { MerchantBackend } from "../../../../declaration.js"; -type Entity = MerchantBackend.Webhooks.WebhookEntry; +type Entity = TalerMerchantApi.WebhookEntry; interface Props { webhooks: Entity[]; @@ -32,8 +32,6 @@ interface Props { onSelect: (e: Entity) => void; onCreate: () => void; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } @@ -44,8 +42,6 @@ export function CardTable({ onSelect, onLoadMoreAfter, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: Props): VNode { const [rowSelection, rowSelectionHandler] = useState<string[]>([]); @@ -85,8 +81,6 @@ export function CardTable({ rowSelectionHandler={rowSelectionHandler} onLoadMoreAfter={onLoadMoreAfter} onLoadMoreBefore={onLoadMoreBefore} - hasMoreAfter={hasMoreAfter} - hasMoreBefore={hasMoreBefore} /> ) : ( <EmptyTable /> @@ -104,35 +98,26 @@ interface TableProps { onSelect: (e: Entity) => void; rowSelectionHandler: StateUpdater<string[]>; onLoadMoreBefore?: () => void; - hasMoreBefore?: boolean; - hasMoreAfter?: boolean; onLoadMoreAfter?: () => void; } -function toggleSelected<T>(id: T): (prev: T[]) => T[] { - return (prev: T[]): T[] => - prev.indexOf(id) == -1 ? [...prev, id] : prev.filter((e) => e != id); -} - function Table({ instances, onLoadMoreAfter, onDelete, onSelect, onLoadMoreBefore, - hasMoreAfter, - hasMoreBefore, }: TableProps): VNode { const { i18n } = useTranslationContext(); return ( <div class="table-container"> - {hasMoreBefore && ( + {onLoadMoreBefore && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more webhooks before the first one`} onClick={onLoadMoreBefore} > - <i18n.Translate>load newer webhooks</i18n.Translate> + <i18n.Translate>load first page</i18n.Translate> </button> )} <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> @@ -186,13 +171,13 @@ function Table({ })} </tbody> </table> - {hasMoreAfter && ( + {onLoadMoreAfter && ( <button class="button is-fullwidth" data-tooltip={i18n.str`load more webhooks after the last one`} onClick={onLoadMoreAfter} > - <i18n.Translate>load older webhooks</i18n.Translate> + <i18n.Translate>load next page</i18n.Translate> </button> )} </div> @@ -205,7 +190,7 @@ function EmptyTable(): VNode { <div class="content has-text-grey has-text-centered"> <p> <span class="icon is-large"> - <i class="mdi mdi-emoticon-sad mdi-48px" /> + <i class="mdi mdi-magnify mdi-48px" /> </span> </p> <p> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx index a6f6f1511..789b8d73b 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -20,57 +20,54 @@ */ import { - ErrorType, - HttpError, - useTranslationContext, + HttpStatusCode, + TalerError, + TalerMerchantApi, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { - useInstanceWebhooks, - useWebhookAPI, -} from "../../../../hooks/webhooks.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useInstanceWebhooks } from "../../../../hooks/webhooks.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; interface Props { - onUnauthorized: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; - onNotFound: () => VNode; onCreate: () => void; onSelect: (id: string) => void; } -export default function ListWebhooks({ - onUnauthorized, - onLoadError, - onCreate, - onSelect, - onNotFound, -}: Props): VNode { - const [position, setPosition] = useState<string | undefined>(undefined); +export default function ListWebhooks({ onCreate, onSelect }: Props): VNode { const { i18n } = useTranslationContext(); const [notif, setNotif] = useState<Notification | undefined>(undefined); - const { deleteWebhook } = useWebhookAPI(); - const result = useInstanceWebhooks({ position }, (id) => setPosition(id)); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); + const result = useInstanceWebhooks(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } } return ( @@ -78,17 +75,16 @@ export default function ListWebhooks({ <NotificationCard notification={notif} /> <ListPage - webhooks={result.data.webhooks} - onLoadMoreBefore={ - result.isReachingStart ? result.loadMorePrev : undefined - } - onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined} + webhooks={result.body.webhooks} + onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} + onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} onCreate={onCreate} onSelect={(e) => { onSelect(e.webhook_id); }} - onDelete={(e: MerchantBackend.Webhooks.WebhookEntry) => - deleteWebhook(e.webhook_id) + onDelete={(e: TalerMerchantApi.WebhookEntry) => { + return lib.instance + .deleteWebhook(state.token, e.webhook_id) .then(() => setNotif({ message: i18n.str`webhook delete successfully`, @@ -101,8 +97,8 @@ export default function ListWebhooks({ type: "ERROR", description: error.message, }), - ) - } + ); + }} /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx index 8d07cb31f..303d17b72 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/Update.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 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 index 76a23b6e5..6aca62582 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -28,10 +28,9 @@ import { FormProvider, } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { TalerMerchantApi } from "@gnu-taler/taler-util"; -type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId; +type Entity = TalerMerchantApi.WebhookPatchDetails & WithId; interface Props { onUpdate: (d: Entity) => Promise<void>; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx index 3f723ed87..5b2ba7bb9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,71 +19,69 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, TalerError, TalerMerchantApi, assertUnreachable } from "@gnu-taler/taler-util"; import { - ErrorType, - HttpError, - useTranslationContext, + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend, WithId } from "../../../../declaration.js"; +import { useSessionContext } from "../../../../context/session.js"; import { - useWebhookAPI, useWebhookDetails, } from "../../../../hooks/webhooks.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { UpdatePage } from "./UpdatePage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -export type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId; +export type Entity = TalerMerchantApi.WebhookPatchDetails & WithId; interface Props { onBack?: () => void; onConfirm: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (e: HttpError<MerchantBackend.ErrorDetail>) => VNode; tid: string; } export default function UpdateWebhook({ tid, onConfirm, onBack, - onUnauthorized, - onNotFound, - onLoadError, }: Props): VNode { - const { updateWebhook } = useWebhookAPI(); + const { lib } = useSessionContext(); + const { state } = useSessionContext(); const result = useWebhookDetails(tid); const [notif, setNotif] = useState<Notification | undefined>(undefined); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } } return ( <Fragment> <NotificationCard notification={notif} /> <UpdatePage - webhook={{ ...result.data, id: tid }} + webhook={{ ...result.body, id: tid }} onBack={onBack} onUpdate={(data) => { - return updateWebhook(tid, data) + return lib.instance.updateWebhook(state.token, tid, data) .then(onConfirm) .catch((error) => { setNotif({ diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx index 1c98b7c9b..272c40b55 100644 --- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,184 +19,156 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { useTranslationContext } from "@gnu-taler/web-util/browser"; -import { ComponentChildren, h, VNode } from "preact"; -import { useCallback, useEffect, useState } from "preact/hooks"; -import { useBackendContext } from "../../context/backend.js"; -import { useInstanceContext } from "../../context/instance.js"; -import { AccessToken, LoginToken } from "../../declaration.js"; -import { useCredentialsChecker } from "../../hooks/backend.js"; - -interface Props { - onConfirm: (token: LoginToken | undefined) => void; -} - -function normalizeToken(r: string): AccessToken { - return `secret-token:${r}` as AccessToken; -} - -export function LoginPage({ onConfirm }: Props): VNode { - const { url: backendURL } = useBackendContext(); - const { admin, id } = useInstanceContext(); - const { requestNewLoginToken } = useCredentialsChecker(); +import { HttpStatusCode, createAccessToken } from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { ComponentChildren, Fragment, VNode, h } from "preact"; +import { useState } from "preact/hooks"; +import { NotificationCard } from "../../components/menu/index.js"; +import { useSessionContext } from "../../context/session.js"; +import { Notification } from "../../utils/types.js"; + +interface Props {} + +const tokenRequest = { + scope: "write", + duration: { + d_us: "forever" as const, + }, + refreshable: true, +}; + +export function LoginPage(_p: Props): VNode { const [token, setToken] = useState(""); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { state, logIn } = useSessionContext(); + const { lib } = useSessionContext(); const { i18n } = useTranslationContext(); - - const doLogin = useCallback(async function doLoginImpl() { - const secretToken = normalizeToken(token); - const baseUrl = id === undefined ? backendURL : `${backendURL}/instances/${id}` - const result = await requestNewLoginToken(baseUrl, secretToken); - if (result.valid) { - const { token, expiration } = result - onConfirm({ token, expiration }); + async function doLoginImpl() { + const result = await lib.authenticate.createAccessTokenBearer( + createAccessToken(token), + tokenRequest, + ); + if (result.type === "ok") { + const { token } = result.body; + logIn(token); + return; } else { - onConfirm(undefined); + switch (result.case) { + case HttpStatusCode.Unauthorized: { + setNotif({ + message: "Your password is incorrect", + type: "ERROR", + }); + return; + } + case HttpStatusCode.NotFound: { + setNotif({ + message: "Your instance not found", + type: "ERROR", + }); + return; + } + } } - }, [id, token]) - - if (admin && id !== "default") { - //admin trying to access another instance - return (<div class="columns is-centered" style={{ margin: "auto" }}> - <div class="column is-two-thirds "> - <div class="modal-card" style={{ width: "100%", margin: 0 }}> - <header - class="modal-card-head" - style={{ border: "1px solid", borderBottom: 0 }} - > - <p class="modal-card-title">{i18n.str`Login required`}</p> - </header> - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} - > - <p> - <i18n.Translate>Need the access token for the instance.</i18n.Translate> - </p> - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Access Token</i18n.Translate> - </label> - </div> - <div class="field-body"> - <div class="field"> - <p class="control is-expanded"> - <input - class="input" - type="password" - placeholder={"current access token"} - name="token" - onKeyPress={(e) => - e.keyCode === 13 - ? doLogin() - : null - } - value={token} - onInput={(e): void => setToken(e?.currentTarget.value)} - /> - </p> - </div> - </div> - </div> - </section> - <footer - class="modal-card-foot " - style={{ - justifyContent: "flex-end", - border: "1px solid", - borderTop: 0, - }} - > - <AsyncButton - onClick={doLogin} - > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </footer> - </div> - </div> - </div>) } return ( - <div class="columns is-centered" style={{ margin: "auto" }}> - <div class="column is-two-thirds "> - <div class="modal-card" style={{ width: "100%", margin: 0 }}> - <header - class="modal-card-head" - style={{ border: "1px solid", borderBottom: 0 }} - > - <p class="modal-card-title">{i18n.str`Login required`}</p> - </header> - <section - class="modal-card-body" - style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} - > - <i18n.Translate>Please enter your access token.</i18n.Translate> - - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Access Token</i18n.Translate> - </label> - </div> - <div class="field-body"> - <div class="field"> - <p class="control is-expanded"> - <input - class="input" - type="password" - placeholder={"current access token"} - name="token" - onKeyPress={(e) => - e.keyCode === 13 - ? doLogin() - : null - } - value={token} - onInput={(e): void => setToken(e?.currentTarget.value)} - /> - </p> + <Fragment> + <NotificationCard notification={notif} /> + <div class="columns is-centered" style={{ margin: "auto" }}> + <div class="column is-two-thirds "> + <div class="modal-card" style={{ width: "100%", margin: 0 }}> + <header + class="modal-card-head" + style={{ border: "1px solid", borderBottom: 0 }} + > + <p class="modal-card-title">{i18n.str`Login required`}</p> + </header> + <section + class="modal-card-body" + style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }} + > + <i18n.Translate> + Please enter your access token for <b>"{state.instance}"</b>. + </i18n.Translate> + + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Access Token</i18n.Translate> + </label> + </div> + <div class="field-body"> + <div class="field"> + <p class="control is-expanded"> + <input + class="input" + type="password" + placeholder={"current access token"} + name="token" + onKeyPress={(e) => + e.keyCode === 13 ? doLoginImpl() : null + } + value={token} + onInput={(e): void => setToken(e?.currentTarget.value)} + /> + </p> + </div> </div> </div> - </div> - </section> - <footer - class="modal-card-foot " - style={{ - justifyContent: "space-between", - border: "1px solid", - borderTop: 0, - }} - > - <div /> - <AsyncButton - type="is-info" - onClick={doLogin} + </section> + <footer + class="modal-card-foot " + style={{ + justifyContent: "space-between", + border: "1px solid", + borderTop: 0, + }} > - <i18n.Translate>Confirm</i18n.Translate> - </AsyncButton> - </footer> + <div /> + <AsyncButton type="is-info" onClick={doLoginImpl}> + <i18n.Translate>Confirm</i18n.Translate> + </AsyncButton> + </footer> + </div> </div> </div> - </div> + </Fragment> ); } -function AsyncButton({ onClick, disabled, type = "", children }: { type?: string, disabled?: boolean, onClick: () => Promise<void>, children: ComponentChildren }): VNode { - const [running, setRunning] = useState(false) - return <button class={"button " + type} disabled={disabled || running} onClick={() => { - setRunning(true) - onClick().then(() => { - setRunning(false) - }).catch(() => { - setRunning(false) - }) - }}> - {children} - </button> +function AsyncButton({ + onClick, + disabled, + type = "", + children, +}: { + type?: string; + disabled?: boolean; + onClick: () => Promise<void>; + children: ComponentChildren; +}): VNode { + const [running, setRunning] = useState(false); + return ( + <button + class={"button " + type} + disabled={disabled || running} + onClick={() => { + setRunning(true); + onClick() + .then(() => { + setRunning(false); + }) + .catch(() => { + setRunning(false); + }); + }} + > + {children} + </button> + ); } - - diff --git a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx index 061a67025..4d348c02b 100644 --- a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,10 +19,19 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { h, VNode } from "preact"; -import { Link } from "preact-router"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { Link, route } from "preact-router"; +import { NotificationCard } from "../../components/menu/index.js"; +import { + cleanAllCache, + DEFAULT_ADMIN_USERNAME, + useSessionContext, +} from "../../context/session.js"; +import InstanceCreatePage from "../../paths/admin/create/index.js"; +import { InstancePaths } from "../../Routing.js"; -export default function NotFoundPage(): VNode { +export function NotFoundPage(): VNode { return ( <div> <p>That page doesn't exist.</p> @@ -32,3 +41,32 @@ export default function NotFoundPage(): VNode { </div> ); } + +export function NotFoundPageOrAdminCreate(): VNode { + const { state } = useSessionContext(); + const { i18n } = useTranslationContext(); + if (state.isAdmin && state.instance === DEFAULT_ADMIN_USERNAME) { + return ( + <Fragment> + <NotificationCard + notification={{ + message: i18n.str`No 'default' instance configured yet.`, + description: i18n.str`Create a 'default' instance to begin using the merchant backoffice.`, + type: "INFO", + }} + /> + <InstanceCreatePage + forceId={DEFAULT_ADMIN_USERNAME} + onConfirm={() => { + // we need to clear everything since we take some + // 404 as "default instance don't exist" + cleanAllCache() + route(InstancePaths.bank_list); + }} + /> + </Fragment> + ); + } + + return <NotFoundPage /> +} diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx index 87bd2fa39..0c4b9dd1a 100644 --- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx @@ -1,10 +1,30 @@ +/* + 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 { useLang, useTranslationContext } from "@gnu-taler/web-util/browser"; import { VNode, h } from "preact"; -import { FormErrors, FormProvider } from "../../components/form/FormProvider.js"; +import { + FormErrors, + FormProvider, +} from "../../components/form/FormProvider.js"; import { InputSelector } from "../../components/form/InputSelector.js"; import { InputToggle } from "../../components/form/InputToggle.js"; import { LangSelector } from "../../components/menu/LangSelector.js"; -import { Settings, useSettings } from "../../hooks/useSettings.js"; +import { Preferences, usePreference } from "../../hooks/preference.js"; +import { AbsoluteTime } from "@gnu-taler/taler-util"; function getBrowserLang(): string | undefined { if (typeof window === "undefined") return undefined; @@ -14,99 +34,108 @@ function getBrowserLang(): string | undefined { } export function Settings({ onClose }: { onClose?: () => void }): VNode { - const { i18n } = useTranslationContext() - const borwserLang = getBrowserLang() - const { update } = useLang() + const { i18n } = useTranslationContext(); + const borwserLang = getBrowserLang(); + const { update } = useLang(undefined, {}); - const [value, updateValue] = useSettings() - const errors: FormErrors<Settings> = { - } + const [value, , updateValue] = usePreference(); + const errors: FormErrors<Preferences> = {}; - function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void { - const next = s(value) - const v: Settings = { + function valueHandler(s: (d: Partial<Preferences>) => Partial<Preferences>): void { + const next = s(value); + const v: Preferences = { advanceOrderMode: next.advanceOrderMode ?? false, - dateFormat: next.dateFormat ?? "ymd" - } - updateValue(v) + hideMissingAccountUntil: next.hideMissingAccountUntil ?? AbsoluteTime.never(), + hideKycUntil: next.hideKycUntil ?? AbsoluteTime.never(), + dateFormat: next.dateFormat ?? "ymd", + }; + updateValue(v); } - return <div> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <div> - - <FormProvider<Settings> - name="settings" - errors={errors} - object={value} - valueHandler={valueHandler} - > - <div class="field is-horizontal"> - <div class="field-label is-normal"> - <label class="label"> - <i18n.Translate>Language</i18n.Translate> - <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}> - <i class="mdi mdi-information" /> - </span> - </label> - </div> - <div class="field field-body has-addons is-flex-grow-3"> - <LangSelector /> - - {borwserLang !== undefined && <button - data-tooltip={i18n.str`generate random secret key`} - class="button is-info mr-2" - onClick={(e) => { - update(borwserLang.substring(0, 2)) - }} - > - <i18n.Translate>Set default</i18n.Translate> - </button>} + return ( + <div> + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-four-fifths"> + <div> + <FormProvider<Preferences> + name="settings" + errors={errors} + object={value} + valueHandler={valueHandler} + > + <div class="field is-horizontal"> + <div class="field-label is-normal"> + <label class="label"> + <i18n.Translate>Language</i18n.Translate> + <span + class="icon has-tooltip-right" + data-tooltip={ + "Force language setting instance of taking the browser" + } + > + <i class="mdi mdi-information" /> + </span> + </label> + </div> + <div class="field field-body has-addons is-flex-grow-3"> + <LangSelector /> + + {borwserLang !== undefined && ( + <button + data-tooltip={i18n.str`generate random secret key`} + class="button is-info mr-2" + onClick={(e) => { + update(borwserLang.substring(0, 2)); + e.preventDefault() + }} + > + <i18n.Translate>Set default</i18n.Translate> + </button> + )} + </div> </div> - </div> - <InputToggle<Settings> - label={i18n.str`Advance order creation`} - tooltip={i18n.str`Shows more options in the order creation form`} - name="advanceOrderMode" - /> - <InputSelector<Settings> - name="dateFormat" - label={i18n.str`Date format`} - expand={true} - help={ - value.dateFormat === "dmy" ? "31/12/2001" : value.dateFormat === "mdy" ? "12/31/2001" : value.dateFormat === "ymd" ? "2001/12/31" : "" - } - toStr={(e) => { - if (e === "ymd") return "year month day" - if (e === "mdy") return "month day year" - if (e === "dmy") return "day month year" - return "choose one" - }} - values={[ - "ymd", - "mdy", - "dmy", - ]} - tooltip={i18n.str`how the date is going to be displayed`} - /> - </FormProvider> + <InputToggle<Preferences> + label={i18n.str`Advance order creation`} + tooltip={i18n.str`Shows more options in the order creation form`} + name="advanceOrderMode" + /> + <InputSelector<Preferences> + name="dateFormat" + label={i18n.str`Date format`} + expand={true} + help={ + value.dateFormat === "dmy" + ? "31/12/2001" + : value.dateFormat === "mdy" + ? "12/31/2001" + : value.dateFormat === "ymd" + ? "2001/12/31" + : "" + } + toStr={(e) => { + if (e === "ymd") return "year month day"; + if (e === "mdy") return "month day year"; + if (e === "dmy") return "day month year"; + return "choose one"; + }} + values={["ymd", "mdy", "dmy"]} + tooltip={i18n.str`how the date is going to be displayed`} + /> + </FormProvider> + </div> </div> + <div class="column" /> </div> - <div class="column" /> - </div> - </section > - {onClose && - <section class="section is-main-section"> - <button - class="button" - onClick={onClose} - > - <i18n.Translate>Close</i18n.Translate> - </button> </section> - } - </div > -}
\ No newline at end of file + {onClose && ( + <section class="section is-main-section"> + <button class="button" onClick={onClose}> + <i18n.Translate>Close</i18n.Translate> + </button> + </section> + )} + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts index 5bcb68021..43384299b 100644 --- a/packages/merchant-backoffice-ui/src/schemas/index.ts +++ b/packages/merchant-backoffice-ui/src/schemas/index.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,6 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { Amounts } from "@gnu-taler/taler-util"; import { isAfter, isFuture } from "date-fns"; import * as yup from "yup"; import { PAYTO_REGEX } from "../utils/constants.js"; @@ -126,27 +127,6 @@ export const InstanceSchema = yup.object().shape({ export const InstanceUpdateSchema = InstanceSchema.clone().omit(["id"]); export const InstanceCreateSchema = InstanceSchema.clone(); -export const AuthorizeRewardSchema = yup.object().shape({ - justification: yup.string().required(), - amount: yup - .string() - .required() - .test("amount", "the amount is not valid", currencyWithAmountIsValid) - .test("amount_positive", "the amount is not valid", currencyGreaterThan0), - next_url: yup.string().required(), -}); - -const stringIsValidJSON = (value?: string) => { - const p = value?.trim(); - if (!p) return true; - try { - JSON.parse(p); - return true; - } catch { - return false; - } -}; - export const OrderCreateSchema = yup.object().shape({ pricing: yup .object() diff --git a/packages/merchant-backoffice-ui/src/scss/_aside.scss b/packages/merchant-backoffice-ui/src/scss/_aside.scss index e0922093b..b7b59516b 100644 --- a/packages/merchant-backoffice-ui/src/scss/_aside.scss +++ b/packages/merchant-backoffice-ui/src/scss/_aside.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_card.scss b/packages/merchant-backoffice-ui/src/scss/_card.scss index 62db7f457..a4118400f 100644 --- a/packages/merchant-backoffice-ui/src/scss/_card.scss +++ b/packages/merchant-backoffice-ui/src/scss/_card.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss b/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss index 34c40092b..62414a00a 100644 --- a/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss +++ b/packages/merchant-backoffice-ui/src/scss/_custom-calendar.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_footer.scss b/packages/merchant-backoffice-ui/src/scss/_footer.scss index 5855af742..7e90c40cc 100644 --- a/packages/merchant-backoffice-ui/src/scss/_footer.scss +++ b/packages/merchant-backoffice-ui/src/scss/_footer.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_form.scss b/packages/merchant-backoffice-ui/src/scss/_form.scss index bd28a17cf..126d3d0cc 100644 --- a/packages/merchant-backoffice-ui/src/scss/_form.scss +++ b/packages/merchant-backoffice-ui/src/scss/_form.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_hero-bar.scss b/packages/merchant-backoffice-ui/src/scss/_hero-bar.scss index 0276468d7..cb3f438e9 100644 --- a/packages/merchant-backoffice-ui/src/scss/_hero-bar.scss +++ b/packages/merchant-backoffice-ui/src/scss/_hero-bar.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_loading.scss b/packages/merchant-backoffice-ui/src/scss/_loading.scss index d88d8c355..32f64f276 100644 --- a/packages/merchant-backoffice-ui/src/scss/_loading.scss +++ b/packages/merchant-backoffice-ui/src/scss/_loading.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_main-section.scss b/packages/merchant-backoffice-ui/src/scss/_main-section.scss index 5a8b20ba0..444af5235 100644 --- a/packages/merchant-backoffice-ui/src/scss/_main-section.scss +++ b/packages/merchant-backoffice-ui/src/scss/_main-section.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_misc.scss b/packages/merchant-backoffice-ui/src/scss/_misc.scss index 045d087e2..a0dbc64fc 100644 --- a/packages/merchant-backoffice-ui/src/scss/_misc.scss +++ b/packages/merchant-backoffice-ui/src/scss/_misc.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_modal.scss b/packages/merchant-backoffice-ui/src/scss/_modal.scss index b2bfd3e9e..d2565e7c7 100644 --- a/packages/merchant-backoffice-ui/src/scss/_modal.scss +++ b/packages/merchant-backoffice-ui/src/scss/_modal.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_nav-bar.scss b/packages/merchant-backoffice-ui/src/scss/_nav-bar.scss index 406e0392f..4c0e2f5cc 100644 --- a/packages/merchant-backoffice-ui/src/scss/_nav-bar.scss +++ b/packages/merchant-backoffice-ui/src/scss/_nav-bar.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_table.scss b/packages/merchant-backoffice-ui/src/scss/_table.scss index e4fbfc7b3..6c7765a74 100644 --- a/packages/merchant-backoffice-ui/src/scss/_table.scss +++ b/packages/merchant-backoffice-ui/src/scss/_table.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_theme-default.scss b/packages/merchant-backoffice-ui/src/scss/_theme-default.scss index e74ece0e9..f34497bde 100644 --- a/packages/merchant-backoffice-ui/src/scss/_theme-default.scss +++ b/packages/merchant-backoffice-ui/src/scss/_theme-default.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_tiles.scss b/packages/merchant-backoffice-ui/src/scss/_tiles.scss index 94dd6c21d..75bc6b94e 100644 --- a/packages/merchant-backoffice-ui/src/scss/_tiles.scss +++ b/packages/merchant-backoffice-ui/src/scss/_tiles.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/_title-bar.scss b/packages/merchant-backoffice-ui/src/scss/_title-bar.scss index bac3f6b42..5de384a32 100644 --- a/packages/merchant-backoffice-ui/src/scss/_title-bar.scss +++ b/packages/merchant-backoffice-ui/src/scss/_title-bar.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/fonts/nunito.css b/packages/merchant-backoffice-ui/src/scss/fonts/nunito.css index a578506e8..591fc3da2 100644 --- a/packages/merchant-backoffice-ui/src/scss/fonts/nunito.css +++ b/packages/merchant-backoffice-ui/src/scss/fonts/nunito.css @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/libs/_all.scss b/packages/merchant-backoffice-ui/src/scss/libs/_all.scss index cba6f26eb..ab8030a13 100644 --- a/packages/merchant-backoffice-ui/src/scss/libs/_all.scss +++ b/packages/merchant-backoffice-ui/src/scss/libs/_all.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/main.scss b/packages/merchant-backoffice-ui/src/scss/main.scss index c4be8aa73..4a46472f9 100644 --- a/packages/merchant-backoffice-ui/src/scss/main.scss +++ b/packages/merchant-backoffice-ui/src/scss/main.scss @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/scss/toggle.scss b/packages/merchant-backoffice-ui/src/scss/toggle.scss index 24636da2f..6c7346eb3 100644 --- a/packages/merchant-backoffice-ui/src/scss/toggle.scss +++ b/packages/merchant-backoffice-ui/src/scss/toggle.scss @@ -4,6 +4,7 @@ $green: #56c080; cursor: pointer; display: inline-block; } + .toggle-switch { display: inline-block; background: #ccc; @@ -13,10 +14,12 @@ $green: #56c080; position: relative; vertical-align: middle; transition: background 0.25s; + &:before, &:after { content: ""; } + &:before { display: block; background: linear-gradient(to bottom, #fff 0%, #eee 100%); @@ -29,23 +32,36 @@ $green: #56c080; left: 4px; transition: left 0.25s; } + .toggle:hover &:before { background: linear-gradient(to bottom, #fff 0%, #fff 100%); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5); } - .toggle-checkbox:checked + & { + + &.disabled:before { + background: linear-gradient(to bottom, #ccc 0%, #bbb 100%); + } + + .toggle:hover &.disabled:before { + background: linear-gradient(to bottom, #ccc 0%, #bbb 100%); + } + + .toggle-checkbox:checked+& { background: $green; + &:before { left: 30px; } } } + .toggle-checkbox { position: absolute; visibility: hidden; } + .toggle-label { margin-left: 5px; position: relative; top: 2px; -} +}
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/settings.ts b/packages/merchant-backoffice-ui/src/settings.ts new file mode 100644 index 000000000..0e377bec2 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/settings.ts @@ -0,0 +1,84 @@ +/* + 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 { + Codec, + buildCodecForObject, + canonicalizeBaseUrl, + codecForString, + codecOptional +} from "@gnu-taler/taler-util"; + +export interface MerchantUiSettings { + // Where merchant backend is localted + // default: window.origin without "webui/" + backendBaseURL?: string; +} + +/** + * Global settings for the bank UI. + */ +const defaultSettings: MerchantUiSettings = { + backendBaseURL: buildDefaultBackendBaseURL(), +}; + +const codecForBankUISettings = (): Codec<MerchantUiSettings> => + buildCodecForObject<MerchantUiSettings>() + .property("backendBaseURL", codecOptional(codecForString())) + .build("MerchantUiSettings"); + +function removeUndefineField<T extends object>(obj: T): T { + const keys = Object.keys(obj) as Array<keyof T>; + return keys.reduce((prev, cur) => { + if (typeof prev[cur] === "undefined") { + delete prev[cur]; + } + return prev; + }, obj); +} + +export function fetchSettings(listener: (s: MerchantUiSettings) => void): void { + fetch("./settings.json") + .then((resp) => resp.json()) + .then((json) => codecForBankUISettings().decode(json)) + .then((result) => + listener({ + ...defaultSettings, + ...removeUndefineField(result), + }), + ) + .catch((e) => { + console.log("failed to fetch settings", e); + listener(defaultSettings); + }); +} + +export function buildDefaultBackendBaseURL(): string { + if (typeof window !== "undefined") { + const currentLocation = new URL( + window.location.pathname, + window.location.origin, + ).href; + /** + * By default, merchant backend serves the html content + * from the /webui root. This should cover most of the + * cases and the rootPath will be the merchant backend + * URL where the instances are + */ + return canonicalizeBaseUrl(currentLocation.replace("/webui", "")); + } + throw Error("No default URL") +} diff --git a/packages/merchant-backoffice-ui/src/stories.test.ts b/packages/merchant-backoffice-ui/src/stories.test.ts index abd993550..6ce88b916 100644 --- a/packages/merchant-backoffice-ui/src/stories.test.ts +++ b/packages/merchant-backoffice-ui/src/stories.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/stories.tsx b/packages/merchant-backoffice-ui/src/stories.tsx index 67e658016..8bb06b8cb 100644 --- a/packages/merchant-backoffice-ui/src/stories.tsx +++ b/packages/merchant-backoffice-ui/src/stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/sw.js b/packages/merchant-backoffice-ui/src/sw.js index 0b1799e23..bf52db6fa 100644 --- a/packages/merchant-backoffice-ui/src/sw.js +++ b/packages/merchant-backoffice-ui/src/sw.js @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/utils/amount.ts b/packages/merchant-backoffice-ui/src/utils/amount.ts index 475489d3e..c94101b4b 100644 --- a/packages/merchant-backoffice-ui/src/utils/amount.ts +++ b/packages/merchant-backoffice-ui/src/utils/amount.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -17,8 +17,8 @@ import { amountFractionalBase, AmountJson, Amounts, + TalerMerchantApi, } from "@gnu-taler/taler-util"; -import { MerchantBackend } from "../declaration.js"; /** * merge refund with the same description and a difference less than one minute @@ -27,9 +27,9 @@ import { MerchantBackend } from "../declaration.js"; * @returns list with the new refund, may be merged with the last */ export function mergeRefunds( - prev: MerchantBackend.Orders.RefundDetails[], - cur: MerchantBackend.Orders.RefundDetails, -): MerchantBackend.Orders.RefundDetails[] { + prev: TalerMerchantApi.RefundDetails[], + cur: TalerMerchantApi.RefundDetails, +): TalerMerchantApi.RefundDetails[] { let tail; if ( diff --git a/packages/merchant-backoffice-ui/src/utils/constants.ts b/packages/merchant-backoffice-ui/src/utils/constants.ts index 7c4e288b3..6b4d8eade 100644 --- a/packages/merchant-backoffice-ui/src/utils/constants.ts +++ b/packages/merchant-backoffice-ui/src/utils/constants.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -27,8 +27,6 @@ export const PAYTO_WIRE_METHOD_LOOKUP = export const AMOUNT_REGEX = /^[a-zA-Z][a-zA-Z]{1,11}:[0-9][0-9,]*\.?[0-9,]*$/; -export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/; - export const AMOUNT_ZERO_REGEX = /^[a-zA-Z][a-zA-Z]*:0$/; export const CROCKFORD_BASE32_REGEX = @@ -37,11 +35,10 @@ export const CROCKFORD_BASE32_REGEX = export const URL_REGEX = /^((https?:)(\/\/\/?)([\w]*(?::[\w]*)?@)?([\d\w\.-]+)(?::(\d+))?)\/$/; -// how much rows we add every time user hit load more -export const PAGE_SIZE = 20; -// how bigger can be the result set -// after this threshold, load more with move the cursor -export const MAX_RESULT_SIZE = PAGE_SIZE * 2 - 1; +export const PAGINATED_LIST_SIZE = 5; +// when doing paginated request, ask for one more +// and use it to know if there are more to request +export const PAGINATED_LIST_REQUEST = PAGINATED_LIST_SIZE + 1; // how much we will wait for all request, in seconds export const DEFAULT_REQUEST_TIMEOUT = 10; diff --git a/packages/merchant-backoffice-ui/src/utils/crypto.ts b/packages/merchant-backoffice-ui/src/utils/crypto.ts deleted file mode 100644 index 27e6ade02..000000000 --- a/packages/merchant-backoffice-ui/src/utils/crypto.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -const encTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; -// base32 RFC 3548 -function encodeBase32(data: ArrayBuffer) { - const dataBytes = new Uint8Array(data); - let sb = ""; - const size = data.byteLength; - let bitBuf = 0; - let numBits = 0; - let pos = 0; - while (pos < size || numBits > 0) { - if (pos < size && numBits < 5) { - const d = dataBytes[pos++]; - bitBuf = (bitBuf << 8) | d; - numBits += 8; - } - if (numBits < 5) { - // zero-padding - bitBuf = bitBuf << (5 - numBits); - numBits = 5; - } - const v = (bitBuf >>> (numBits - 5)) & 31; - sb += encTable[v]; - numBits -= 5; - } - return sb; -} - -export function isBase32RFC3548Charset(s: string): boolean { - for (let idx = 0; idx < s.length; idx++) { - const c = s.charAt(idx); - if (encTable.indexOf(c) === -1) return false; - } - return true; -} - -export function randomBase32Key(): string { - var buf = new Uint8Array(20); - window.crypto.getRandomValues(buf); - return encodeBase32(buf); -} diff --git a/packages/merchant-backoffice-ui/src/utils/regex.test.ts b/packages/merchant-backoffice-ui/src/utils/regex.test.ts index 984f1a472..78f2ef5ae 100644 --- a/packages/merchant-backoffice-ui/src/utils/regex.test.ts +++ b/packages/merchant-backoffice-ui/src/utils/regex.test.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 diff --git a/packages/merchant-backoffice-ui/src/utils/table.ts b/packages/merchant-backoffice-ui/src/utils/table.ts index 71358e25f..982b68e5e 100644 --- a/packages/merchant-backoffice-ui/src/utils/table.ts +++ b/packages/merchant-backoffice-ui/src/utils/table.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -14,7 +14,6 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { WithId } from "../declaration.js"; /** * @@ -51,7 +50,7 @@ export function buildActions<T extends WithId>( */ export function undefinedIfEmpty< T extends Record<string, unknown> | Array<unknown>, ->(obj: T): T | undefined { +>(obj: T | undefined): T | undefined { if (obj === undefined) return undefined; return Object.values(obj).some((v) => v !== undefined) ? obj : undefined; } diff --git a/packages/merchant-backoffice-ui/src/utils/types.ts b/packages/merchant-backoffice-ui/src/utils/types.ts index 0d249f3c4..9ce6da4d1 100644 --- a/packages/merchant-backoffice-ui/src/utils/types.ts +++ b/packages/merchant-backoffice-ui/src/utils/types.ts @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -23,7 +23,7 @@ export interface KeyValue { export interface Notification { message: string; description?: string | VNode; - details?: string | VNode; + details?: string | VNode | string; type: MessageType; } |