summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-09-04 14:17:55 -0300
committerSebastian <sebasjm@gmail.com>2023-09-04 14:17:55 -0300
commite1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7 (patch)
treed4ed5506ab3550a7e9b1a082d7ffeddf9f3c4954
parentff20c3e25e076c24f7cb93eabe58b6f934f51f35 (diff)
downloadwallet-core-e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7.tar.gz
wallet-core-e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7.tar.bz2
wallet-core-e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7.zip
backoffcie new version, lot of changes
-rw-r--r--packages/merchant-backoffice-ui/src/Application.tsx60
-rw-r--r--packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx86
-rw-r--r--packages/merchant-backoffice-ui/src/InstanceRoutes.tsx219
-rw-r--r--packages/merchant-backoffice-ui/src/components/exception/login.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputDate.tsx11
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx305
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx (renamed from packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx)97
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx31
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx81
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/index.tsx58
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx8
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx18
-rw-r--r--packages/merchant-backoffice-ui/src/context/backend.ts52
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts420
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/backend.ts111
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/bank.ts217
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/index.ts23
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.test.ts6
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.ts5
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/otp.ts223
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/reserve.test.ts114
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/reserves.ts64
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/urls.ts34
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/useSettings.ts37
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx96
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx175
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx65
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx64
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx385
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx107
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx114
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx96
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx10
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.stories.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/Create.stories.tsx7
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx161
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx42
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx27
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx18
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/Table.tsx62
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx48
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx22
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx8
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx36
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/details/Details.stories.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx)23
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx (renamed from packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx)40
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx18
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/list/List.stories.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx39
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx82
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx127
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx51
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx62
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/index.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx121
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx165
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx90
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx3
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx10
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx3
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/Update.stories.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx118
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/create/Create.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx195
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx104
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/create/index.tsx70
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/list/List.stories.tsx28
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/list/ListPage.tsx64
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx213
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/list/index.tsx106
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/update/Update.stories.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx185
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx102
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/list/Table.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/paths/settings/index.tsx103
-rw-r--r--packages/merchant-backoffice-ui/src/schemas/index.ts4
-rw-r--r--packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx7
90 files changed, 4658 insertions, 1576 deletions
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
index f6a81ff8d..5e82821ae 100644
--- a/packages/merchant-backoffice-ui/src/Application.tsx
+++ b/packages/merchant-backoffice-ui/src/Application.tsx
@@ -19,19 +19,20 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util";
import {
ErrorType,
TranslationProvider,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { route } from "preact-router";
-import { useMemo, useState } from "preact/hooks";
+import { useMemo } from "preact/hooks";
import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
import { Loading } from "./components/exception/loading.js";
import {
- NotificationCard,
- NotYetReadyAppMenu,
+ NotConnectedAppMenu,
+ NotificationCard
} from "./components/menu/index.js";
import {
BackendContextProvider,
@@ -41,23 +42,24 @@ import { ConfigContextProvider } from "./context/config.js";
import { useBackendConfig } from "./hooks/backend.js";
import { strings } from "./i18n/strings.js";
import LoginPage from "./paths/login/index.js";
-import { HttpStatusCode } from "@gnu-taler/taler-util";
-import { Settings } from "./paths/settings/index.js";
export function Application(): VNode {
return (
- // <FetchContextProvider>
<BackendContextProvider>
<TranslationProvider source={strings}>
<ApplicationStatusRoutes />
</TranslationProvider>
</BackendContextProvider>
- // </FetchContextProvider>
);
}
+/**
+ * Check connection testing against /config
+ *
+ * @returns
+ */
function ApplicationStatusRoutes(): VNode {
- const { updateLoginStatus, triedToLog } = useBackendContext();
+ const { url, updateLoginStatus, triedToLog } = useBackendContext();
const result = useBackendConfig();
const { i18n } = useTranslationContext();
@@ -71,19 +73,10 @@ function ApplicationStatusRoutes(): VNode {
: { currency: "unknown", version: "unknown" };
const ctx = useMemo(() => ({ currency, version }), [currency, version]);
- const [showSettings, setShowSettings] = useState(false)
-
- if (showSettings) {
- return <Fragment>
- <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" />
- <Settings />
- </Fragment>
- }
-
if (!triedToLog) {
return (
<Fragment>
- <NotYetReadyAppMenu title="Welcome!" onShowSettings={() => setShowSettings(true)} />
+ <NotConnectedAppMenu title="Welcome!" />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment>
);
@@ -97,7 +90,7 @@ function ApplicationStatusRoutes(): VNode {
) {
return (
<Fragment>
- <NotYetReadyAppMenu title="Login" onShowSettings={() => setShowSettings(true)} />
+ <NotConnectedAppMenu title="Login" />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment>
);
@@ -108,7 +101,7 @@ function ApplicationStatusRoutes(): VNode {
) {
return (
<Fragment>
- <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
+ <NotConnectedAppMenu title="Error" />
<NotificationCard
notification={{
message: i18n.str`Server not found`,
@@ -122,7 +115,7 @@ function ApplicationStatusRoutes(): VNode {
}
if (result.type === ErrorType.SERVER) {
<Fragment>
- <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
+ <NotConnectedAppMenu title="Error" />
<NotificationCard
notification={{
message: i18n.str`Server response with an error code`,
@@ -135,7 +128,7 @@ function ApplicationStatusRoutes(): VNode {
}
if (result.type === ErrorType.UNREADABLE) {
<Fragment>
- <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
+ <NotConnectedAppMenu title="Error" />
<NotificationCard
notification={{
message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
@@ -148,7 +141,7 @@ function ApplicationStatusRoutes(): VNode {
}
return (
<Fragment>
- <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
+ <NotConnectedAppMenu title="Error" />
<NotificationCard
notification={{
message: i18n.str`Unexpected Error`,
@@ -161,6 +154,25 @@ function ApplicationStatusRoutes(): VNode {
);
}
+ const SUPPORTED_VERSION = "5:0:1"
+ if (!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}`,
+ }}
+ />
+ <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
+ </Fragment>
+
+ }
+
return (
<div class="has-navbar-fixed-top">
<ConfigContextProvider value={ctx}>
diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
index 277c2b176..46dea98e3 100644
--- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
@@ -22,7 +22,7 @@ import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
import { createHashHistory } from "history";
import { Fragment, h, VNode } from "preact";
import { Router, Route, route } from "preact-router";
-import { useState } from "preact/hooks";
+import { useEffect, useState } from "preact/hooks";
import {
NotificationCard,
NotYetReadyAppMenu,
@@ -35,52 +35,55 @@ import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
import { Settings } from "./paths/settings/index.js";
+/**
+ * Check if admin against /management/instances
+ * @returns
+ */
export function ApplicationReadyRoutes(): VNode {
const { i18n } = useTranslationContext();
+ const [unauthorized, setUnauthorized] = useState(false)
const {
url: backendURL,
- updateLoginStatus,
- clearAllTokens,
+ updateLoginStatus: updateLoginStatus2,
} = useBackendContext();
+ function updateLoginStatus(url: string, token: string | undefined) {
+ console.log("updateing", url, token)
+ updateLoginStatus2(url, token)
+ setUnauthorized(false)
+ }
+
const result = useBackendInstancesTestForAdmin();
const clearTokenAndGoToRoot = () => {
- clearAllTokens();
route("/");
};
const [showSettings, setShowSettings] = useState(false)
+ // useEffect(() => {
+ // setUnauthorized(FF)
+ // }, [FF])
+ const unauthorizedAdmin = !result.loading && !result.ok && result.type === ErrorType.CLIENT && result.status === HttpStatusCode.Unauthorized
if (showSettings) {
return <Fragment>
- <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} />
- <Settings/>
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
+ <Settings />
</Fragment>
}
- if (result.loading) return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." />;
- let admin = true;
- let instanceNameByBackendURL;
+ if (result.loading) {
+ return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." isPasswordOk={false} />;
+ }
- if (!result.ok) {
- if (
- result.type === ErrorType.CLIENT &&
- result.status === HttpStatusCode.Unauthorized
- ) {
- return (
- <Fragment>
- <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} />
- <NotificationCard
- notification={{
- message: i18n.str`Access denied`,
- description: i18n.str`Check your token is valid`,
- type: "ERROR",
- }}
- />
- <LoginPage onConfirm={updateLoginStatus} />
- </Fragment>
- );
- }
+ 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]) {
@@ -89,7 +92,7 @@ export function ApplicationReadyRoutes(): VNode {
// does not match our pattern
return (
<Fragment>
- <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} />
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
<NotificationCard
notification={{
message: i18n.str`Couldn't access the server.`,
@@ -102,10 +105,24 @@ export function ApplicationReadyRoutes(): VNode {
);
}
- admin = false;
instanceNameByBackendURL = match[1];
}
+ console.log(unauthorized, unauthorizedAdmin)
+ 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}>
@@ -113,6 +130,11 @@ export function ApplicationReadyRoutes(): VNode {
default
component={DefaultMainRoute}
admin={admin}
+ onUnauthorized={() => setUnauthorized(true)}
+ onLoginPass={() => {
+ console.log("ahora si")
+ setUnauthorized(false)
+ }}
instanceNameByBackendURL={instanceNameByBackendURL}
/>
</Router>
@@ -122,6 +144,8 @@ export function ApplicationReadyRoutes(): VNode {
function DefaultMainRoute({
instance,
admin,
+ onUnauthorized,
+ onLoginPass,
instanceNameByBackendURL,
url, //from preact-router
}: any): VNode {
@@ -133,6 +157,8 @@ function DefaultMainRoute({
<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/InstanceRoutes.tsx
index 1547442ea..4a4b3fee4 100644
--- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
@@ -40,6 +40,7 @@ import {
import { useInstanceKYCDetails } from "./hooks/instance.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 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";
@@ -47,6 +48,9 @@ import OrderListPage from "./paths/instance/orders/list/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";
@@ -58,6 +62,9 @@ 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/validators/create/index.js";
+import ValidatorListPage from "./paths/instance/validators/list/index.js";
+import ValidatorUpdatePage from "./paths/instance/validators/update/index.js";
import TransferCreatePage from "./paths/instance/transfers/create/index.js";
import TransferListPage from "./paths/instance/transfers/list/index.js";
import InstanceUpdatePage, {
@@ -69,11 +76,16 @@ import NotFoundPage from "./paths/notfound/index.js";
import { Notification } from "./utils/types.js";
import { MerchantBackend } from "./declaration.js";
import { Settings } from "./paths/settings/index.js";
+import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
export enum InstancePaths {
- // details = '/',
error = "/error",
- update = "/update",
+ server = "/server",
+ token = "/token",
+
+ bank_list = "/bank",
+ bank_update = "/bank/:bid/update",
+ bank_new = "/bank/new",
product_list = "/products",
product_update = "/product/:pid/update",
@@ -102,11 +114,15 @@ export enum InstancePaths {
webhooks_update = "/webhooks/:tid/update",
webhooks_new = "/webhooks/new",
- settings = "/settings",
+ validators_list = "/validators",
+ validators_update = "/validators/:vid/update",
+ validators_new = "/validators/new",
+
+ settings = "/inteface",
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
-const noop = () => {};
+const noop = () => { };
export enum AdminPaths {
list_instances = "/instances",
@@ -118,6 +134,8 @@ export interface Props {
id: string;
admin?: boolean;
path: string;
+ onUnauthorized: () => void;
+ onLoginPass: () => void;
setInstanceName: (s: string) => void;
}
@@ -125,40 +143,29 @@ export function InstanceRoutes({
id,
admin,
path,
+ onUnauthorized,
+ onLoginPass,
setInstanceName,
}: Props): VNode {
- const [_, updateDefaultToken] = useBackendDefaultToken();
+ const [defaultToken, updateDefaultToken] = useBackendDefaultToken();
const [token, updateToken] = useBackendInstanceToken(id);
- const {
- updateLoginStatus: changeBackend,
- addTokenCleaner,
- clearAllTokens,
- } = useBackendContext();
- const cleaner = useCallback(() => {
- updateToken(undefined);
- }, [id]);
const { i18n } = useTranslationContext();
type GlobalNotifState = (Notification & { to: string }) | undefined;
const [globalNotification, setGlobalNotification] =
useState<GlobalNotifState>(undefined);
- useEffect(() => {
- addTokenCleaner(cleaner);
- }, [addTokenCleaner, cleaner]);
-
const changeToken = (token?: string) => {
if (admin) {
updateToken(token);
} else {
updateDefaultToken(token);
}
+ onLoginPass()
};
- const updateLoginStatus = (url: string, token?: string) => {
- changeBackend(url);
- if (!token) return;
- changeToken(token);
- };
+ // const updateLoginStatus = (url: string, token?: string) => {
+ // changeToken(token);
+ // };
const value = useMemo(
() => ({ id, token, admin, changeToken }),
@@ -192,18 +199,17 @@ export function InstanceRoutes({
};
}
- const LoginPageAccessDenied = () => (
- <Fragment>
- <NotificationCard
- notification={{
- message: i18n.str`Access denied`,
- description: i18n.str`The access token provided is invalid.`,
- type: "ERROR",
- }}
- />
- <LoginPage onConfirm={updateLoginStatus} />
- </Fragment>
- );
+ // const LoginPageAccessDeniend = onUnauthorized
+ const LoginPageAccessDenied = () => {
+ onUnauthorized()
+ return <NotificationCard
+ notification={{
+ message: i18n.str`Access denied`,
+ description: i18n.str`Redirecting to login page.`,
+ type: "ERROR",
+ }}
+ />
+ }
function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
return function IfAdminCreateDefaultOrImpl(props?: T) {
@@ -234,8 +240,10 @@ export function InstanceRoutes({
}
const clearTokenAndGoToRoot = () => {
- clearAllTokens();
route("/");
+ // clear all tokens
+ updateToken(undefined)
+ updateDefaultToken(undefined)
};
return (
@@ -244,11 +252,12 @@ export function InstanceRoutes({
instance={id}
admin={admin}
onShowSettings={() => {
- route("/settings")
+ route("/inteface")
}}
path={path}
onLogout={clearTokenAndGoToRoot}
setInstanceName={setInstanceName}
+ isPasswordOk={defaultToken !== undefined}
/>
<KycBanner />
<NotificationCard notification={globalNotification} />
@@ -308,7 +317,7 @@ export function InstanceRoutes({
* Update instance page
*/}
<Route
- path={InstancePaths.update}
+ path={InstancePaths.server}
component={InstanceUpdatePage}
onBack={() => {
route(`/`);
@@ -322,13 +331,26 @@ export function InstanceRoutes({
onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
/>
{/**
+ * Update instance page
+ */}
+ <Route
+ path={InstancePaths.token}
+ component={TokenPage}
+ onChange={() => {
+ route(`/`);
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.error)}
+ />
+ {/**
* Product pages
*/}
<Route
path={InstancePaths.product_list}
component={ProductListPage}
onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onCreate={() => {
route(InstancePaths.product_new);
}}
@@ -361,6 +383,45 @@ export function InstanceRoutes({
}}
/>
{/**
+ * Bank pages
+ */}
+ <Route
+ path={InstancePaths.bank_list}
+ component={BankAccountListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
+ 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.product_list)}
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.bank_list);
+ }}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ />
+ <Route
+ path={InstancePaths.bank_new}
+ component={BankAccountCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.bank_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.bank_list);
+ }}
+ />
+ {/**
* Order pages
*/}
<Route
@@ -373,7 +434,7 @@ export function InstanceRoutes({
route(InstancePaths.order_details.replace(":oid", id));
}}
onUnauthorized={LoginPageAccessDenied}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
/>
<Route
@@ -389,8 +450,8 @@ export function InstanceRoutes({
<Route
path={InstancePaths.order_new}
component={OrderCreatePage}
- onConfirm={() => {
- route(InstancePaths.order_list);
+ onConfirm={(orderId: string) => {
+ route(InstancePaths.order_details.replace(":oid", orderId));
}}
onBack={() => {
route(InstancePaths.order_list);
@@ -404,7 +465,7 @@ export function InstanceRoutes({
component={TransferListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onCreate={() => {
route(InstancePaths.transfers_new);
}}
@@ -427,7 +488,7 @@ export function InstanceRoutes({
component={WebhookListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onCreate={() => {
route(InstancePaths.webhooks_new);
}}
@@ -459,6 +520,45 @@ export function InstanceRoutes({
}}
/>
{/**
+ * Validator pages
+ */}
+ <Route
+ path={InstancePaths.validators_list}
+ component={ValidatorListPage}
+ onUnauthorized={LoginPageAccessDenied}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
+ onCreate={() => {
+ route(InstancePaths.validators_new);
+ }}
+ onSelect={(id: string) => {
+ route(InstancePaths.validators_update.replace(":vid", id));
+ }}
+ />
+ <Route
+ path={InstancePaths.validators_update}
+ component={ValidatorUpdatePage}
+ onConfirm={() => {
+ route(InstancePaths.validators_list);
+ }}
+ onUnauthorized={LoginPageAccessDenied}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.validators_list)}
+ onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
+ onBack={() => {
+ route(InstancePaths.validators_list);
+ }}
+ />
+ <Route
+ path={InstancePaths.validators_new}
+ component={ValidatorCreatePage}
+ onConfirm={() => {
+ route(InstancePaths.validators_list);
+ }}
+ onBack={() => {
+ route(InstancePaths.validators_list);
+ }}
+ />
+ {/**
* Templates pages
*/}
<Route
@@ -466,7 +566,7 @@ export function InstanceRoutes({
component={TemplateListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onCreate={() => {
route(InstancePaths.templates_new);
}}
@@ -535,7 +635,7 @@ export function InstanceRoutes({
component={ReservesListPage}
onUnauthorized={LoginPageAccessDenied}
onNotFound={IfAdminCreateDefaultOr(NotFoundPage)}
- onLoadError={ServerErrorRedirectTo(InstancePaths.update)}
+ onLoadError={ServerErrorRedirectTo(InstancePaths.server)}
onSelect={(id: string) => {
route(InstancePaths.reserves_details.replace(":rid", id));
}}
@@ -590,7 +690,7 @@ function AdminInstanceUpdatePage({
const { updateLoginStatus: changeBackend } = useBackendContext();
const updateLoginStatus = (url: string, token?: string): void => {
changeBackend(url);
- if (token) changeToken(token);
+ changeToken(token);
};
const value = useMemo(
() => ({ id, token, admin: true, changeToken }),
@@ -607,20 +707,20 @@ function AdminInstanceUpdatePage({
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 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 ||
+ 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,
- };
+ ? error.payload.detail
+ : undefined,
+ type: "ERROR" as const,
+ };
return (
<Fragment>
<NotificationCard notification={notif} />
@@ -650,7 +750,8 @@ function AdminInstanceUpdatePage({
function KycBanner(): VNode {
const kycStatus = useInstanceKYCDetails();
const { i18n } = useTranslationContext();
- const today = format(new Date(), "yyyy-MM-dd");
+ const [settings] = useSettings();
+ const today = format(new Date(), dateFormatForSettings(settings));
const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide");
const hasBeenHidden = today === lastHide;
const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx
index f2f94a7c5..4fa440fc7 100644
--- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx
+++ b/packages/merchant-backoffice-ui/src/components/exception/login.tsx
@@ -93,7 +93,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
<input
class="input"
type="password"
- placeholder={"set new access token"}
+ placeholder={"current access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
@@ -186,7 +186,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
<input
class="input"
type="password"
- placeholder={"set new access token"}
+ placeholder={"current access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
index 1f41c3564..a398629dc 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx
@@ -20,16 +20,18 @@
*/
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
-import { h, VNode } from "preact";
+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";
export interface Props<T> extends InputProps<T> {
readonly?: boolean;
expand?: boolean;
//FIXME: create separated components InputDate and InputTimestamp
withTimestampSupport?: boolean;
+ side?: ComponentChildren;
}
export function InputDate<T>({
@@ -41,9 +43,11 @@ export function InputDate<T>({
tooltip,
expand,
withTimestampSupport,
+ side,
}: Props<keyof T>): VNode {
const [opened, setOpened] = useState(false);
const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
const { error, required, value, onChange } = useField<T>(name);
@@ -51,14 +55,14 @@ export function InputDate<T>({
if (!value) {
strValue = withTimestampSupport ? "unknown" : "";
} else if (value instanceof Date) {
- strValue = format(value, "yyyy/MM/dd");
+ strValue = format(value, dateFormatForSettings(settings));
} else if (value.t_s) {
strValue =
value.t_s === "never"
? withTimestampSupport
? "never"
: ""
- : format(new Date(value.t_s * 1000), "yyyy/MM/dd");
+ : format(new Date(value.t_s * 1000), dateFormatForSettings(settings));
}
return (
@@ -142,6 +146,7 @@ export function InputDate<T>({
</button>
</span>
)}
+ {side}
</div>
<DatePicker
opened={opened}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
index 8d324660e..5cd69a0b3 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx
@@ -18,9 +18,9 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { parsePaytoUri, PaytoUriGeneric, stringifyPaytoUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
-import { useCallback, useState } from "preact/hooks";
import { COUNTRY_TABLE } from "../../utils/constants.js";
import { undefinedIfEmpty } from "../../utils/table.js";
import { FormErrors, FormProvider } from "./FormProvider.js";
@@ -28,23 +28,23 @@ import { Input } from "./Input.js";
import { InputGroup } from "./InputGroup.js";
import { InputSelector } from "./InputSelector.js";
import { InputProps, useField } from "./useField.js";
-import { InputWithAddon } from "./InputWithAddon.js";
-import { MerchantBackend } from "../../declaration.js";
+import { useEffect, useState } from "preact/hooks";
export interface Props<T> extends InputProps<T> {
isValid?: (e: any) => boolean;
}
+// type Entity = PaytoUriGeneric
// https://datatracker.ietf.org/doc/html/rfc8905
type Entity = {
// iban, bitcoin, x-taler-bank. it defined the format
target: string;
// path1 if the first field to be used
- path1: string;
+ path1?: string;
// path2 if the second field to be used, optional
path2?: string;
- // options of the payto uri
- options: {
+ // params of the payto uri
+ params: {
"receiver-name"?: string;
sender?: string;
message?: string;
@@ -52,13 +52,6 @@ type Entity = {
instruction?: string;
[name: string]: string | undefined;
};
- auth: {
- type: "unset" | "basic" | "none";
- url?: string;
- username?: string;
- password?: string;
- repeat?: string;
- };
};
function isEthereumAddress(address: string) {
@@ -171,14 +164,10 @@ const targets = [
"bitcoin",
"ethereum",
];
-const accountAuthType = ["none", "basic"];
const noTargetValue = targets[0];
-const defaultTarget: Partial<Entity> = {
+const defaultTarget: Entity = {
target: noTargetValue,
- options: {},
- auth: {
- type: "unset" as const,
- },
+ params: {},
};
export function InputPaytoForm<T>({
@@ -187,110 +176,91 @@ export function InputPaytoForm<T>({
label,
tooltip,
}: Props<keyof T>): VNode {
- const { value: paytos, onChange, required } = useField<T>(name);
-
- const [value, valueHandler] = useState<Partial<Entity>>(defaultTarget);
+ const { value: initialValueStr, onChange } = useField<T>(name);
- let payToPath;
- if (value.target === "iban" && value.path1) {
- payToPath = `/${value.path1.toUpperCase()}`;
- } else if (value.path1) {
- if (value.path2) {
- payToPath = `/${value.path1}/${value.path2}`;
- } else {
- payToPath = `/${value.path1}`;
- }
+ 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 { i18n } = useTranslationContext();
+ const [value, setValue] = useState<Partial<Entity>>(initial)
- const ops = value.options ?? {};
- const url = tryUrl(`payto://${value.target}${payToPath}`);
- if (url) {
- Object.keys(ops).forEach((opt_key) => {
- const opt_value = ops[opt_key];
- if (opt_value) url.searchParams.set(opt_key, opt_value);
- });
- }
- const paytoURL = !url ? "" : url.href;
+ const { i18n } = useTranslationContext();
const errors: FormErrors<Entity> = {
target:
- value.target === noTargetValue && !paytos.length
+ value.target === noTargetValue
? i18n.str`required`
: undefined,
path1: !value.path1
? i18n.str`required`
: value.target === "iban"
- ? validateIBAN(value.path1, i18n)
- : value.target === "bitcoin"
- ? validateBitcoin(value.path1, i18n)
- : value.target === "ethereum"
- ? validateEthereum(value.path1, i18n)
- : undefined,
+ ? validateIBAN(value.path1, i18n)
+ : value.target === "bitcoin"
+ ? validateBitcoin(value.path1, i18n)
+ : value.target === "ethereum"
+ ? validateEthereum(value.path1, i18n)
+ : undefined,
path2:
value.target === "x-taler-bank"
? !value.path2
? i18n.str`required`
: undefined
: undefined,
- options: undefinedIfEmpty({
- "receiver-name": !value.options?.["receiver-name"]
+ params: undefinedIfEmpty({
+ "receiver-name": !value.params?.["receiver-name"]
? i18n.str`required`
: undefined,
}),
- auth: !value.auth
- ? undefined
- : undefinedIfEmpty({
- username:
- value.auth.type === "basic" && !value.auth.username
- ? i18n.str`required`
- : undefined,
- password:
- value.auth.type === "basic" && !value.auth.password
- ? i18n.str`required`
- : undefined,
- repeat:
- value.auth.type === "basic" && !value.auth.repeat
- ? i18n.str`required`
- : value.auth.repeat !== value.auth.password
- ? i18n.str`is not the same`
- : undefined,
- }),
};
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,
+ })
+ useEffect(() => {
+ onChange(str as any)
+ }, [str])
- const submit = useCallback((): void => {
- const accounts: MerchantBackend.Instances.MerchantBankAccount[] = paytos;
- const alreadyExists =
- accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
- if (!alreadyExists) {
- const newValue: MerchantBackend.Instances.MerchantBankAccount = {
- payto_uri: paytoURL,
- };
- if (value.auth) {
- if (value.auth.url) {
- newValue.credit_facade_url = value.auth.url;
- }
- if (value.auth.type === "none") {
- newValue.credit_facade_credentials = {
- type: "none",
- };
- }
- if (value.auth.type === "basic") {
- newValue.credit_facade_credentials = {
- type: "basic",
- username: value.auth.username ?? "",
- password: value.auth.password ?? "",
- };
- }
- }
- onChange([newValue, ...accounts] as any);
- }
- valueHandler(defaultTarget);
- }, [value]);
+ // const submit = useCallback((): void => {
+ // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos;
+ // // const alreadyExists =
+ // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1;
+ // // if (!alreadyExists) {
+ // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = {
+ // payto_uri: paytoURL,
+ // };
+ // if (value.auth) {
+ // if (value.auth.url) {
+ // newValue.credit_facade_url = value.auth.url;
+ // }
+ // if (value.auth.type === "none") {
+ // newValue.credit_facade_credentials = {
+ // type: "none",
+ // };
+ // }
+ // if (value.auth.type === "basic") {
+ // newValue.credit_facade_credentials = {
+ // type: "basic",
+ // username: value.auth.username ?? "",
+ // password: value.auth.password ?? "",
+ // };
+ // }
+ // }
+ // onChange(newValue as any);
+ // // }
+ // // valueHandler(defaultTarget);
+ // }, [value]);
//FIXME: translating plural singular
return (
@@ -299,11 +269,11 @@ export function InputPaytoForm<T>({
name="tax"
errors={errors}
object={value}
- valueHandler={valueHandler}
+ valueHandler={setValue}
>
<InputSelector<Entity>
name="target"
- label={i18n.str`Target type`}
+ label={i18n.str`Account type`}
tooltip={i18n.str`Method to use for wire transfer`}
values={targets}
toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)}
@@ -400,150 +370,15 @@ export function InputPaytoForm<T>({
{value.target !== noTargetValue && (
<Fragment>
<Input
- name="options.receiver-name"
+ name="params.receiver-name"
label={i18n.str`Name`}
tooltip={i18n.str`Bank account owner's name.`}
/>
- <InputWithAddon
- name="auth.url"
- label={i18n.str`Account info URL`}
- help="https://bank.com"
- expand
- tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
- />
- <InputSelector
- name="auth.type"
- label={i18n.str`Auth type`}
- tooltip={i18n.str`Choose the authentication type for the account info URL`}
- values={accountAuthType}
- toStr={(str) => {
- // if (str === "unset") {
- // return "Without change";
- // }
- if (str === "none") return "Without authentication";
- return "Username and password";
- }}
- />
- {value.auth?.type === "basic" ? (
- <Fragment>
- <Input
- name="auth.username"
- label={i18n.str`Username`}
- tooltip={i18n.str`Username to access the account information.`}
- />
- <Input
- name="auth.password"
- inputType="password"
- label={i18n.str`Password`}
- tooltip={i18n.str`Password to access the account information.`}
- />
- <Input
- name="auth.repeat"
- inputType="password"
- label={i18n.str`Repeat password`}
- />
- </Fragment>
- ) : undefined}
-
- {/* <InputWithAddon
- name="options.credit_credentials"
- label={i18n.str`Account info`}
- inputType={showKey ? "text" : "password"}
- help="From where the merchant can download information about incoming wire transfers to this account"
- expand
- tooltip={i18n.str`Useful to validate the purchase`}
- fromStr={(v) => v.toUpperCase()}
- addonAfter={
- <span class="icon">
- {showKey ? (
- <i class="mdi mdi-eye" />
- ) : (
- <i class="mdi mdi-eye-off" />
- )}
- </span>
- }
- side={
- <span style={{ display: "flex" }}>
- <button
- data-tooltip={
- showKey
- ? i18n.str`show secret key`
- : i18n.str`hide secret key`
- }
- class="button is-info mr-3"
- onClick={(e) => {
- setShowKey(!showKey);
- }}
- >
- {showKey ? (
- <i18n.Translate>hide</i18n.Translate>
- ) : (
- <i18n.Translate>show</i18n.Translate>
- )}
- </button>
- </span>
- }
- /> */}
</Fragment>
)}
- {/**
- * Show the values in the list
- */}
- <div class="field is-horizontal">
- <div class="field-label is-normal" />
- <div class="field-body" style={{ display: "block" }}>
- {paytos.map(
- (v: MerchantBackend.Instances.MerchantBankAccount, i: number) => (
- <div
- key={i}
- class="tags has-addons mt-3 mb-0 mr-3"
- style={{ flexWrap: "nowrap" }}
- >
- <span
- class="tag is-medium is-info mb-0"
- style={{ maxWidth: "90%" }}
- >
- {v.payto_uri}
- </span>
- <a
- class="tag is-medium is-danger is-delete mb-0"
- onClick={() => {
- onChange(paytos.filter((f: any) => f !== v) as any);
- }}
- />
- </div>
- ),
- )}
- {!paytos.length && i18n.str`No accounts yet.`}
- {required && (
- <span class="icon has-text-danger is-right">
- <i class="mdi mdi-alert" />
- </span>
- )}
- </div>
- </div>
- {value.target !== noTargetValue && (
- <div class="buttons is-right mt-5">
- <button
- class="button is-info"
- data-tooltip={i18n.str`add tax to the tax list`}
- disabled={hasErrors}
- onClick={submit}
- >
- <i18n.Translate>Add</i18n.Translate>
- </button>
- </div>
- )}
</FormProvider>
</InputGroup>
);
}
-function tryUrl(s: string): URL | undefined {
- try {
- return new URL(s);
- } catch (e) {
- return undefined;
- }
-}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
index 1c1fcb907..be5800d14 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx
@@ -22,32 +22,41 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import emptyImage from "../../assets/empty.png";
-import { MerchantBackend, WithId } from "../../declaration.js";
import { FormErrors, FormProvider } from "./FormProvider.js";
import { InputWithAddon } from "./InputWithAddon.js";
+import { TranslatedString } from "@gnu-taler/taler-util";
-type Entity = MerchantBackend.Products.ProductDetail & WithId;
+type Entity = {
+ id: string,
+ description: string;
+ image?: string;
+ extra?: string;
+};
-export interface Props {
- selected?: Entity;
- onChange: (p?: Entity) => void;
- products: (MerchantBackend.Products.ProductDetail & WithId)[];
+export interface Props<T extends Entity> {
+ selected?: T;
+ onChange: (p?: T) => void;
+ label: TranslatedString;
+ list: T[];
+ withImage?: boolean;
}
-interface ProductSearch {
+interface Search {
name: string;
}
-export function InputSearchProduct({
+export function InputSearchOnList<T extends Entity>({
selected,
onChange,
- products,
-}: Props): VNode {
- const [prodForm, setProdName] = useState<Partial<ProductSearch>>({
+ label,
+ list,
+ withImage,
+}: Props<T>): VNode {
+ const [nameForm, setNameForm] = useState<Partial<Search>>({
name: "",
});
- const errors: FormErrors<ProductSearch> = {
+ const errors: FormErrors<Search> = {
name: undefined,
};
const { i18n } = useTranslationContext();
@@ -55,15 +64,17 @@ export function InputSearchProduct({
if (selected) {
return (
<article class="media">
- <figure class="media-left">
- <p class="image is-128x128">
- <img src={selected.image ? selected.image : emptyImage} />
- </p>
- </figure>
+ {withImage &&
+ <figure class="media-left">
+ <p class="image is-128x128">
+ <img src={selected.image ? selected.image : emptyImage} />
+ </p>
+ </figure>
+ }
<div class="media-content">
<div class="content">
<p class="media-meta">
- <i18n.Translate>Product id</i18n.Translate>: <b>{selected.id}</b>
+ <i18n.Translate>ID</i18n.Translate>: <b>{selected.id}</b>
</p>
<p>
<i18n.Translate>Description</i18n.Translate>:{" "}
@@ -84,15 +95,15 @@ export function InputSearchProduct({
}
return (
- <FormProvider<ProductSearch>
+ <FormProvider<Search>
errors={errors}
- object={prodForm}
- valueHandler={setProdName}
+ object={nameForm}
+ valueHandler={setNameForm}
>
- <InputWithAddon<ProductSearch>
+ <InputWithAddon<Search>
name="name"
- label={i18n.str`Product`}
- tooltip={i18n.str`search products by it's description or id`}
+ label={label}
+ tooltip={i18n.str`enter description or id`}
addonAfter={
<span class="icon">
<i class="mdi mdi-magnify" />
@@ -100,13 +111,14 @@ export function InputSearchProduct({
}
>
<div>
- <ProductList
- name={prodForm.name}
- list={products}
+ <DropdownList
+ name={nameForm.name}
+ list={list}
onSelect={(p) => {
- setProdName({ name: "" });
+ setNameForm({ name: "" });
onChange(p);
}}
+ withImage={!!withImage}
/>
</div>
</InputWithAddon>
@@ -114,13 +126,14 @@ export function InputSearchProduct({
);
}
-interface ProductListProps {
+interface DropdownListProps<T extends Entity> {
name?: string;
- onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void;
- list: (MerchantBackend.Products.ProductDetail & WithId)[];
+ onSelect: (p: T) => void;
+ list: T[];
+ withImage: boolean;
}
-function ProductList({ name, onSelect, list }: ProductListProps) {
+function DropdownList<T extends Entity>({ name, onSelect, list, withImage }: DropdownListProps<T>) {
const { i18n } = useTranslationContext();
if (!name) {
/* FIXME
@@ -149,7 +162,7 @@ function ProductList({ name, onSelect, list }: ProductListProps) {
{!filtered.length ? (
<div class="dropdown-item">
<i18n.Translate>
- no products found with that description
+ no match found with that description or id
</i18n.Translate>
</div>
) : (
@@ -161,18 +174,20 @@ function ProductList({ name, onSelect, list }: ProductListProps) {
style={{ cursor: "pointer" }}
>
<article class="media">
- <div class="media-left">
- <div class="image" style={{ minWidth: 64 }}>
- <img
- src={p.image ? p.image : emptyImage}
- style={{ width: 64, height: 64 }}
- />
+ {withImage &&
+ <div class="media-left">
+ <div class="image" style={{ minWidth: 64 }}>
+ <img
+ src={p.image ? p.image : emptyImage}
+ style={{ width: 64, height: 64 }}
+ />
+ </div>
</div>
- </div>
+ }
<div class="media-content">
<div class="content">
<p>
- <strong>{p.id}</strong> <small>{p.price}</small>
+ <strong>{p.id}</strong> {p.extra !== undefined ? <small>{p.extra}</small> : undefined}
<br />
{p.description}
</p>
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
index 61ddf3c84..f95dfcd05 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
@@ -56,7 +56,7 @@ export function InputToggle<T>({
return (
<div class="field is-horizontal">
<div class="field-label is-normal">
- <label class="label" style={{ width: 200 }}>
+ <label class="label" >
{label}
{tooltip && (
<span class="icon has-tooltip-right" data-tooltip={tooltip}>
@@ -65,7 +65,7 @@ export function InputToggle<T>({
)}
</label>
</div>
- <div class="field-body is-flex-grow-1">
+ <div class="field-body is-flex-grow-3">
<div class="field">
<p class={expand ? "control is-expanded" : "control"}>
<label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index 24380ce98..b75dc83b3 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -24,14 +24,13 @@ import { Fragment, h, VNode } from "preact";
import { useBackendContext } from "../../context/backend.js";
import { Entity } from "../../paths/admin/create/CreatePage.js";
import { Input } from "../form/Input.js";
-import { InputCurrency } from "../form/InputCurrency.js";
import { InputDuration } from "../form/InputDuration.js";
import { InputGroup } from "../form/InputGroup.js";
import { InputImage } from "../form/InputImage.js";
import { InputLocation } from "../form/InputLocation.js";
-import { InputPaytoForm } from "../form/InputPaytoForm.js";
-import { InputWithAddon } from "../form/InputWithAddon.js";
import { InputSelector } from "../form/InputSelector.js";
+import { InputToggle } from "../form/InputToggle.js";
+import { InputWithAddon } from "../form/InputWithAddon.js";
export function DefaultInstanceFormFields({
readonlyId,
@@ -85,28 +84,10 @@ export function DefaultInstanceFormFields({
tooltip={i18n.str`Logo image.`}
/>
- <InputPaytoForm<Entity>
- name="accounts"
- label={i18n.str`Bank account`}
- tooltip={i18n.str`URI specifying bank account for crediting revenue.`}
- />
-
- <InputCurrency<Entity>
- name="default_max_deposit_fee"
- label={i18n.str`Default max deposit fee`}
- tooltip={i18n.str`Maximum deposit fees this merchant is willing to pay per order by default.`}
- />
-
- <InputCurrency<Entity>
- name="default_max_wire_fee"
- label={i18n.str`Default max wire fee`}
- tooltip={i18n.str`Maximum wire fees this merchant is willing to pay per wire transfer by default.`}
- />
-
- <Input<Entity>
- name="default_wire_fee_amortization"
- label={i18n.str`Default wire fee amortization`}
- tooltip={i18n.str`Number of orders excess wire transfer fees will be divided by to compute per order surcharge.`}
+ <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
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index f3cf80b92..be2f8dde5 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -25,6 +25,7 @@ import { useBackendContext } from "../../context/backend.js";
import { useConfigContext } from "../../context/config.js";
import { useInstanceKYCDetails } from "../../hooks/instance.js";
import { LangSelector } from "./LangSelector.js";
+import { useCredentialsChecker } from "../../hooks/backend.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -36,6 +37,7 @@ interface Props {
instance: string;
admin?: boolean;
mimic?: boolean;
+ isPasswordOk: boolean;
}
export function Sidebar({
@@ -45,6 +47,7 @@ export function Sidebar({
onLogout,
admin,
mimic,
+ isPasswordOk
}: Props): VNode {
const config = useConfigContext();
const backend = useBackendContext();
@@ -53,7 +56,7 @@ export function Sidebar({
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
return (
- <aside class="aside is-placed-left is-expanded">
+ <aside class="aside is-placed-left is-expanded" style={{ overflowY: "scroll" }}>
{mobile && (
<div
class="footer"
@@ -78,10 +81,10 @@ export function Sidebar({
</div>
</div>
<div class="menu is-menu-main">
- {instance ? (
+ {isPasswordOk && instance ? (
<Fragment>
<ul class="menu-list">
- <li>
+ <li>
<a href={"/orders"} class="has-icon">
<span class="icon">
<i class="mdi mdi-cash-register" />
@@ -104,7 +107,7 @@ export function Sidebar({
<li>
<a href={"/transfers"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-bank" />
+ <i class="mdi mdi-arrow-left-right" />
</span>
<span class="menu-item-label">
<i18n.Translate>Transfers</i18n.Translate>
@@ -137,12 +140,22 @@ export function Sidebar({
</p>
<ul class="menu-list">
<li>
- <a href={"/update"} class="has-icon">
+ <a href={"/bank"} class="has-icon">
<span class="icon">
- <i class="mdi mdi-square-edit-outline" />
+ <i class="mdi mdi-bank" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Bank account</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/validators"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-lock" />
</span>
<span class="menu-item-label">
- <i18n.Translate>Account</i18n.Translate>
+ <i18n.Translate>Validators</i18n.Translate>
</span>
</a>
</li>
@@ -164,6 +177,26 @@ export function Sidebar({
</span>
</a>
</li>
+ <li>
+ <a href={"/server"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-square-edit-outline" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Server</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a href={"/token"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-security" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Access token</i18n.Translate>
+ </span>
+ </a>
+ </li>
</ul>
</Fragment>
) : undefined}
@@ -174,12 +207,12 @@ export function Sidebar({
<li>
<a class="has-icon is-state-info is-hoverable"
onClick={(): void => onShowSettings()}
- >
+ >
<span class="icon">
<i class="mdi mdi-newspaper" />
</span>
<span class="menu-item-label">
- <i18n.Translate>Settings</i18n.Translate>
+ <i18n.Translate>Interface</i18n.Translate>
</span>
</a>
</li>
@@ -211,7 +244,7 @@ export function Sidebar({
</span>
</div>
</li>
- {admin && !mimic && (
+ {isPasswordOk && admin && !mimic && (
<Fragment>
<p class="menu-label">
<i18n.Translate>Instances</i18n.Translate>
@@ -238,19 +271,21 @@ export function Sidebar({
</li>
</Fragment>
)}
- <li>
- <a
- class="has-icon is-state-info is-hoverable"
- onClick={(): void => onLogout()}
- >
- <span class="icon">
- <i class="mdi mdi-logout default" />
- </span>
- <span class="menu-item-label">
- <i18n.Translate>Log out</i18n.Translate>
- </span>
- </a>
- </li>
+ {isPasswordOk &&
+ <li>
+ <a
+ class="has-icon is-state-info is-hoverable"
+ onClick={(): void => onLogout()}
+ >
+ <span class="icon">
+ <i class="mdi mdi-logout default" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Log out</i18n.Translate>
+ </span>
+ </a>
+ </li>
+ }
</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 cdbae4ae0..cb318906f 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -24,7 +24,7 @@ import { Sidebar } from "./SideBar.js";
function getInstanceTitle(path: string, id: string): string {
switch (path) {
- case InstancePaths.update:
+ case InstancePaths.server:
return `${id}: Settings`;
case InstancePaths.order_list:
return `${id}: Orders`;
@@ -50,6 +50,12 @@ function getInstanceTitle(path: string, id: string): string {
return `${id}: New webhook`;
case InstancePaths.webhooks_update:
return `${id}: Update webhook`;
+ case InstancePaths.validators_list:
+ return `${id}: Validators`;
+ case InstancePaths.validators_new:
+ return `${id}: New validator`;
+ case InstancePaths.validators_update:
+ return `${id}: Update validators`;
case InstancePaths.templates_new:
return `${id}: New template`;
case InstancePaths.templates_update:
@@ -58,6 +64,10 @@ function getInstanceTitle(path: string, id: string): string {
return `${id}: Templates`;
case InstancePaths.templates_use:
return `${id}: Use template`;
+ case InstancePaths.settings:
+ return `${id}: Interface`;
+ case InstancePaths.settings:
+ return `${id}: Interface`;
default:
return "";
}
@@ -77,6 +87,7 @@ interface MenuProps {
onLogout?: () => void;
onShowSettings: () => void;
setInstanceName: (s: string) => void;
+ isPasswordOk: boolean;
}
function WithTitle({
@@ -100,14 +111,15 @@ export function Menu({
path,
admin,
setInstanceName,
+ isPasswordOk
}: MenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
const titleWithSubtitle = title
? title
: !admin
- ? getInstanceTitle(path, instance)
- : getAdminTitle(path, instance);
+ ? getInstanceTitle(path, instance)
+ : getAdminTitle(path, instance);
const adminInstance = instance === "default";
const mimic = admin && !adminInstance;
return (
@@ -129,14 +141,15 @@ export function Menu({
mimic={mimic}
instance={instance}
mobile={mobileOpen}
+ isPasswordOk={isPasswordOk}
/>
)}
{mimic && (
<nav class="level" style={{
zIndex: 100,
- position:"fixed",
- width:"50%",
+ position: "fixed",
+ width: "50%",
marginLeft: "20%"
}}>
<div class="level-item has-text-centered has-background-warning">
@@ -161,8 +174,9 @@ export function Menu({
interface NotYetReadyAppMenuProps {
title: string;
- onLogout?: () => void;
onShowSettings: () => void;
+ onLogout?: () => void;
+ isPasswordOk: boolean;
}
interface NotifProps {
@@ -181,8 +195,8 @@ export function NotificationCard({
n.type === "ERROR"
? "message is-danger"
: n.type === "WARN"
- ? "message is-warning"
- : "message is-info"
+ ? "message is-warning"
+ : "message is-info"
}
>
<div class="message-header">
@@ -201,10 +215,36 @@ export function NotificationCard({
);
}
+interface NotConnectedAppMenuProps {
+ title: string;
+}
+export function NotConnectedAppMenu({
+ title,
+}: NotConnectedAppMenuProps): VNode {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ document.title = `Taler Backoffice: ${title}`;
+ }, [title]);
+
+ return (
+ <div
+ class={mobileOpen ? "has-aside-mobile-expanded" : ""}
+ onClick={() => setMobileOpen(false)}
+ >
+ <NavigationBar
+ onMobileMenu={() => setMobileOpen(!mobileOpen)}
+ title={title}
+ />
+ </div>
+ );
+}
+
export function NotYetReadyAppMenu({
onLogout,
onShowSettings,
title,
+ isPasswordOk
}: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
@@ -222,7 +262,7 @@ export function NotYetReadyAppMenu({
title={title}
/>
{onLogout && (
- <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} />
+ <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} isPasswordOk={isPasswordOk} />
)}
</div>
);
diff --git a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
index b2ec4dd11..377d9c1ba 100644
--- a/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/InventoryProductForm.tsx
@@ -20,7 +20,7 @@ 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 { InputSearchProduct } from "../form/InputSearchProduct.js";
+import { InputSearchOnList } from "../form/InputSearchOnList.js";
type Form = {
product: MerchantBackend.Products.ProductDetail & WithId;
@@ -95,10 +95,12 @@ export function InventoryProductForm({
return (
<FormProvider<Form> errors={errors} object={state} valueHandler={setState}>
- <InputSearchProduct
+ <InputSearchOnList
+ label={i18n.str`Search product`}
selected={state.product}
onChange={(p) => setState((v) => ({ ...v, product: p }))}
- products={inventory}
+ list={inventory}
+ withImage
/>
{state.product && (
<div class="columns mt-5">
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index 7956a9ea5..4cd90aa45 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -58,12 +58,12 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
!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> = {};
@@ -148,15 +148,17 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
name="minimum_age"
label={i18n.str`Age restricted`}
tooltip={i18n.str`is this product restricted for customer below certain age?`}
+ help={i18n.str`can be overriden by the order configuration`}
/>
<Input<Entity>
name="unit"
- label={i18n.str`Unit`}
+ label={i18n.str`Unit name`}
tooltip={i18n.str`unit describing quantity of product sold (e.g. 2 kilograms, 5 liters, 3 items, 5 meters) for customers`}
+ help={i18n.str`exajmple: kg, items or liters`}
/>
<InputCurrency<Entity>
name="price"
- label={i18n.str`Price`}
+ label={i18n.str`Price per unit`}
tooltip={i18n.str`sale price for customers, including taxes, for above units of the product`}
/>
<InputStock
diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts
index f7f8afea6..43e9e4d27 100644
--- a/packages/merchant-backoffice-ui/src/context/backend.ts
+++ b/packages/merchant-backoffice-ui/src/context/backend.ts
@@ -28,8 +28,8 @@ interface BackendContextType {
token?: string;
triedToLog: boolean;
resetBackend: () => void;
- clearAllTokens: () => void;
- addTokenCleaner: (c: () => void) => void;
+ // clearAllTokens: () => void;
+ // addTokenCleaner: (c: () => void) => void;
updateLoginStatus: (url: string, token?: string) => void;
updateToken: (token?: string) => void;
}
@@ -39,8 +39,8 @@ const BackendContext = createContext<BackendContextType>({
token: undefined,
triedToLog: false,
resetBackend: () => null,
- clearAllTokens: () => null,
- addTokenCleaner: () => null,
+ // clearAllTokens: () => null,
+ // addTokenCleaner: () => null,
updateLoginStatus: () => null,
updateToken: () => null,
});
@@ -56,30 +56,30 @@ function useBackendContextState(
_updateToken(t);
};
- const tokenCleaner = useCallback(() => {
- updateToken(undefined);
- }, []);
- const [cleaners, setCleaners] = useState([tokenCleaner]);
- const addTokenCleaner = (c: () => void) => setCleaners((cs) => [...cs, c]);
- const addTokenCleanerMemo = useCallback(
- (c: () => void) => {
- addTokenCleaner(c);
- },
- [tokenCleaner],
- );
+ // const tokenCleaner = useCallback(() => {
+ // updateToken(undefined);
+ // }, []);
+ // const [cleaners, setCleaners] = useState([tokenCleaner]);
+ // const addTokenCleaner = (c: () => void) => setCleaners((cs) => [...cs, c]);
+ // const addTokenCleanerMemo = useCallback(
+ // (c: () => void) => {
+ // addTokenCleaner(c);
+ // },
+ // [tokenCleaner],
+ // );
- const clearAllTokens = () => {
- cleaners.forEach((c) => c());
- for (let i = 0; i < localStorage.length; i++) {
- const k = localStorage.key(i);
- if (k && /^backend-token/.test(k)) localStorage.removeItem(k);
- }
- resetBackend();
- };
+ // const clearAllTokens = () => {
+ // cleaners.forEach((c) => c());
+ // for (let i = 0; i < localStorage.length; i++) {
+ // const k = localStorage.key(i);
+ // if (k && /^backend-token/.test(k)) localStorage.removeItem(k);
+ // }
+ // resetBackend();
+ // };
const updateLoginStatus = (url: string, token?: string) => {
changeBackend(url);
- if (token) updateToken(token);
+ updateToken(token);
};
return {
@@ -88,9 +88,9 @@ function useBackendContextState(
triedToLog,
updateLoginStatus,
resetBackend,
- clearAllTokens,
+ // clearAllTokens,
updateToken,
- addTokenCleaner: addTokenCleanerMemo,
+ // addTokenCleaner: addTokenCleanerMemo,
};
}
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index db3122266..5ca9c1e09 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -25,6 +25,8 @@ type EddsaSignature = string;
type WireTransferIdentifierRawP = string;
type RelativeTime = Duration;
type ImageDataUrl = string;
+type MerchantUserType = "business" | "individual";
+
export interface WithId {
id: string;
@@ -312,46 +314,8 @@ export namespace MerchantBackend {
// header.
token?: string;
}
- type FacadeCredentials = NoFacadeCredentials | BasicAuthFacadeCredentials;
-
- interface NoFacadeCredentials {
- type: "none";
- }
-
- interface BasicAuthFacadeCredentials {
- type: "basic";
-
- // Username to use to authenticate
- username: string;
-
- // Password to use to authenticate
- password: string;
- }
-
- interface MerchantBankAccount {
- // The payto:// URI where the wallet will send coins.
- payto_uri: string;
-
- // Optional base URL for a facade where the
- // merchant backend can see incoming wire
- // transfers to reconcile its accounting
- // with that of the exchange. Used by
- // taler-merchant-wirewatch.
- credit_facade_url?: string;
-
- // Credentials for accessing the credit facade.
- credit_facade_credentials?: FacadeCredentials;
- }
//POST /private/instances
interface InstanceConfigurationMessage {
- // Bank accounts of the merchant. A merchant may have
- // multiple accounts, thus this is an array. Note that by
- // removing accounts from this list the respective account is set to
- // inactive and thus unavailable for new contracts, but preserved
- // in the database as existing offers and contracts may still refer
- // to it.
- accounts: MerchantBankAccount[];
-
// Name of the merchant instance to create (will become $INSTANCE).
id: string;
@@ -361,12 +325,16 @@ export namespace MerchantBackend {
// Type of the user (business or individual).
// Defaults to 'business'. Should become mandatory field
// in the future, left as optional for API compatibility for now.
- user_type?: string;
+ user_type?: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
- email: string;
- website: string;
- // An optional base64-encoded logo image
- logo: ImageDataUrl;
+ // Merchant logo.
+ logo?: ImageDataUrl;
// "Authentication" header required to authorize management access the instance.
// Optional, if not given authentication will be disabled for
@@ -381,17 +349,10 @@ export namespace MerchantBackend {
// (to be put into contracts).
jurisdiction: Location;
- // Maximum wire fee this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_wire_fee: Amount;
-
- // Default factor for wire fee amortization calculations.
- // Can be overridden by the frontend on a per-order basis.
- default_wire_fee_amortization: Integer;
-
- // Maximum deposit fee (sum over all coins) this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_deposit_fee: Amount;
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
// If the frontend does NOT specify an execution date, how long should
// we tell the exchange to wait to aggregate transactions before
@@ -406,11 +367,6 @@ export namespace MerchantBackend {
// PATCH /private/instances/$INSTANCE
interface InstanceReconfigurationMessage {
- // Bank accounts of the merchant. A merchant may have
- // multiple accounts, thus this is an array. Note that removing
- // URIs from this list deactivates the specified accounts
- // (they will no longer be used for future contracts).
- accounts: MerchantBankAccount[];
// Merchant name corresponding to this instance.
name: string;
@@ -418,7 +374,16 @@ export namespace MerchantBackend {
// Type of the user (business or individual).
// Defaults to 'business'. Should become mandatory field
// in the future, left as optional for API compatibility for now.
- user_type?: string;
+ user_type?: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
// The merchant's physical address (to be put into contracts).
address: Location;
@@ -427,17 +392,10 @@ export namespace MerchantBackend {
// (to be put into contracts).
jurisdiction: Location;
- // Maximum wire fee this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_wire_fee: Amount;
-
- // Default factor for wire fee amortization calculations.
- // Can be overridden by the frontend on a per-order basis.
- default_wire_fee_amortization: Integer;
-
- // Maximum deposit fee (sum over all coins) this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_deposit_fee: Amount;
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
// If the frontend does NOT specify an execution date, how long should
// we tell the exchange to wait to aggregate transactions before
@@ -460,7 +418,14 @@ export namespace MerchantBackend {
// Merchant name corresponding to this instance.
name: string;
- deleted?: boolean;
+ // Type of the user ("business" or "individual").
+ user_type: MerchantUserType;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
// Merchant instance this response is about ($INSTANCE)
id: string;
@@ -472,8 +437,63 @@ export namespace MerchantBackend {
// specify the desired payment target in /order requests. Note that
// front-ends do not have to support wallets selecting payment targets.
payment_targets: string[];
+
+ // Has this instance been deleted (but not purged)?
+ deleted: boolean;
}
+ //GET /private/instances/$INSTANCE
+ interface QueryInstancesResponse {
+
+ // Merchant name corresponding to this instance.
+ name: string;
+ // Type of the user ("business" or "individual").
+ user_type: MerchantUserType;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+
+ // Authentication configuration.
+ // Does not contain the token when token auth is configured.
+ auth: {
+ method: "external" | "token";
+ };
+ }
+ // DELETE /private/instances/$INSTANCE
+ }
+
+ namespace KYC {
//GET /private/instances/$INSTANCE/kyc
interface AccountKycRedirects {
// Array of pending KYCs.
@@ -513,56 +533,76 @@ export namespace MerchantBackend {
exchange_http_status: number;
}
- //GET /private/instances/$INSTANCE
- interface QueryInstancesResponse {
- // The URI where the wallet will send coins. A merchant may have
- // multiple accounts, thus this is an array.
- accounts: MerchantAccount[];
+ }
- // Merchant name corresponding to this instance.
- name: string;
+ namespace BankAccounts {
- // Public key of the merchant/instance, in Crockford Base32 encoding.
- merchant_pub: EddsaPublicKey;
+ interface AccountAddDetails {
- // The merchant's physical address (to be put into contracts).
- address: Location;
+ // payto:// URI of the account.
+ payto_uri: string;
- // The jurisdiction under which the merchant conducts its business
- // (to be put into contracts).
- jurisdiction: Location;
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
- // Maximum wire fee this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_wire_fee: Amount;
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
- // Default factor for wire fee amortization calculations.
- // Can be overridden by the frontend on a per-order basis.
- default_wire_fee_amortization: Integer;
+ }
- // Maximum deposit fee (sum over all coins) this instance is willing to pay.
- // Can be overridden by the frontend on a per-order basis.
- default_max_deposit_fee: Amount;
+ type FacadeCredentials =
+ | NoFacadeCredentials
+ | BasicAuthFacadeCredentials;
- // If the frontend does NOT specify an execution date, how long should
- // we tell the exchange to wait to aggregate transactions before
- // executing the wire transfer? This delay is added to the current
- // time when we generate the advisory execution time for the exchange.
- default_wire_transfer_delay: RelativeTime;
+ interface NoFacadeCredentials {
+ type: "none";
+ }
- // If the frontend does NOT specify a payment deadline, how long should
- // offers we make be valid by default?
- default_pay_delay: RelativeTime;
+ interface BasicAuthFacadeCredentials {
+ type: "basic";
- // Authentication configuration.
- // Does not contain the token when token auth is configured.
- auth: {
- method: "external" | "token";
- token?: string;
- };
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+ }
+
+ interface AccountAddResponse {
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // Salt used to compute h_wire.
+ salt: HashCode;
+ }
+
+ interface AccountPatchDetails {
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
}
- interface MerchantAccount {
+
+ interface AccountsSummaryResponse {
+
+ // List of accounts that are known for the instance.
+ accounts: BankAccountEntry[];
+ }
+
+ interface BankAccountEntry {
// payto:// URI of the account.
payto_uri: string;
@@ -587,7 +627,6 @@ export namespace MerchantBackend {
active: boolean;
}
- // DELETE /private/instances/$INSTANCE
}
namespace Products {
@@ -957,6 +996,10 @@ export namespace MerchantBackend {
// high entropy to prevent adversarial claims (like it is
// if the backend auto-generates one). Default is 'true'.
create_token?: boolean;
+
+ // OTP device ID to associate with the order.
+ // This parameter is optional.
+ otp_id?: string;
}
type Order = MinimalOrderDetail | ContractTerms;
@@ -1031,9 +1074,9 @@ export namespace MerchantBackend {
}
}
- namespace Tips {
+ namespace Rewards {
// GET /private/reserves
- interface TippingReserveStatus {
+ interface RewardReserveStatus {
// Array of all known reserves (possibly empty!)
reserves: ReserveStatusEntry[];
}
@@ -1057,7 +1100,7 @@ export namespace MerchantBackend {
// Amount picked up so far.
pickup_amount: Amount;
- // Amount approved for tips that exceeds the pickup_amount.
+ // Amount approved for rewards that exceeds the pickup_amount.
committed_amount: Amount;
// Is this reserve active (false if it was deleted but not purged)
@@ -1068,7 +1111,7 @@ export namespace MerchantBackend {
// Amount that the merchant promises to put into the reserve
initial_balance: Amount;
- // Exchange the merchant intends to use for tipping
+ // Exchange the merchant intends to use for reward
exchange_url: string;
// Desired wire method, for example "iban" or "x-taler-bank"
@@ -1081,30 +1124,30 @@ export namespace MerchantBackend {
// Wire accounts of the exchange where to transfer the funds.
accounts: WireAccount[];
}
- interface TipCreateRequest {
- // Amount that the customer should be tipped
+ interface RewardCreateRequest {
+ // Amount that the customer should be reward
amount: Amount;
- // Justification for giving the tip
+ // Justification for giving the reward
justification: string;
- // URL that the user should be directed to after tipping,
- // will be included in the tip_token.
+ // URL that the user should be directed to after rewarding,
+ // will be included in the reward_token.
next_url: string;
}
- interface TipCreateConfirmation {
- // Unique tip identifier for the tip that was created.
- tip_id: HashCode;
+ interface RewardCreateConfirmation {
+ // Unique reward identifier for the reward that was created.
+ reward_id: HashCode;
- // taler://tip URI for the tip
- taler_tip_uri: string;
+ // taler://reward URI for the reward
+ taler_reward_uri: string;
// URL that will directly trigger processing
- // the tip when the browser is redirected to it
- tip_status_url: string;
+ // the reward when the browser is redirected to it
+ reward_status_url: string;
- // when does the tip expire
- tip_expiration: Timestamp;
+ // when does the reward expire
+ reward_expiration: Timestamp;
}
interface ReserveDetail {
@@ -1124,12 +1167,12 @@ export namespace MerchantBackend {
// Amount picked up so far.
pickup_amount: Amount;
- // Amount approved for tips that exceeds the pickup_amount.
+ // Amount approved for rewards that exceeds the pickup_amount.
committed_amount: Amount;
- // Array of all tips created by this reserves (possibly empty!).
+ // Array of all rewards created by this reserves (possibly empty!).
// Only present if asked for explicitly.
- tips?: TipStatusEntry[];
+ rewards?: RewardStatusEntry[];
// Is this reserve active (false if it was deleted but not purged)?
active: boolean;
@@ -1144,31 +1187,31 @@ export namespace MerchantBackend {
exchange_url: string;
}
- interface TipStatusEntry {
- // Unique identifier for the tip.
- tip_id: HashCode;
+ interface RewardStatusEntry {
+ // Unique identifier for the reward.
+ reward_id: HashCode;
- // Total amount of the tip that can be withdrawn.
+ // Total amount of the reward that can be withdrawn.
total_amount: Amount;
- // Human-readable reason for why the tip was granted.
+ // Human-readable reason for why the reward was granted.
reason: string;
}
- interface TipDetails {
- // Amount that we authorized for this tip.
+ interface RewardDetails {
+ // Amount that we authorized for this reward.
total_authorized: Amount;
// Amount that was picked up by the user already.
total_picked_up: Amount;
- // Human-readable reason given when authorizing the tip.
+ // Human-readable reason given when authorizing the reward.
reason: string;
- // Timestamp indicating when the tip is set to expire (may be in the past).
+ // Timestamp indicating when the reward is set to expire (may be in the past).
expiration: Timestamp;
- // Reserve public key from which the tip is funded.
+ // Reserve public key from which the reward is funded.
reserve_pub: EddsaPublicKey;
// Array showing the pickup operations of the wallet (possibly empty!).
@@ -1239,6 +1282,63 @@ export namespace MerchantBackend {
}
}
+ namespace OTP {
+ interface OtpDeviceAddDetails {
+ // Device ID to use.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ otp_description: string;
+
+ // A base64-encoded key
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ interface OtpDevicePatchDetails {
+ // Human-readable description for the device.
+ otp_description: string;
+
+ // A base64-encoded key
+ otp_key: string | undefined;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ interface OtpDeviceSummaryResponse {
+ // Array of devices that are present in our backend.
+ otp_devices: OtpDeviceEntry[];
+ }
+ interface OtpDeviceEntry {
+ // Device identifier.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ device_description: string;
+ }
+
+ interface OtpDeviceDetails {
+ // Human-readable description for the device.
+ device_description: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+
+ }
namespace Template {
interface TemplateAddDetails {
// Template ID to use.
@@ -1247,12 +1347,9 @@ export namespace MerchantBackend {
// Human-readable description for the template.
template_description: string;
- // A base64-encoded key of the point-of-sale.
+ // OTP device ID.
// This parameter is optional.
- pos_key?: string;
-
- // Algorithm for computing the POS confirmation, 0 for none.
- pos_algorithm?: number;
+ otp_id?: string;
// Additional information in a separate template.
template_contract: TemplateContractDetails;
@@ -1276,12 +1373,9 @@ export namespace MerchantBackend {
// Human-readable description for the template.
template_description: string;
- // A base64-encoded key of the point-of-sale.
+ // OTP device ID.
// This parameter is optional.
- pos_key?: string;
-
- // Algorithm for computing the POS confirmation, 0 for none.
- pos_algorithm?: Integer;
+ otp_id?: string;
// Additional information in a separate template.
template_contract: TemplateContractDetails;
@@ -1304,12 +1398,9 @@ export namespace MerchantBackend {
// Human-readable description for the template.
template_description: string;
- // A base64-encoded key of the point-of-sale.
+ // OTP device ID.
// This parameter is optional.
- pos_key?: string;
-
- // Algorithm for computing the POS confirmation, 0 for none.
- pos_algorithm?: Integer;
+ otp_id?: string;
// Additional information in a separate template.
template_contract: TemplateContractDetails;
@@ -1424,21 +1515,6 @@ export namespace MerchantBackend {
// Maximum total deposit fee accepted by the merchant for this contract
max_fee: Amount;
- // Maximum wire fee accepted by the merchant (customer share to be
- // divided by the 'wire_fee_amortization' factor, and further reduced
- // if deposit fees are below 'max_fee'). Default if missing is zero.
- max_wire_fee: Amount;
-
- // Over how many customer transactions does the merchant expect to
- // amortize wire fees on average? If the exchange's wire fee is
- // above 'max_wire_fee', the difference is divided by this number
- // to compute the expected customer's contribution to the wire fee.
- // The customer's contribution may further be reduced by the difference
- // between the 'max_fee' and the sum of the actual deposit fees.
- // Optional, default value if missing is 1. 0 and negative values are
- // invalid and also interpreted as 1.
- wire_fee_amortization: number;
-
// List of products that are part of the purchase (see Product).
products: Product[];
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts
index 145a366f6..ecd34df6d 100644
--- a/packages/merchant-backoffice-ui/src/hooks/backend.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts
@@ -33,8 +33,9 @@ import {
} from "@gnu-taler/web-util/browser";
import { useApiContext } from "@gnu-taler/web-util/browser";
+
export function useMatchMutate(): (
- re: RegExp,
+ re?: RegExp,
value?: unknown,
) => Promise<any> {
const { cache, mutate } = useSWRConfig();
@@ -45,13 +46,19 @@ export function useMatchMutate(): (
);
}
- return function matchRegexMutate(re: RegExp, value?: unknown) {
- const allKeys = Array.from(cache.keys());
- const keys = allKeys.filter((key) => re.test(key));
- const mutations = keys.map((key) => {
- return mutate(key, value, true);
+ 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,
});
- return Promise.all(mutations);
};
}
@@ -106,32 +113,32 @@ interface useBackendInstanceRequestType {
) => Promise<HttpResponseOk<T>>;
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
reserveDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
- tipsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
- multiFetcher: <T>(url: string[]) => Promise<HttpResponseOk<T>[]>;
+ rewardsDetailFetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
+ multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>;
orderFetcher: <T>(
- endpoint: string,
- paid?: YesOrNo,
- refunded?: YesOrNo,
- wired?: YesOrNo,
- searchDate?: Date,
- delta?: number,
+ params: [endpoint: string,
+ paid?: YesOrNo,
+ refunded?: YesOrNo,
+ wired?: YesOrNo,
+ searchDate?: Date,
+ delta?: number,]
) => Promise<HttpResponseOk<T>>;
transferFetcher: <T>(
- endpoint: string,
- payto_uri?: string,
- verified?: string,
- position?: string,
- delta?: number,
+ params: [endpoint: string,
+ payto_uri?: string,
+ verified?: string,
+ position?: string,
+ delta?: number,]
) => Promise<HttpResponseOk<T>>;
templateFetcher: <T>(
- endpoint: string,
- position?: string,
- delta?: number,
+ params: [endpoint: string,
+ position?: string,
+ delta?: number]
) => Promise<HttpResponseOk<T>>;
webhookFetcher: <T>(
- endpoint: string,
- position?: string,
- delta?: number,
+ params: [endpoint: string,
+ position?: string,
+ delta?: number]
) => Promise<HttpResponseOk<T>>;
}
interface useBackendBaseRequestType {
@@ -147,7 +154,7 @@ export function useCredentialsChecker() {
const { request } = useApiContext();
//check against instance details endpoint
//while merchant backend doesn't have a login endpoint
- return async function testLogin(
+ async function testLogin(
instance: string,
token: string,
): Promise<{
@@ -167,6 +174,7 @@ export function useCredentialsChecker() {
return { valid: false, cause: ErrorType.UNEXPECTED };
}
};
+ return testLogin
}
/**
@@ -212,8 +220,9 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const multiFetcher = useCallback(
function multiFetcherImpl<T>(
- endpoints: string[],
+ args: [endpoints: string[]],
): Promise<HttpResponseOk<T>[]> {
+ const [endpoints] = args
return Promise.all(
endpoints.map((endpoint) =>
requestHandler<T>(baseUrl, endpoint, { token }),
@@ -232,13 +241,14 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const orderFetcher = useCallback(
function orderFetcherImpl<T>(
- endpoint: string,
- paid?: YesOrNo,
- refunded?: YesOrNo,
- wired?: YesOrNo,
- searchDate?: Date,
- delta?: number,
+ 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
? (searchDate.getTime() / 1000) + 1
@@ -260,7 +270,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
params: {
- tips: "yes",
+ rewards: "yes",
},
token,
});
@@ -268,8 +278,8 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
[baseUrl, token],
);
- const tipsDetailFetcher = useCallback(
- function tipsDetailFetcherImpl<T>(
+ const rewardsDetailFetcher = useCallback(
+ function rewardsDetailFetcherImpl<T>(
endpoint: string,
): Promise<HttpResponseOk<T>> {
return requestHandler<T>(baseUrl, endpoint, {
@@ -284,12 +294,13 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const transferFetcher = useCallback(
function transferFetcherImpl<T>(
- endpoint: string,
- payto_uri?: string,
- verified?: string,
- position?: string,
- delta?: number,
+ 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;
@@ -305,10 +316,11 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const templateFetcher = useCallback(
function templateFetcherImpl<T>(
- endpoint: string,
- position?: string,
- delta?: number,
+ args: [endpoint: string,
+ position?: string,
+ delta?: number,]
): Promise<HttpResponseOk<T>> {
+ const [endpoint, position, delta] = args
const params: any = {};
if (delta !== undefined) {
params.limit = delta;
@@ -322,10 +334,11 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const webhookFetcher = useCallback(
function webhookFetcherImpl<T>(
- endpoint: string,
- position?: string,
- delta?: number,
+ args: [endpoint: string,
+ position?: string,
+ delta?: number,]
): Promise<HttpResponseOk<T>> {
+ const [endpoint, position, delta] = args
const params: any = {};
if (delta !== undefined) {
params.limit = delta;
@@ -343,7 +356,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
multiFetcher,
orderFetcher,
reserveDetailFetcher,
- tipsDetailFetcher,
+ rewardsDetailFetcher,
transferFetcher,
templateFetcher,
webhookFetcher,
diff --git a/packages/merchant-backoffice-ui/src/hooks/bank.ts b/packages/merchant-backoffice-ui/src/hooks/bank.ts
new file mode 100644
index 000000000..03b064646
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/bank.ts
@@ -0,0 +1,217 @@
+/*
+ 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,
+ 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";
+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 function useBankAccountAPI(): BankAccountAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ 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 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,
+ });
+ 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;
+ };
+
+ return {
+ createBankAccount,
+ updateBankAccount,
+ deleteBankAccount,
+ };
+}
+
+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>>;
+}
+
+export interface InstanceBankAccountFilter {
+}
+
+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 };
+ }
+ 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, 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 };
+}
diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts
index b77b9dea8..79b22304a 100644
--- a/packages/merchant-backoffice-ui/src/hooks/index.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/index.ts
@@ -19,9 +19,10 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { StateUpdater, useCallback, useState } from "preact/hooks";
+import { StateUpdater, useCallback, useEffect, useState } from "preact/hooks";
import { ValueOrFunction } from "../utils/types.js";
import { useMemoryStorage } from "@gnu-taler/web-util/browser";
+import { useMatchMutate } from "./backend.js";
const calculateRootPath = () => {
const rootPath =
@@ -56,8 +57,22 @@ export function useBackendDefaultToken(
): [string | undefined, ((d: string | undefined) => void)] {
// uncomment for testing
initialValue = "secret-token:secret" as string | undefined
- const { update, value } = useMemoryStorage(`backend-token`, initialValue)
- return [value, update];
+ const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token`, initialValue)
+ const clearCache = useMatchMutate()
+ useEffect(() => {
+ clearCache()
+ }, [token])
+
+ function updateToken(
+ value: (string | undefined)
+ ): void {
+ if (value === undefined) {
+ reset()
+ } else {
+ setToken(value)
+ }
+ }
+ return [token, updateToken];
}
export function useBackendInstanceToken(
@@ -73,14 +88,12 @@ export function useBackendInstanceToken(
function updateToken(
value: (string | undefined)
): void {
- console.log("seeting token", value)
if (value === undefined) {
reset()
} else {
setToken(value)
}
}
- console.log("token", token)
return [token, updateToken];
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
index f78de85dd..d15b3f6d7 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
@@ -113,7 +113,7 @@ describe("instance api interaction with details", () => {
name: "instance_name",
auth: {
method: "token",
- token: "not-secret",
+ // token: "not-secret",
},
} as MerchantBackend.Instances.QueryInstancesResponse,
});
@@ -154,7 +154,7 @@ describe("instance api interaction with details", () => {
name: "instance_name",
auth: {
method: "token",
- token: "secret",
+ // token: "secret",
},
} as MerchantBackend.Instances.QueryInstancesResponse,
});
@@ -190,7 +190,7 @@ describe("instance api interaction with details", () => {
name: "instance_name",
auth: {
method: "token",
- token: "not-secret",
+ // token: "not-secret",
},
} as MerchantBackend.Instances.QueryInstancesResponse,
});
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
index eae65d64c..32ed30c6f 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -198,6 +198,7 @@ export function useInstanceDetails(): HttpResponse<
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
+ revalidateIfStale: false,
errorRetryCount: 0,
errorRetryInterval: 1,
shouldRetryOnError: false,
@@ -211,7 +212,7 @@ export function useInstanceDetails(): HttpResponse<
type KYCStatus =
| { type: "ok" }
- | { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects };
+ | { type: "redirect"; status: MerchantBackend.KYC.AccountKycRedirects };
export function useInstanceKYCDetails(): HttpResponse<
KYCStatus,
@@ -220,7 +221,7 @@ export function useInstanceKYCDetails(): HttpResponse<
const { fetcher } = useBackendInstanceRequest();
const { data, error } = useSWR<
- HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>,
+ HttpResponseOk<MerchantBackend.KYC.AccountKycRedirects>,
RequestError<MerchantBackend.ErrorDetail>
>([`/private/kyc`], fetcher, {
refreshInterval: 60 * 1000,
diff --git a/packages/merchant-backoffice-ui/src/hooks/otp.ts b/packages/merchant-backoffice-ui/src/hooks/otp.ts
new file mode 100644
index 000000000..3544b4881
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/otp.ts
@@ -0,0 +1,223 @@
+/*
+ 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,
+ 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";
+const useSWR = _useSWR as unknown as SWRHook;
+
+const MOCKED_DEVICES: Record<string, MerchantBackend.OTP.OtpDeviceAddDetails> = {
+ "1": {
+ otp_description: "first device",
+ otp_algorithm: 1,
+ otp_device_id: "1",
+ otp_key: "123",
+ },
+ "2": {
+ otp_description: "second device",
+ otp_algorithm: 0,
+ otp_device_id: "2",
+ otp_key: "456",
+ }
+}
+
+export function useOtpDeviceAPI(): OtpDeviceAPI {
+ const mutateAll = useMatchMutate();
+ const { request } = useBackendInstanceRequest();
+
+ 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,
+ });
+ 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 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;
+ };
+
+ return {
+ createOtpDevice,
+ updateOtpDevice,
+ deleteOtpDevice,
+ };
+}
+
+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 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 = false;
+
+ 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 };
+ }
+ 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, 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 };
+}
diff --git a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts
index d2831ecff..b3eecd754 100644
--- a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts
@@ -25,16 +25,16 @@ import {
useInstanceReserves,
useReserveDetails,
useReservesAPI,
- useTipDetails,
+ useRewardDetails,
} from "./reserves.js";
import { ApiMockEnvironment } from "./testing.js";
import {
- API_AUTHORIZE_TIP,
- API_AUTHORIZE_TIP_FOR_RESERVE,
+ API_AUTHORIZE_REWARD,
+ API_AUTHORIZE_REWARD_FOR_RESERVE,
API_CREATE_RESERVE,
API_DELETE_RESERVE,
API_GET_RESERVE_BY_ID,
- API_GET_TIP_BY_ID,
+ API_GET_REWARD_BY_ID,
API_LIST_RESERVES,
} from "./urls.js";
import * as tests from "@gnu-taler/web-util/testing";
@@ -48,7 +48,7 @@ describe("reserve api interaction with listing", () => {
reserves: [
{
reserve_pub: "11",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
],
},
});
@@ -89,10 +89,10 @@ describe("reserve api interaction with listing", () => {
reserves: [
{
reserve_pub: "11",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
{
reserve_pub: "22",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
],
},
});
@@ -115,10 +115,10 @@ describe("reserve api interaction with listing", () => {
reserves: [
{
reserve_pub: "11",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
{
reserve_pub: "22",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
],
});
},
@@ -138,13 +138,13 @@ describe("reserve api interaction with listing", () => {
reserves: [
{
reserve_pub: "11",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
{
reserve_pub: "22",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
{
reserve_pub: "33",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
],
},
});
@@ -182,10 +182,10 @@ describe("reserve api interaction with listing", () => {
reserves: [
{
reserve_pub: "22",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
{
reserve_pub: "33",
- } as MerchantBackend.Tips.ReserveStatusEntry,
+ } as MerchantBackend.Rewards.ReserveStatusEntry,
],
},
});
@@ -213,16 +213,16 @@ describe("reserve api interaction with listing", () => {
});
describe("reserve api interaction with details", () => {
- it("should evict cache when adding a tip for a specific reserve", async () => {
+ 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" }],
- tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
- } as MerchantBackend.Tips.ReserveDetail,
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ } as MerchantBackend.Rewards.ReserveDetail,
qparam: {
- tips: "yes",
+ rewards: "yes",
},
});
@@ -246,37 +246,37 @@ describe("reserve api interaction with details", () => {
if (!query.ok) return;
expect(query.data).deep.equals({
accounts: [{ payto_uri: "payto://here" }],
- tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
});
- env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), {
+ env.addRequestExpectation(API_AUTHORIZE_REWARD_FOR_RESERVE("11"), {
request: {
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
},
response: {
- tip_id: "id2",
- taler_tip_uri: "uri",
- tip_expiration: { t_s: 1 },
- tip_status_url: "url",
+ 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" }],
- tips: [
- { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
- { reason: "not", tip_id: "id2", total_amount: "USD:12" },
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
],
- } as MerchantBackend.Tips.ReserveDetail,
+ } as MerchantBackend.Rewards.ReserveDetail,
qparam: {
- tips: "yes",
+ rewards: "yes",
},
});
- api.authorizeTipReserve("11", {
+ api.authorizeRewardReserve("11", {
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
@@ -294,9 +294,9 @@ describe("reserve api interaction with details", () => {
expect(query.data).deep.equals({
accounts: [{ payto_uri: "payto://here" }],
- tips: [
- { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
- { reason: "not", tip_id: "id2", total_amount: "USD:12" },
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
],
});
},
@@ -308,16 +308,16 @@ describe("reserve api interaction with details", () => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
- it("should evict cache when adding a tip for a random reserve", async () => {
+ 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" }],
- tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
- } as MerchantBackend.Tips.ReserveDetail,
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
+ } as MerchantBackend.Rewards.ReserveDetail,
qparam: {
- tips: "yes",
+ rewards: "yes",
},
});
@@ -341,37 +341,37 @@ describe("reserve api interaction with details", () => {
if (!query.ok) return;
expect(query.data).deep.equals({
accounts: [{ payto_uri: "payto://here" }],
- tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
+ rewards: [{ reason: "why?", reward_id: "id1", total_amount: "USD:10" }],
});
- env.addRequestExpectation(API_AUTHORIZE_TIP, {
+ env.addRequestExpectation(API_AUTHORIZE_REWARD, {
request: {
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
},
response: {
- tip_id: "id2",
- taler_tip_uri: "uri",
- tip_expiration: { t_s: 1 },
- tip_status_url: "url",
+ 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" }],
- tips: [
- { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
- { reason: "not", tip_id: "id2", total_amount: "USD:12" },
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
],
- } as MerchantBackend.Tips.ReserveDetail,
+ } as MerchantBackend.Rewards.ReserveDetail,
qparam: {
- tips: "yes",
+ rewards: "yes",
},
});
- api.authorizeTip({
+ api.authorizeReward({
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
@@ -387,9 +387,9 @@ describe("reserve api interaction with details", () => {
expect(query.data).deep.equals({
accounts: [{ payto_uri: "payto://here" }],
- tips: [
- { reason: "why?", tip_id: "id1", total_amount: "USD:10" },
- { reason: "not", tip_id: "id2", total_amount: "USD:12" },
+ rewards: [
+ { reason: "why?", reward_id: "id1", total_amount: "USD:10" },
+ { reason: "not", reward_id: "id2", total_amount: "USD:12" },
],
});
},
@@ -402,15 +402,15 @@ describe("reserve api interaction with details", () => {
});
});
-describe("reserve api interaction with tip details", () => {
- it("should list tips", async () => {
+describe("reserve api interaction with reward details", () => {
+ it("should list rewards", async () => {
const env = new ApiMockEnvironment();
- env.addRequestExpectation(API_GET_TIP_BY_ID("11"), {
+ env.addRequestExpectation(API_GET_REWARD_BY_ID("11"), {
response: {
total_picked_up: "USD:12",
reason: "not",
- } as MerchantBackend.Tips.TipDetails,
+ } as MerchantBackend.Rewards.RewardDetails,
qparam: {
pickups: "yes",
},
@@ -418,7 +418,7 @@ describe("reserve api interaction with tip details", () => {
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
- const query = useTipDetails("11");
+ const query = useRewardDetails("11");
return { query };
},
{},
diff --git a/packages/merchant-backoffice-ui/src/hooks/reserves.ts b/packages/merchant-backoffice-ui/src/hooks/reserves.ts
index bb55b2474..b719bfbe6 100644
--- a/packages/merchant-backoffice-ui/src/hooks/reserves.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/reserves.ts
@@ -31,11 +31,11 @@ export function useReservesAPI(): ReserveMutateAPI {
const { request } = useBackendInstanceRequest();
const createReserve = async (
- data: MerchantBackend.Tips.ReserveCreateRequest,
+ data: MerchantBackend.Rewards.ReserveCreateRequest,
): Promise<
- HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>
+ HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>
> => {
- const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>(
+ const res = await request<MerchantBackend.Rewards.ReserveCreateConfirmation>(
`/private/reserves`,
{
method: "POST",
@@ -49,12 +49,12 @@ export function useReservesAPI(): ReserveMutateAPI {
return res;
};
- const authorizeTipReserve = async (
+ const authorizeRewardReserve = async (
pub: string,
- data: MerchantBackend.Tips.TipCreateRequest,
- ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
- const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(
- `/private/reserves/${pub}/authorize-tip`,
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => {
+ const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>(
+ `/private/reserves/${pub}/authorize-reward`,
{
method: "POST",
data,
@@ -67,11 +67,11 @@ export function useReservesAPI(): ReserveMutateAPI {
return res;
};
- const authorizeTip = async (
- data: MerchantBackend.Tips.TipCreateRequest,
- ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
- const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(
- `/private/tips`,
+ const authorizeReward = async (
+ data: MerchantBackend.Rewards.RewardCreateRequest,
+ ): Promise<HttpResponseOk<MerchantBackend.Rewards.RewardCreateConfirmation>> => {
+ const res = await request<MerchantBackend.Rewards.RewardCreateConfirmation>(
+ `/private/rewards`,
{
method: "POST",
data,
@@ -97,33 +97,33 @@ export function useReservesAPI(): ReserveMutateAPI {
return res;
};
- return { createReserve, authorizeTip, authorizeTipReserve, deleteReserve };
+ return { createReserve, authorizeReward, authorizeRewardReserve, deleteReserve };
}
export interface ReserveMutateAPI {
createReserve: (
- data: MerchantBackend.Tips.ReserveCreateRequest,
- ) => Promise<HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>>;
- authorizeTipReserve: (
+ data: MerchantBackend.Rewards.ReserveCreateRequest,
+ ) => Promise<HttpResponseOk<MerchantBackend.Rewards.ReserveCreateConfirmation>>;
+ authorizeRewardReserve: (
id: string,
- data: MerchantBackend.Tips.TipCreateRequest,
- ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
- authorizeTip: (
- data: MerchantBackend.Tips.TipCreateRequest,
- ) => Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>>;
+ 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.Tips.TippingReserveStatus,
+ MerchantBackend.Rewards.RewardReserveStatus,
MerchantBackend.ErrorDetail
> {
const { fetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>,
+ HttpResponseOk<MerchantBackend.Rewards.RewardReserveStatus>,
RequestError<MerchantBackend.ErrorDetail>
>([`/private/reserves`], fetcher);
@@ -136,13 +136,13 @@ export function useInstanceReserves(): HttpResponse<
export function useReserveDetails(
reserveId: string,
): HttpResponse<
- MerchantBackend.Tips.ReserveDetail,
+ MerchantBackend.Rewards.ReserveDetail,
MerchantBackend.ErrorDetail
> {
const { reserveDetailFetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Tips.ReserveDetail>,
+ HttpResponseOk<MerchantBackend.Rewards.ReserveDetail>,
RequestError<MerchantBackend.ErrorDetail>
>([`/private/reserves/${reserveId}`], reserveDetailFetcher, {
refreshInterval: 0,
@@ -158,15 +158,15 @@ export function useReserveDetails(
return { loading: true };
}
-export function useTipDetails(
- tipId: string,
-): HttpResponse<MerchantBackend.Tips.TipDetails, MerchantBackend.ErrorDetail> {
- const { tipsDetailFetcher } = useBackendInstanceRequest();
+export function useRewardDetails(
+ rewardId: string,
+): HttpResponse<MerchantBackend.Rewards.RewardDetails, MerchantBackend.ErrorDetail> {
+ const { rewardsDetailFetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
- HttpResponseOk<MerchantBackend.Tips.TipDetails>,
+ HttpResponseOk<MerchantBackend.Rewards.RewardDetails>,
RequestError<MerchantBackend.ErrorDetail>
- >([`/private/tips/${tipId}`], tipsDetailFetcher, {
+ >([`/private/rewards/${rewardId}`], rewardsDetailFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
diff --git a/packages/merchant-backoffice-ui/src/hooks/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts
index 6b339c05a..00c5e95af 100644
--- a/packages/merchant-backoffice-ui/src/hooks/urls.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/urls.ts
@@ -139,15 +139,15 @@ export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({
////////////////////
export const API_CREATE_RESERVE: Query<
- MerchantBackend.Tips.ReserveCreateRequest,
- MerchantBackend.Tips.ReserveCreateConfirmation
+ MerchantBackend.Rewards.ReserveCreateRequest,
+ MerchantBackend.Rewards.ReserveCreateConfirmation
> = {
method: "POST",
url: "http://backend/instances/default/private/reserves",
};
export const API_LIST_RESERVES: Query<
unknown,
- MerchantBackend.Tips.TippingReserveStatus
+ MerchantBackend.Rewards.RewardReserveStatus
> = {
method: "GET",
url: "http://backend/instances/default/private/reserves",
@@ -155,34 +155,34 @@ export const API_LIST_RESERVES: Query<
export const API_GET_RESERVE_BY_ID = (
pub: string,
-): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({
+): Query<unknown, MerchantBackend.Rewards.ReserveDetail> => ({
method: "GET",
url: `http://backend/instances/default/private/reserves/${pub}`,
});
-export const API_GET_TIP_BY_ID = (
+export const API_GET_REWARD_BY_ID = (
pub: string,
-): Query<unknown, MerchantBackend.Tips.TipDetails> => ({
+): Query<unknown, MerchantBackend.Rewards.RewardDetails> => ({
method: "GET",
- url: `http://backend/instances/default/private/tips/${pub}`,
+ url: `http://backend/instances/default/private/rewards/${pub}`,
});
-export const API_AUTHORIZE_TIP_FOR_RESERVE = (
+export const API_AUTHORIZE_REWARD_FOR_RESERVE = (
pub: string,
): Query<
- MerchantBackend.Tips.TipCreateRequest,
- MerchantBackend.Tips.TipCreateConfirmation
+ MerchantBackend.Rewards.RewardCreateRequest,
+ MerchantBackend.Rewards.RewardCreateConfirmation
> => ({
method: "POST",
- url: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`,
+ url: `http://backend/instances/default/private/reserves/${pub}/authorize-reward`,
});
-export const API_AUTHORIZE_TIP: Query<
- MerchantBackend.Tips.TipCreateRequest,
- MerchantBackend.Tips.TipCreateConfirmation
+export const API_AUTHORIZE_REWARD: Query<
+ MerchantBackend.Rewards.RewardCreateRequest,
+ MerchantBackend.Rewards.RewardCreateConfirmation
> = {
method: "POST",
- url: `http://backend/instances/default/private/tips`,
+ url: `http://backend/instances/default/private/rewards`,
};
export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({
@@ -211,7 +211,7 @@ export const API_GET_INSTANCE_BY_ID = (
export const API_GET_INSTANCE_KYC_BY_ID = (
id: string,
-): Query<unknown, MerchantBackend.Instances.AccountKycRedirects> => ({
+): Query<unknown, MerchantBackend.KYC.AccountKycRedirects> => ({
method: "GET",
url: `http://backend/management/instances/${id}/kyc`,
});
@@ -263,7 +263,7 @@ export const API_GET_CURRENT_INSTANCE: Query<
export const API_GET_CURRENT_INSTANCE_KYC: Query<
unknown,
- MerchantBackend.Instances.AccountKycRedirects
+ MerchantBackend.KYC.AccountKycRedirects
> = {
method: "GET",
url: `http://backend/instances/default/private/kyc`,
diff --git a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
index 5c0932f27..7dee9f896 100644
--- a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
@@ -19,6 +19,9 @@ import {
Codec,
buildCodecForObject,
codecForBoolean,
+ codecForConstString,
+ codecForEither,
+ codecForString,
} from "@gnu-taler/taler-util";
function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
@@ -31,29 +34,49 @@ function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
}
export interface Settings {
- advanceOrderMode: boolean
+ 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>,
- <T extends keyof Settings>(key: T, value: Settings[T]) => void,
+ (s: Settings) => void,
] {
- const { value, update } = useLocalStorage(SETTINGS_KEY);
+ const { value, update } = useLocalStorage(SETTINGS_KEY, defaultSettings);
- const parsed: Settings = value ?? defaultSettings;
- function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
- update({ ...parsed, [k]: v });
+ // 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"
}
- return [parsed, updateField];
}
+
+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/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
index 14e2fcb46..a8108251d 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -19,7 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -29,9 +28,8 @@ import {
FormProvider,
} from "../../../components/form/FormProvider.js";
import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
-import { SetTokenNewInstanceModal } from "../../../components/modal/index.js";
import { MerchantBackend } from "../../../declaration.js";
-import { INSTANCE_ID_REGEX, PAYTO_REGEX } from "../../../utils/constants.js";
+import { INSTANCE_ID_REGEX } from "../../../utils/constants.js";
import { undefinedIfEmpty } from "../../../utils/table.js";
export type Entity = MerchantBackend.Instances.InstanceConfigurationMessage & {
@@ -47,19 +45,19 @@ interface Props {
function with_defaults(id?: string): Partial<Entity> {
return {
id,
- accounts: [],
+ // accounts: [],
user_type: "business",
+ use_stefan: false,
default_pay_delay: { d_us: 2 * 1000 * 60 * 60 * 1000 }, // two hours
- default_wire_fee_amortization: 1,
default_wire_transfer_delay: { d_us: 1000 * 2 * 60 * 60 * 24 * 1000 }, // two days
};
}
export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
const [value, valueHandler] = useState(with_defaults(forceId));
- const [isTokenSet, updateIsTokenSet] = useState<boolean>(false);
- const [isTokenDialogActive, updateIsTokenDialogActive] =
- useState<boolean>(false);
+ // const [isTokenSet, updateIsTokenSet] = useState<boolean>(false);
+ // const [isTokenDialogActive, updateIsTokenDialogActive] =
+ // useState<boolean>(false);
const { i18n } = useTranslationContext();
@@ -67,42 +65,24 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
id: !value.id
? i18n.str`required`
: !INSTANCE_ID_REGEX.test(value.id)
- ? i18n.str`is not valid`
- : undefined,
+ ? i18n.str`is not valid`
+ : undefined,
name: !value.name ? i18n.str`required` : undefined,
user_type: !value.user_type
? i18n.str`required`
: value.user_type !== "business" && value.user_type !== "individual"
- ? i18n.str`should be business or individual`
- : undefined,
- accounts:
- !value.accounts || !value.accounts.length
- ? i18n.str`required`
- : undefinedIfEmpty(
- value.accounts.map((p) => {
- return !PAYTO_REGEX.test(p.payto_uri)
- ? i18n.str`is not valid`
- : undefined;
- }),
- ),
- default_max_deposit_fee: !value.default_max_deposit_fee
- ? i18n.str`required`
- : !Amounts.parse(value.default_max_deposit_fee)
- ? i18n.str`invalid format`
- : undefined,
- default_max_wire_fee: !value.default_max_wire_fee
- ? i18n.str`required`
- : !Amounts.parse(value.default_max_wire_fee)
- ? i18n.str`invalid format`
- : undefined,
- default_wire_fee_amortization:
- value.default_wire_fee_amortization === undefined
- ? i18n.str`required`
- : isNaN(value.default_wire_fee_amortization)
- ? i18n.str`is not a number`
- : value.default_wire_fee_amortization < 1
- ? i18n.str`must be 1 or greater`
+ ? i18n.str`should be business or individual`
: undefined,
+ // accounts:
+ // !value.accounts || !value.accounts.length
+ // ? i18n.str`required`
+ // : undefinedIfEmpty(
+ // value.accounts.map((p) => {
+ // return !PAYTO_REGEX.test(p.payto_uri)
+ // ? i18n.str`is not valid`
+ // : undefined;
+ // }),
+ // ),
default_pay_delay: !value.default_pay_delay
? i18n.str`required`
: undefined,
@@ -129,12 +109,12 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
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}` };
+ // const newToken = value.auth_token;
+ // value.auth_token = undefined;
+ value.auth = { method: "external" }
+ // newToken === null || newToken === undefined
+ // ? { method: "external" }
+ // : { method: "token", token: `secret-token:${newToken}` };
if (!value.address) value.address = {};
if (!value.jurisdiction) value.jurisdiction = {};
// remove above use conversion
@@ -142,16 +122,16 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
return onCreate(value as Entity);
};
- function updateToken(token: string | null) {
- valueHandler((old) => ({
- ...old,
- auth_token: token === null ? undefined : token,
- }));
- }
+ // function updateToken(token: string | null) {
+ // valueHandler((old) => ({
+ // ...old,
+ // auth_token: token === null ? undefined : token,
+ // }));
+ // }
return (
<div>
- <div class="columns">
+ {/* <div class="columns">
<div class="column" />
<div class="column is-four-fifths">
{isTokenDialogActive && (
@@ -174,9 +154,9 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
)}
</div>
<div class="column" />
- </div>
+ </div> */}
- <section class="hero is-hero-bar">
+ {/* <section class="hero is-hero-bar">
<div class="hero-body">
<div class="level">
<div class="level-item has-text-centered">
@@ -186,8 +166,8 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
!isTokenSet
? "button is-danger has-tooltip-bottom"
: !value.auth_token
- ? "button has-tooltip-bottom"
- : "button is-info has-tooltip-bottom"
+ ? "button has-tooltip-bottom"
+ : "button is-info has-tooltip-bottom"
}
data-tooltip={i18n.str`change authorization configuration`}
onClick={() => updateIsTokenDialogActive(true)}
@@ -228,7 +208,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
</div>
</div>
</div>
- </section>
+ </section> */}
<section class="section is-main-section">
<div class="columns">
@@ -250,7 +230,7 @@ export function CreatePage({ onCreate, onBack, forceId }: Props): VNode {
)}
<AsyncButton
onClick={submit}
- disabled={!isTokenSet || hasErrors}
+ disabled={hasErrors}
data-tooltip={
hasErrors
? i18n.str`Need to complete marked fields and choose authorization method`
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
new file mode 100644
index 000000000..3336c53a4
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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/Accounts/Create",
+ component: TestedComponent,
+};
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
new file mode 100644
index 000000000..3ac510f63
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -0,0 +1,175 @@
+/*
+ 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 { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { useBackendContext } from "../../../../context/backend.js";
+import { MerchantBackend } from "../../../../declaration.js";
+import { InputPaytoForm } from "../../../../components/form/InputPaytoForm.js";
+import { parsePayUri, stringifyPaytoUri } from "@gnu-taler/taler-util";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
+import { InputSelector } from "../../../../components/form/InputSelector.js";
+
+type Entity = MerchantBackend.BankAccounts.AccountAddDetails & { repeatPassword: string };
+
+interface Props {
+ onCreate: (d: Entity) => 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 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,
+ }),
+ credit_facade_url: !state.credit_facade_url
+ ? undefined
+ : !isValidURL(state.credit_facade_url) ? i18n.str`not valid url`
+ : 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,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ delete state.repeatPassword
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <InputPaytoForm<Entity>
+ name="payto_uri"
+ label={i18n.str`Account`}
+ />
+ <Input<Entity>
+ name="credit_facade_url"
+ label={i18n.str`Account info URL`}
+ help="https://bank.com"
+ expand
+ tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`}
+ />
+ <InputSelector
+ name="credit_facade_credentials.type"
+ label={i18n.str`Auth type`}
+ tooltip={i18n.str`Choose the authentication type for the account info URL`}
+ values={accountAuthType}
+ toStr={(str) => {
+ if (str === "none") return "Without authentication";
+ return "Username and password";
+ }}
+ />
+ {state.credit_facade_credentials?.type === "basic" ? (
+ <Fragment>
+ <Input
+ name="credit_facade_credentials.username"
+ label={i18n.str`Username`}
+ tooltip={i18n.str`Username to access the account information.`}
+ />
+ <Input
+ name="credit_facade_credentials.password"
+ inputType="password"
+ label={i18n.str`Password`}
+ tooltip={i18n.str`Password to access the account information.`}
+ />
+ <Input
+ name="repeatPassword"
+ inputType="password"
+ label={i18n.str`Repeat password`}
+ />
+ </Fragment>
+ ) : undefined}
+ </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</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
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
new file mode 100644
index 000000000..7d33d25ce
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx
@@ -0,0 +1,65 @@
+/*
+ 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 { useWebhookAPI } from "../../../../hooks/webhooks.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;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { createBankAccount } = useBankAccountAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: Entity) => {
+ return createBankAccount(request)
+ .then((d) => {
+ onConfirm()
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create device`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
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
new file mode 100644
index 000000000..6b4b63735
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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 { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Accounts/List",
+ component: TestedComponent,
+};
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
new file mode 100644
index 000000000..24da755b9
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ 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 } 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;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
+ onSelect: (e: MerchantBackend.BankAccounts.BankAccountEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ accounts={devices.map((o) => ({
+ ...o,
+ id: String(o.h_wire),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ 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
new file mode 100644
index 000000000..7d6db0782
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx
@@ -0,0 +1,385 @@
+/*
+ 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 { 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;
+
+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({
+ accounts,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>Bank accounts</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new accounts`}
+ >
+ <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">
+ {accounts.length > 0 ? (
+ <Table
+ accounts={accounts}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ accounts: Entity[];
+ 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": [], }
+ const accountsByType = accounts.reduce((prev, acc) => {
+ const parsed = parsePaytoUri(acc.payto_uri)
+ if (!parsed) return prev //skip
+ if (parsed.targetType !== "bitcoin" && parsed.targetType !== "x-taler-bank" && parsed.targetType !== "iban") {
+ prev["unknown"].push({ parsed, acc })
+ } else {
+ prev[parsed.targetType].push({ parsed, acc })
+ }
+ return prev
+ }, emptyList)
+
+ const bitcoinAccounts = accountsByType["bitcoin"]
+ const talerbankAccounts = accountsByType["x-taler-bank"]
+ const ibanAccounts = accountsByType["iban"]
+ const unkownAccounts = accountsByType["unknown"]
+
+
+ return (
+ <Fragment>
+
+ {bitcoinAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Bitcoin type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Address</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sewgit 1</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Sewgit 2</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {bitcoinAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriBitcoin
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetPath}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.segwitAddrs[0]}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.segwitAddrs[1]}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+
+
+ {talerbankAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Taler type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Host</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Account name</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {talerbankAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriTalerBank
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.host}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.account}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+ {ibanAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>IBAN type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Account name</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>IBAN</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>BIC</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {ibanAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriIBAN
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.params["receiver-name"]}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.iban}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.bic ?? ""}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+
+ {unkownAccounts.length > 0 && <div class="table-container">
+ <p class="card-header-title"><i18n.Translate>Other type accounts</i18n.Translate></p>
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>Type</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Path</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {unkownAccounts.map(({ parsed, acc }, idx) => {
+ const ac = parsed as PaytoUriUnknown
+ return (
+ <tr key={idx}>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetType}
+ </td>
+ <td
+ onClick={(): void => onSelect(acc)}
+ style={{ cursor: "pointer" }}
+ >
+ {ac.targetPath}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected accounts from the database`}
+ onClick={() => onDelete(acc)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>}
+ </Fragment>
+
+ );
+}
+
+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 accounts yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
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
new file mode 100644
index 000000000..9788ce0ec
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
@@ -0,0 +1,107 @@
+/*
+ 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,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } 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 { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.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 ListValidators({
+ 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));
+
+ 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>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ devices={result.data.accounts}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.h_wire);
+ }}
+ onDelete={(e: MerchantBackend.BankAccounts.BankAccountEntry) =>
+ deleteBankAccount(e.h_wire)
+ .then(() =>
+ setNotif({
+ message: i18n.str`bank account delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the bank account`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </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
new file mode 100644
index 000000000..fcb77b820
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ 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 { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Validators/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
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
new file mode 100644
index 000000000..802f593cf
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
@@ -0,0 +1,114 @@
+/*
+ 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 { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../../../components/form/FormProvider.js";
+import { Input } from "../../../../components/form/Input.js";
+import { MerchantBackend, WithId } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.BankAccounts.AccountPatchDetails & WithId;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ account: Entity;
+}
+export function UpdatePage({ account, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>(account);
+
+ const errors: FormErrors<Entity> = {
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onUpdate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ Account: <b>{account.id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="credit_facade_url"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`dddd`}
+ />
+ </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</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
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
new file mode 100644
index 000000000..44dee7651
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx
@@ -0,0 +1,96 @@
+/*
+ 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,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useState } from "preact/hooks";
+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 { Notification } from "../../../../utils/types.js";
+import { UpdatePage } from "./UpdatePage.js";
+
+export type Entity = MerchantBackend.BankAccounts.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 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);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ account={{ ...result.data, id: bid }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateBankAccount(bid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update account`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
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 e5937ab7b..21dadb1e3 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/DetailPage.tsx
@@ -36,14 +36,13 @@ interface Props {
function convert(
from: MerchantBackend.Instances.QueryInstancesResponse,
): Entity {
- const { accounts: allAccounts, ...rest } = from;
- const accounts = allAccounts.filter((a) => a.active);
const defaults = {
default_wire_fee_amortization: 1,
+ use_stefan: true,
default_pay_delay: { d_us: 1000 * 60 * 60 * 1000 }, //one hour
default_wire_transfer_delay: { d_us: 1000 * 60 * 60 * 2 * 1000 }, //two hours
};
- return { ...defaults, ...rest, accounts };
+ return { ...defaults, ...from };
}
export function DetailPage({ selected }: Props): VNode {
@@ -74,11 +73,6 @@ export function DetailPage({ selected }: Props): VNode {
<div class="column is-6">
<FormProvider<Entity> object={value} valueHandler={valueHandler}>
<Input<Entity> name="name" readonly label={i18n.str`Name`} />
- <Input<Entity>
- name="accounts"
- readonly
- label={i18n.str`Account address`}
- />
</FormProvider>
</div>
<div class="column" />
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 3a6e0fbfe..367fabce2 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
@@ -51,17 +51,15 @@ function createExample<Props>(
export const Example = createExample(TestedComponent, {
selected: {
- accounts: [],
name: "name",
auth: { method: "external" },
address: {},
+ user_type: "business",
jurisdiction: {},
- default_max_deposit_fee: "TESTKUDOS:2",
- default_max_wire_fee: "TESTKUDOS:1",
+ use_stefan: true,
default_pay_delay: {
d_us: 1000 * 1000, //one second
},
- default_wire_fee_amortization: 1,
default_wire_transfer_delay: {
d_us: 1000 * 1000, //one second
},
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 6f50ac830..d33f64ada 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
@@ -54,5 +54,5 @@ export const Example = tests.createExample(TestedComponent, {
payto_uri: "payto://iban/de123123123",
},
],
- } as MerchantBackend.Instances.AccountKycRedirects,
+ } 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 67005d3cc..338081886 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
@@ -24,7 +24,7 @@ import { h, VNode } from "preact";
import { MerchantBackend } from "../../../../declaration.js";
export interface Props {
- status: MerchantBackend.Instances.AccountKycRedirects;
+ status: MerchantBackend.KYC.AccountKycRedirects;
}
export function ListPage({ status }: Props): VNode {
@@ -85,11 +85,11 @@ export function ListPage({ status }: Props): VNode {
);
}
interface PendingTableProps {
- entries: MerchantBackend.Instances.MerchantAccountKycRedirect[];
+ entries: MerchantBackend.KYC.MerchantAccountKycRedirect[];
}
interface TimedOutTableProps {
- entries: MerchantBackend.Instances.ExchangeKycTimeout[];
+ entries: MerchantBackend.KYC.ExchangeKycTimeout[];
}
function PendingTable({ entries }: PendingTableProps): VNode {
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 fcf611c3c..bd9f65718 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
@@ -42,12 +42,13 @@ function createExample<Props>(
export const Example = createExample(TestedComponent, {
instanceConfig: {
- default_max_deposit_fee: "",
- default_max_wire_fee: "",
default_pay_delay: {
d_us: 1000 * 1000 * 60 * 60, //one hour
},
- default_wire_fee_amortization: 1,
+ default_wire_transfer_delay: {
+ d_us: 1000 * 1000 * 60 * 60, //one hour
+ },
+ use_stefan: true,
},
instanceInventory: [
{
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 fa9347c6e..ea2cf849a 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
@@ -44,6 +44,7 @@ import { OrderCreateSchema as schema } from "../../../../schemas/index.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;
@@ -52,34 +53,38 @@ interface Props {
instanceInventory: (MerchantBackend.Products.ProductDetail & WithId)[];
}
interface InstanceConfig {
- default_max_wire_fee: string;
- default_max_deposit_fee: string;
- default_wire_fee_amortization: number;
+ use_stefan: boolean;
default_pay_delay: Duration;
+ default_wire_transfer_delay: Duration;
}
-function with_defaults(config: InstanceConfig): Partial<Entity> {
+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),
+ });
return {
inventoryProducts: {},
products: [],
pricing: {},
payments: {
- max_wire_fee: config.default_max_wire_fee,
- max_fee: config.default_max_deposit_fee,
- wire_fee_amortization: config.default_wire_fee_amortization,
+ max_fee: undefined,
pay_deadline: defaultPayDeadline,
refund_deadline: defaultPayDeadline,
createToken: true,
+ wire_transfer_deadline: defaultWireDeadline,
},
shipping: {},
- extra: "",
+ extra: {},
};
}
@@ -107,8 +112,6 @@ interface Payments {
wire_transfer_deadline?: Date;
auto_refund_deadline?: Date;
max_fee?: string;
- max_wire_fee?: string;
- wire_fee_amortization?: number;
createToken: boolean;
minimum_age?: number;
}
@@ -118,7 +121,7 @@ interface Entity {
pricing: Partial<Pricing>;
payments: Partial<Payments>;
shipping: Partial<Shipping>;
- extra: string;
+ extra: Record<string, string>;
}
const stringIsValidJSON = (value: string) => {
@@ -136,8 +139,9 @@ export function CreatePage({
instanceConfig,
instanceInventory,
}: Props): VNode {
- const [value, valueHandler] = useState(with_defaults(instanceConfig));
const config = useConfigContext();
+ const instance_default = with_defaults(instanceConfig, config.currency)
+ const [value, valueHandler] = useState(instance_default);
const zero = Amounts.zeroOfCurrency(config.currency);
const [settings] = useSettings()
const inventoryList = Object.values(value.inventoryProducts || {});
@@ -160,10 +164,10 @@ export function CreatePage({
? i18n.str`must be greater than 0`
: undefined,
}),
- extra:
- value.extra && !stringIsValidJSON(value.extra)
- ? i18n.str`not a valid json`
- : undefined,
+ // extra:
+ // value.extra && !stringIsValidJSON(value.extra)
+ // ? i18n.str`not a valid json`
+ // : undefined,
payments: undefinedIfEmpty({
refund_deadline: !value.payments?.refund_deadline
? undefined
@@ -202,6 +206,7 @@ export function CreatePage({
)
? i18n.str`auto refund cannot be after refund deadline`
: undefined,
+
}),
shipping: undefinedIfEmpty({
delivery_date: !value.shipping?.delivery_date
@@ -225,7 +230,7 @@ export function CreatePage({
amount: order.pricing.order_price,
summary: order.pricing.summary,
products: productList,
- extra: value.extra,
+ extra: JSON.stringify(value.extra),
pay_deadline: value.payments.pay_deadline
? {
t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000),
@@ -250,9 +255,7 @@ export function CreatePage({
),
}
: undefined,
- wire_fee_amortization: value.payments.wire_fee_amortization as number,
max_fee: value.payments.max_fee as string,
- max_wire_fee: value.payments.max_wire_fee as string,
delivery_date: value.shipping.delivery_date
? { t_s: value.shipping.delivery_date.getTime() / 1000 }
@@ -326,6 +329,8 @@ export function CreatePage({
const totalAsString = Amounts.stringify(totalPrice.amount);
const allProducts = productList.concat(inventoryList.map(asProduct));
+ const [newField, setNewField] = useState("")
+
useEffect(() => {
valueHandler((v) => {
return {
@@ -486,16 +491,61 @@ export function CreatePage({
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={() => {
+ valueHandler({
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ pay_deadline: instance_default.payments?.pay_deadline
+ }
+ })
+ }}>
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
/>
<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>
+ }
/>
<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>
+ }
/>
<InputDate
name="payments.auto_refund_deadline"
@@ -505,23 +555,13 @@ export function CreatePage({
<InputCurrency
name="payments.max_fee"
- label={i18n.str`Maximum deposit fee`}
- tooltip={i18n.str`Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
+ 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.`}
/>
- <InputCurrency
- name="payments.max_wire_fee"
- label={i18n.str`Maximum wire fee`}
- tooltip={i18n.str`Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.`}
- />
- <InputNumber
- name="payments.wire_fee_amortization"
- label={i18n.str`Wire fee amortization`}
- tooltip={i18n.str`Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.`}
- />
- <InputBoolean
+ <InputToggle
name="payments.createToken"
label={i18n.str`Create token`}
- tooltip={i18n.str`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`}
+ tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`}
/>
<InputNumber
name="payments.minimum_age"
@@ -530,7 +570,7 @@ export function CreatePage({
help={
minAgeByProducts > 0
? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
- : undefined
+ : i18n.str`No product with age restriction in this order`
}
/>
</InputGroup>
@@ -542,12 +582,53 @@ export function CreatePage({
label={i18n.str`Additional information`}
tooltip={i18n.str`Custom information to be included in the contract for this order.`}
>
- <Input
- name="extra"
- inputType="multiline"
- label={`Value`}
- tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
- />
+ {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>
+ }
+ />
+ })}
+ <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"}>
+ <i class="mdi mdi-information" />
+ </span>
+ </label>
+ </div>
+ <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)} />
+ </p>
+ </div>
+ </div>
+ <button class="button" onClick={(e) => {
+ setNewField("")
+ valueHandler({
+ ...value,
+ extra: {
+ ...(value.extra ?? {}),
+ [newField]: ""
+ }
+ })
+ }}>add</button>
+ </div>
</InputGroup>
}
</FormProvider>
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 ffefd5302..2474fd042 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
@@ -38,7 +38,7 @@ export type Entity = {
};
interface Props {
onBack?: () => void;
- onConfirm: () => void;
+ onConfirm: (id: string) => void;
onUnauthorized: () => VNode;
onNotFound: () => VNode;
onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
@@ -95,7 +95,9 @@ export default function OrderCreate({
onBack={onBack}
onCreate={(request: MerchantBackend.Orders.PostOrderRequest) => {
createOrder(request)
- .then(onConfirm)
+ .then((r) => {
+ return onConfirm(r.data.order_id)
+ })
.catch((error) => {
setNotif({
message: "could not create order",
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 e430ede56..6e73a01a5 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
@@ -50,13 +50,11 @@ const defaultContractTerm = {
auditors: [],
exchanges: [],
max_fee: "TESTKUDOS:1",
- max_wire_fee: "TESTKUDOS:1",
merchant: {} as any,
merchant_base_url: "http://merchant.url/",
order_id: "2021.165-03GDFC26Y1NNG",
products: [],
summary: "text summary",
- wire_fee_amortization: 1,
wire_transfer_deadline: {
t_s: "never",
},
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 8965d41c9..e42adc2ff 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
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AmountJson, Amounts } from "@gnu-taler/taler-util";
+import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format, formatDistance } from "date-fns";
import { Fragment, h, VNode } from "preact";
@@ -38,6 +38,7 @@ import { MerchantBackend } from "../../../../declaration.js";
import { mergeRefunds } from "../../../../utils/amount.js";
import { RefundModal } from "../list/Table.js";
import { Event, Timeline } from "./Timeline.js";
+import { dateFormatForSettings, datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
type CT = MerchantBackend.ContractTerms;
@@ -87,18 +88,6 @@ function ContractTerms({ value }: { value: CT }) {
label={i18n.str`Max fee`}
tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`}
/>
- <Input<CT>
- readonly
- name="max_wire_fee"
- label={i18n.str`Max wire fee`}
- tooltip={i18n.str`maximum wire fee accepted by the merchant`}
- />
- <Input<CT>
- readonly
- name="wire_fee_amortization"
- label={i18n.str`Wire fee amortization`}
- tooltip={i18n.str`over how many customer transactions does the merchant expect to amortize wire fees on average`}
- />
<InputDate<CT>
readonly
name="timestamp"
@@ -204,6 +193,7 @@ function ClaimedPage({
const [value, valueHandler] = useState<Partial<Claimed>>(order);
const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
return (
<div>
@@ -249,7 +239,7 @@ function ClaimedPage({
</b>{" "}
{format(
new Date(order.contract_terms.timestamp.t_s * 1000),
- "yyyy-MM-dd HH:mm:ss",
+ datetimeFormatForSettings(settings)
)}
</p>
</div>
@@ -427,9 +417,10 @@ function PaidPage({
const [value, valueHandler] = useState<Partial<Paid>>(order);
const { url } = useBackendContext();
- const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part
- const proto = url.startsWith("http://") ? "taler+http" : "taler";
- const refundurl = `${proto}://refund/${refundHost}/${order.contract_terms.order_id}/`;
+ const refundurl = stringifyRefundUri({
+ merchantBaseUrl: url,
+ orderId: order.contract_terms.order_id
+ })
const refundable =
new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000;
const { i18n } = useTranslationContext();
@@ -618,6 +609,7 @@ function UnpaidPage({
}) {
const [value, valueHandler] = useState<Partial<Unpaid>>(order);
const { i18n } = useTranslationContext();
+ const [settings] = useSettings()
return (
<div>
<section class="hero is-hero-bar">
@@ -666,7 +658,7 @@ function UnpaidPage({
? "never"
: format(
new Date(order.creation_time.t_s * 1000),
- "yyyy-MM-dd HH:mm:ss",
+ datetimeFormatForSettings(settings)
)}
</p>
</div>
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 e68889a92..8c863f386 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
@@ -16,6 +16,7 @@
import { format } from "date-fns";
import { h } from "preact";
import { useEffect, useState } from "preact/hooks";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
interface Props {
events: Event[];
@@ -30,7 +31,7 @@ export function Timeline({ events: e }: Props) {
});
events.sort((a, b) => a.when.getTime() - b.when.getTime());
-
+ const [settings] = useSettings();
const [state, setState] = useState(events);
useEffect(() => {
const handle = setTimeout(() => {
@@ -104,7 +105,7 @@ export function Timeline({ events: e }: Props) {
}
})()}
<div class="timeline-content">
- {e.description !== "now" && <p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p>}
+ {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>}
<p>{e.description}</p>
</div>
</div>
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 37770d273..c29a6fa6e 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
@@ -26,19 +26,24 @@ import { useState } from "preact/hooks";
import { DatePicker } from "../../../../components/picker/DatePicker.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
import { CardTable } from "./Table.js";
+import { dateFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
export interface ListPageProps {
errorOrderId: string | undefined;
onShowAll: () => void;
+ onShowNotPaid: () => void;
onShowPaid: () => void;
onShowRefunded: () => void;
onShowNotWired: () => void;
+ onShowWired: () => void;
onCopyURL: (id: string) => void;
isAllActive: string;
isPaidActive: string;
+ isNotPaidActive: string;
isRefundedActive: string;
isNotWiredActive: string;
+ isWiredActive: string;
jumpToDate?: Date;
onSelectDate: (date?: Date) => void;
@@ -66,18 +71,23 @@ export function ListPage({
onCopyURL,
onShowAll,
onShowPaid,
+ onShowNotPaid,
onShowRefunded,
onShowNotWired,
+ onShowWired,
onSelectDate,
isPaidActive,
isRefundedActive,
isNotWiredActive,
onCreate,
+ isNotPaidActive,
+ isWiredActive,
}: ListPageProps): VNode {
const { i18n } = useTranslationContext();
const dateTooltip = i18n.str`select date to show nearby orders`;
const [pickDate, setPickDate] = useState(false);
const [orderId, setOrderId] = useState<string>("");
+ const [settings] = useSettings();
return (
<section class="section is-main-section">
@@ -116,13 +126,13 @@ export function ListPage({
<div class="column is-two-thirds">
<div class="tabs" style={{ overflow: "inherit" }}>
<ul>
- <li class={isAllActive}>
+ <li class={isNotPaidActive}>
<div
class="has-tooltip-right"
- data-tooltip={i18n.str`remove all filters`}
+ data-tooltip={i18n.str`only show paid orders`}
>
- <a onClick={onShowAll}>
- <i18n.Translate>All</i18n.Translate>
+ <a onClick={onShowNotPaid}>
+ <i18n.Translate>New</i18n.Translate>
</a>
</div>
</li>
@@ -156,6 +166,26 @@ export function ListPage({
</a>
</div>
</li>
+ <li class={isWiredActive}>
+ <div
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`only show orders where customers paid, but wire payments from payment provider are still pending`}
+ >
+ <a onClick={onShowWired}>
+ <i18n.Translate>Completed</i18n.Translate>
+ </a>
+ </div>
+ </li>
+ <li class={isAllActive}>
+ <div
+ class="has-tooltip-right"
+ data-tooltip={i18n.str`remove all filters`}
+ >
+ <a onClick={onShowAll}>
+ <i18n.Translate>All</i18n.Translate>
+ </a>
+ </div>
+ </li>
</ul>
</div>
</div>
@@ -180,8 +210,8 @@ export function ListPage({
class="input"
type="text"
readonly
- value={!jumpToDate ? "" : format(jumpToDate, "yyyy/MM/dd")}
- placeholder={i18n.str`date (YYYY/MM/DD)`}
+ value={!jumpToDate ? "" : format(jumpToDate, dateFormatForSettings(settings))}
+ placeholder={i18n.str`date (${dateFormatForSettings(settings)})`}
onClick={() => {
setPickDate(true);
}}
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 3c927033b..608c9b20d 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
@@ -36,6 +36,7 @@ import { ConfirmModal } from "../../../../components/modal/index.js";
import { useConfigContext } from "../../../../context/config.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
import { mergeRefunds } from "../../../../utils/amount.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
interface Props {
@@ -136,6 +137,7 @@ function Table({
hasMoreBefore,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
return (
<div class="table-container">
{onLoadMoreBefore && (
@@ -173,9 +175,9 @@ function Table({
{i.timestamp.t_s === "never"
? "never"
: format(
- new Date(i.timestamp.t_s * 1000),
- "yyyy/MM/dd HH:mm:ss",
- )}
+ new Date(i.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
</td>
<td
onClick={(): void => onSelect(i)}
@@ -260,6 +262,7 @@ export function RefundModal({
}: RefundModalProps): VNode {
type State = { mainReason?: string; description?: string; refund?: string };
const [form, setValue] = useState<State>({});
+ const [settings] = useSettings();
const { i18n } = useTranslationContext();
// const [errors, setErrors] = useState<FormErrors<State>>({});
@@ -281,8 +284,8 @@ export function RefundModal({
const totalRefundable = !orderPrice
? Amounts.zeroOfCurrency(totalRefunded.currency)
: refunds.length
- ? Amounts.sub(orderPrice, totalRefunded).amount
- : orderPrice;
+ ? Amounts.sub(orderPrice, totalRefunded).amount
+ : orderPrice;
const isRefundable = Amounts.isNonZero(totalRefundable);
const duplicatedText = i18n.str`duplicated`;
@@ -296,10 +299,10 @@ export function RefundModal({
refund: !form.refund
? i18n.str`required`
: !Amounts.parse(form.refund)
- ? i18n.str`invalid format`
- : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
- ? i18n.str`this value exceed the refundable amount`
- : undefined,
+ ? i18n.str`invalid format`
+ : Amounts.cmp(totalRefundable, Amounts.parse(form.refund)!) === -1
+ ? i18n.str`this value exceed the refundable amount`
+ : undefined,
};
const hasErrors = Object.keys(errors).some(
(k) => (errors as any)[k] !== undefined,
@@ -361,9 +364,9 @@ export function RefundModal({
{r.timestamp.t_s === "never"
? "never"
: format(
- new Date(r.timestamp.t_s * 1000),
- "yyyy-MM-dd HH:mm:ss",
- )}
+ new Date(r.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
</td>
<td>{r.amount}</td>
<td>{r.reason}</td>
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 6888eda58..48f77e3d3 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
@@ -55,7 +55,7 @@ export default function OrderList({
onSelect,
onNotFound,
}: Props): VNode {
- const [filter, setFilter] = useState<InstanceOrderFilter>({});
+ const [filter, setFilter] = useState<InstanceOrderFilter>({ paid: "no" });
const [orderToBeRefunded, setOrderToBeRefunded] = useState<
MerchantBackend.Orders.OrderHistoryEntry | undefined
>(undefined);
@@ -88,13 +88,15 @@ export default function OrderList({
return onLoadError(result);
}
- const isPaidActive = filter.paid === "yes" ? "is-active" : "";
+ 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" ? "is-active" : "";
+ const isNotWiredActive = filter.wired === "no" && filter.paid === "yes" ? "is-active" : "";
+ const isWiredActive = filter.wired === "yes" ? "is-active" : "";
const isAllActive =
filter.paid === undefined &&
- filter.refunded === undefined &&
- filter.wired === undefined
+ filter.refunded === undefined &&
+ filter.wired === undefined
? "is-active"
: "";
@@ -127,7 +129,9 @@ export default function OrderList({
errorOrderId={errorOrderId}
isAllActive={isAllActive}
isNotWiredActive={isNotWiredActive}
+ isWiredActive={isWiredActive}
isPaidActive={isPaidActive}
+ isNotPaidActive={isNotPaidActive}
isRefundedActive={isRefundedActive}
jumpToDate={filter.date}
onCopyURL={(id) =>
@@ -137,9 +141,11 @@ export default function OrderList({
onSearchOrderById={testIfOrderExistAndSelect}
onSelectDate={setNewDate}
onShowAll={() => setFilter({})}
+ onShowNotPaid={() => setFilter({ paid: "no" })}
onShowPaid={() => setFilter({ paid: "yes" })}
onShowRefunded={() => setFilter({ refunded: "yes" })}
- onShowNotWired={() => setFilter({ wired: "no" })}
+ onShowNotWired={() => setFilter({ wired: "no", paid: "yes" })}
+ onShowWired={() => setFilter({ wired: "yes" })}
/>
{orderToBeRefunded && (
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 6bbb89dfa..cbfe1d573 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
@@ -32,6 +32,7 @@ import {
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";
type Entity = MerchantBackend.Products.ProductDetail & WithId;
@@ -122,6 +123,7 @@ function Table({
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">
@@ -134,7 +136,7 @@ function Table({
<i18n.Translate>Description</i18n.Translate>
</th>
<th>
- <i18n.Translate>Sell</i18n.Translate>
+ <i18n.Translate>Price per unit</i18n.Translate>
</th>
<th>
<i18n.Translate>Taxes</i18n.Translate>
@@ -156,10 +158,10 @@ function Table({
const restStockInfo = !i.next_restock
? ""
: i.next_restock.t_s === "never"
- ? "never"
- : `restock at ${format(
+ ? "never"
+ : `restock at ${format(
new Date(i.next_restock.t_s * 1000),
- "yyyy/MM/dd",
+ dateFormatForSettings(settings),
)}`;
let stockInfo: ComponentChildren = "";
if (i.total_stock < 0) {
@@ -332,26 +334,35 @@ function FastProductWithInfiniteStockUpdateForm({
/>
</FormProvider>
- <div class="buttons is-right mt-5">
- <button class="button" onClick={onCancel}>
- <i18n.Translate>Cancel</i18n.Translate>
- </button>
- <span
- class="has-tooltip-left"
- data-tooltip={i18n.str`update product with new price`}
- >
- <button
- class="button is-info"
- onClick={() =>
- onUpdate({
- ...product,
- price: value.price,
- })
- }
- >
- <i18n.Translate>Confirm</i18n.Translate>
+ <div class="buttons is-expanded">
+
+ <div class="buttons mt-5">
+
+ <button class="button " onClick={onCancel}>
+ <i18n.Translate>Clone</i18n.Translate>
</button>
- </span>
+ </div>
+ <div class="buttons is-right mt-5">
+ <button class="button" onClick={onCancel}>
+ <i18n.Translate>Cancel</i18n.Translate>
+ </button>
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`update product with new price`}
+ >
+ <button
+ class="button is-info"
+ onClick={() =>
+ onUpdate({
+ ...product,
+ price: value.price,
+ })
+ }
+ >
+ <i18n.Translate>Confirm update</i18n.Translate>
+ </button>
+ </span>
+ </div>
</div>
</Fragment>
);
@@ -374,9 +385,8 @@ function FastProductWithManagedStockUpdateForm({
const errors: FormErrors<FastProductUpdate> = {
lost:
currentStock + value.incoming < value.lost
- ? `lost cannot be greater that current + incoming (max ${
- currentStock + value.incoming
- })`
+ ? `lost cannot be greater that current + incoming (max ${currentStock + value.incoming
+ })`
: undefined,
};
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 87efd1554..85c50e5ed 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
@@ -36,6 +36,7 @@ import {
import { Notification } from "../../../../utils/types.js";
import { CardTable } from "./Table.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal, DeleteModal } from "../../../../components/modal/index.js";
interface Props {
onUnauthorized: () => VNode;
@@ -53,6 +54,8 @@ export default function ProductList({
}: Props): VNode {
const result = useInstanceProducts();
const { deleteProduct, updateProduct } = useProductAPI();
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Products.ProductDetail & WithId | null>(null);
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -97,22 +100,43 @@ export default function ProductList({
}
onSelect={(product) => onSelect(product.id)}
onDelete={(prod: MerchantBackend.Products.ProductDetail & WithId) =>
- deleteProduct(prod.id)
- .then(() =>
+ setDeleting(prod)
+ }
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete product`}
+ description={`Delete the product "${deleting.description}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteProduct(deleting.id);
setNotif({
- message: i18n.str`product delete successfully`,
+ message: i18n.str`Product "${deleting.description}" (ID: ${deleting.id}) has been deleted`,
type: "SUCCESS",
- }),
- )
- .catch((error) =>
+ });
+ } catch (error) {
setNotif({
- message: i18n.str`could not delete the product`,
+ message: i18n.str`Failed to delete product`,
type: "ERROR",
- description: error.message,
- }),
- )
- }
- />
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the product named <b>&quot;{deleting.description}&quot;</b> (ID:{" "}
+ <b>{deleting.id}</b>), the stock and related information will be lost
+ </p>
+ <p class="warning">
+ Deleting an product <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
</section>
);
}
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
index fccb20121..2201e75a5 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx
@@ -36,7 +36,7 @@ import {
URL_REGEX,
} from "../../../../utils/constants.js";
-type Entity = MerchantBackend.Tips.ReserveCreateRequest;
+type Entity = MerchantBackend.Rewards.ReserveCreateRequest;
interface Props {
onCreate: (d: Entity) => Promise<void>;
@@ -80,15 +80,15 @@ function ViewStep({
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,
+ ? 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,
+ ? i18n.str`must be a valid URL`
+ : !exchangeQueryError
+ ? undefined
+ : exchangeQueryError,
};
const hasErrors = Object.keys(errors).some(
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
index 94fcdaff7..1d512c843 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatedSuccessfully.tsx
@@ -22,8 +22,8 @@ import { CreatedSuccessfully as Template } from "../../../../components/notifica
import { MerchantBackend, WireAccount } from "../../../../declaration.js";
type Entity = {
- request: MerchantBackend.Tips.ReserveCreateRequest;
- response: MerchantBackend.Tips.ReserveCreateConfirmation;
+ request: MerchantBackend.Rewards.ReserveCreateRequest;
+ response: MerchantBackend.Rewards.ReserveCreateConfirmation;
};
interface Props {
@@ -98,15 +98,15 @@ export function ShowAccountsOfReserveAsQRWithLink({
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);
+ .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));
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
index 8a4fe1565..4bbaf1459 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/index.tsx
@@ -39,9 +39,9 @@ export default function CreateReserve({ onBack, onConfirm }: Props): VNode {
const [createdOk, setCreatedOk] = useState<
| {
- request: MerchantBackend.Tips.ReserveCreateRequest;
- response: MerchantBackend.Tips.ReserveCreateConfirmation;
- }
+ request: MerchantBackend.Rewards.ReserveCreateRequest;
+ response: MerchantBackend.Rewards.ReserveCreateConfirmation;
+ }
| undefined
>(undefined);
@@ -54,7 +54,7 @@ export default function CreateReserve({ onBack, onConfirm }: Props): VNode {
<NotificationCard notification={notif} />
<CreatePage
onBack={onBack}
- onCreate={(request: MerchantBackend.Tips.ReserveCreateRequest) => {
+ onCreate={(request: MerchantBackend.Rewards.ReserveCreateRequest) => {
return createReserve(request)
.then((r) => setCreatedOk({ request, response: r.data }))
.catch((error) => {
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
index b0173b5d3..d8840eeac 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/DetailPage.tsx
@@ -36,11 +36,12 @@ 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 { useTipDetails } from "../../../../hooks/reserves.js";
-import { TipInfo } from "./TipInfo.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.Tips.ReserveDetail;
+type Entity = MerchantBackend.Rewards.ReserveDetail;
type CT = MerchantBackend.ContractTerms;
interface Props {
@@ -116,14 +117,14 @@ export function DetailPage({ id, selected, onBack }: Props): VNode {
<span class="icon">
<i class="mdi mdi-cash-register" />
</span>
- <i18n.Translate>Tips</i18n.Translate>
+ <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.tips && selected.tips.length > 0 ? (
- <Table tips={selected.tips} />
+ {selected.rewards && selected.rewards.length > 0 ? (
+ <Table rewards={selected.rewards} />
) : (
<EmptyTable />
)}
@@ -163,7 +164,7 @@ function EmptyTable(): VNode {
</p>
<p>
<i18n.Translate>
- No tips has been authorized from this reserve
+ No reward has been authorized from this reserve
</i18n.Translate>
</p>
</div>
@@ -171,10 +172,10 @@ function EmptyTable(): VNode {
}
interface TableProps {
- tips: MerchantBackend.Tips.TipStatusEntry[];
+ rewards: MerchantBackend.Rewards.RewardStatusEntry[];
}
-function Table({ tips }: TableProps): VNode {
+function Table({ rewards }: TableProps): VNode {
const { i18n } = useTranslationContext();
return (
<div class="table-container">
@@ -196,8 +197,8 @@ function Table({ tips }: TableProps): VNode {
</tr>
</thead>
<tbody>
- {tips.map((t, i) => {
- return <TipRow id={t.tip_id} key={i} entry={t} />;
+ {rewards.map((t, i) => {
+ return <RewardRow id={t.reward_id} key={i} entry={t} />;
})}
</tbody>
</table>
@@ -205,15 +206,16 @@ function Table({ tips }: TableProps): VNode {
);
}
-function TipRow({
+function RewardRow({
id,
entry,
}: {
id: string;
- entry: MerchantBackend.Tips.TipStatusEntry;
+ entry: MerchantBackend.Rewards.RewardStatusEntry;
}) {
const [selected, setSelected] = useState(false);
- const result = useTipDetails(id);
+ const result = useRewardDetails(id);
+ const [settings] = useSettings();
if (result.loading) {
return (
<tr>
@@ -242,11 +244,11 @@ function TipRow({
<Fragment>
{selected && (
<SimpleModal
- description="tip"
+ description="reward"
active
onCancel={() => setSelected(false)}
>
- <TipInfo id={id} amount={info.total_authorized} entity={info} />
+ <RewardInfo id={id} amount={info.total_authorized} entity={info} />
</SimpleModal>
)}
<tr>
@@ -256,7 +258,7 @@ function TipRow({
<td onClick={onSelect}>
{info.expiration.t_s === "never"
? "never"
- : format(info.expiration.t_s * 1000, "yyyy/MM/dd HH:mm:ss")}
+ : 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
index 2592e2c6e..41c715f20 100644
--- 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
@@ -92,7 +92,7 @@ export const NotYetFunded = createExample(TestedComponent, {
},
});
-export const FundedWithEmptyTips = createExample(TestedComponent, {
+export const FundedWithEmptyRewards = createExample(TestedComponent, {
id: "THISISTHERESERVEID",
selected: {
active: true,
@@ -115,10 +115,10 @@ export const FundedWithEmptyTips = createExample(TestedComponent, {
},
],
exchange_url: "http://exchange.taler/",
- tips: [
+ rewards: [
{
reason: "asdasd",
- tip_id: "123",
+ reward_id: "123",
total_amount: "TESTKUDOS:1",
},
],
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
index 360d39aba..57a051ed7 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/TipInfo.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
@@ -17,8 +17,10 @@ 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";
+import { stringifyRewardUri } from "@gnu-taler/taler-util";
-type Entity = MerchantBackend.Tips.TipDetails;
+type Entity = MerchantBackend.Rewards.RewardDetails;
interface Props {
id: string;
@@ -26,11 +28,10 @@ interface Props {
amount: string;
}
-export function TipInfo({ id, amount, entity }: Props): VNode {
- const { url } = useBackendContext();
- const tipHost = url.replace(/.*:\/\//, ""); // remove protocol part
- const proto = url.startsWith("http://") ? "taler+http" : "taler";
- const tipURL = `${proto}://tip/${tipHost}/${id}`;
+export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode {
+ const { url: merchantBaseUrl } = useBackendContext();
+ const [settings] = useSettings();
+ const rewardURL = stringifyRewardUri({ merchantBaseUrl, merchantRewardId })
return (
<Fragment>
<div class="field is-horizontal">
@@ -52,8 +53,8 @@ export function TipInfo({ id, amount, entity }: Props): VNode {
<div class="field-body is-flex-grow-3">
<div class="field" style={{ overflowWrap: "anywhere" }}>
<p class="control">
- <a target="_blank" rel="noreferrer" href={tipURL}>
- {tipURL}
+ <a target="_blank" rel="noreferrer" href={rewardURL}>
+ {rewardURL}
</a>
</p>
</div>
@@ -73,9 +74,9 @@ export function TipInfo({ id, amount, entity }: Props): VNode {
!entity.expiration || entity.expiration.t_s === "never"
? "never"
: format(
- entity.expiration.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss",
- )
+ entity.expiration.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
}
/>
</p>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
index 1882f50d3..e205ee621 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeTipModal.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/AutorizeRewardModal.tsx
@@ -34,32 +34,32 @@ import {
ContinueModal,
} from "../../../../components/modal/index.js";
import { MerchantBackend } from "../../../../declaration.js";
-import { AuthorizeTipSchema } from "../../../../schemas/index.js";
+import { AuthorizeRewardSchema } from "../../../../schemas/index.js";
import { CreatedSuccessfully } from "./CreatedSuccessfully.js";
-interface AuthorizeTipModalProps {
+interface AuthorizeRewardModalProps {
onCancel: () => void;
- onConfirm: (value: MerchantBackend.Tips.TipCreateRequest) => void;
- tipAuthorized?: {
- response: MerchantBackend.Tips.TipCreateConfirmation;
- request: MerchantBackend.Tips.TipCreateRequest;
+ onConfirm: (value: MerchantBackend.Rewards.RewardCreateRequest) => void;
+ rewardAuthorized?: {
+ response: MerchantBackend.Rewards.RewardCreateConfirmation;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
};
}
-export function AuthorizeTipModal({
+export function AuthorizeRewardModal({
onCancel,
onConfirm,
- tipAuthorized,
-}: AuthorizeTipModalProps): VNode {
+ rewardAuthorized,
+}: AuthorizeRewardModalProps): VNode {
// const result = useOrderDetails(id)
- type State = MerchantBackend.Tips.TipCreateRequest;
+ 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 {
- AuthorizeTipSchema.validateSync(form, { abortEarly: false });
+ AuthorizeRewardSchema.validateSync(form, { abortEarly: false });
} catch (err) {
if (err instanceof yup.ValidationError) {
const yupErrors = err.inner as any[];
@@ -77,12 +77,12 @@ export function AuthorizeTipModal({
const validateAndConfirm = () => {
onConfirm(form as State);
};
- if (tipAuthorized) {
+ if (rewardAuthorized) {
return (
- <ContinueModal description="tip" active onConfirm={onCancel}>
+ <ContinueModal description="reward" active onConfirm={onCancel}>
<CreatedSuccessfully
- entity={tipAuthorized.response}
- request={tipAuthorized.request}
+ entity={rewardAuthorized.response}
+ request={rewardAuthorized.request}
onConfirm={onCancel}
/>
</ContinueModal>
@@ -91,7 +91,7 @@ export function AuthorizeTipModal({
return (
<ConfirmModal
- description="tip"
+ description="New reward"
active
onCancel={onCancel}
disabled={hasErrors}
@@ -105,18 +105,18 @@ export function AuthorizeTipModal({
<InputCurrency<State>
name="amount"
label={i18n.str`Amount`}
- tooltip={i18n.str`amount of tip`}
+ tooltip={i18n.str`amount of reward`}
/>
<Input<State>
name="justification"
label={i18n.str`Justification`}
inputType="multiline"
- tooltip={i18n.str`reason for the tip`}
+ tooltip={i18n.str`reason for the reward`}
/>
<Input<State>
name="next_url"
- label={i18n.str`URL after tip`}
- tooltip={i18n.str`URL to visit after tip payment`}
+ 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
index 643651b52..b78236bc7 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/CreatedSuccessfully.tsx
@@ -17,12 +17,13 @@ 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.Tips.TipCreateConfirmation;
+type Entity = MerchantBackend.Rewards.RewardCreateConfirmation;
interface Props {
entity: Entity;
- request: MerchantBackend.Tips.TipCreateRequest;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
onConfirm: () => void;
onCreateAnother?: () => void;
}
@@ -33,6 +34,7 @@ export function CreatedSuccessfully({
onConfirm,
onCreateAnother,
}: Props): VNode {
+ const [settings] = useSettings();
return (
<Fragment>
<div class="field is-horizontal">
@@ -66,7 +68,7 @@ export function CreatedSuccessfully({
<div class="field-body is-flex-grow-3">
<div class="field">
<p class="control">
- <input readonly class="input" value={entity.tip_status_url} />
+ <input readonly class="input" value={entity.reward_status_url} />
</p>
</div>
</div>
@@ -82,13 +84,13 @@ export function CreatedSuccessfully({
class="input"
readonly
value={
- !entity.tip_expiration ||
- entity.tip_expiration.t_s === "never"
+ !entity.reward_expiration ||
+ entity.reward_expiration.t_s === "never"
? "never"
: format(
- entity.tip_expiration.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss",
- )
+ entity.reward_expiration.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
}
/>
</p>
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
index fe305f4fd..b070bbde3 100644
--- 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
@@ -25,12 +25,6 @@ import { CardTable as TestedComponent } from "./Table.js";
export default {
title: "Pages/Reserve/List",
component: TestedComponent,
- argTypes: {
- onCreate: { action: "onCreate" },
- onDelete: { action: "onDelete" },
- onNewTip: { action: "onNewTip" },
- onSelect: { action: "onSelect" },
- },
};
function createExample<Props>(
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
index 1f229d7cb..795e7ec82 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/Table.tsx
@@ -23,12 +23,13 @@ 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.Tips.ReserveStatusEntry & WithId;
+type Entity = MerchantBackend.Rewards.ReserveStatusEntry & WithId;
interface Props {
instances: Entity[];
- onNewTip: (id: Entity) => void;
+ onNewReward: (id: Entity) => void;
onSelect: (id: Entity) => void;
onDelete: (id: Entity) => void;
onCreate: () => void;
@@ -38,7 +39,7 @@ export function CardTable({
instances,
onCreate,
onSelect,
- onNewTip,
+ onNewReward,
onDelete,
}: Props): VNode {
const [withoutFunds, withFunds] = instances.reduce((prev, current) => {
@@ -70,7 +71,7 @@ export function CardTable({
<div class="table-wrapper has-mobile-cards">
<TableWithoutFund
instances={withoutFunds}
- onNewTip={onNewTip}
+ onNewReward={onNewReward}
onSelect={onSelect}
onDelete={onDelete}
/>
@@ -108,7 +109,7 @@ export function CardTable({
{withFunds.length > 0 ? (
<Table
instances={withFunds}
- onNewTip={onNewTip}
+ onNewReward={onNewReward}
onSelect={onSelect}
onDelete={onDelete}
/>
@@ -124,13 +125,14 @@ export function CardTable({
}
interface TableProps {
instances: Entity[];
- onNewTip: (id: Entity) => void;
+ onNewReward: (id: Entity) => void;
onDelete: (id: Entity) => void;
onSelect: (id: Entity) => void;
}
-function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
+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">
@@ -164,7 +166,7 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
>
{i.creation_time.t_s === "never"
? "never"
- : format(i.creation_time.t_s * 1000, "yyyy/MM/dd HH:mm:ss")}
+ : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
</td>
<td
onClick={(): void => onSelect(i)}
@@ -173,9 +175,9 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
{i.expiration_time.t_s === "never"
? "never"
: format(
- i.expiration_time.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss",
- )}
+ i.expiration_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )}
</td>
<td
onClick={(): void => onSelect(i)}
@@ -207,11 +209,11 @@ function Table({ instances, onNewTip, onSelect, onDelete }: TableProps): VNode {
</button>
<button
class="button is-small is-info has-tooltip-left"
- data-tooltip={i18n.str`authorize new tip from selected reserve`}
+ data-tooltip={i18n.str`authorize new reward from selected reserve`}
type="button"
- onClick={(): void => onNewTip(i)}
+ onClick={(): void => onNewReward(i)}
>
- New Tip
+ New Reward
</button>
</div>
</td>
@@ -249,6 +251,7 @@ function TableWithoutFund({
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">
@@ -276,7 +279,7 @@ function TableWithoutFund({
>
{i.creation_time.t_s === "never"
? "never"
- : format(i.creation_time.t_s * 1000, "yyyy/MM/dd HH:mm:ss")}
+ : format(i.creation_time.t_s * 1000, datetimeFormatForSettings(settings))}
</td>
<td
onClick={(): void => onSelect(i)}
@@ -285,9 +288,9 @@ function TableWithoutFund({
{i.expiration_time.t_s === "never"
? "never"
: format(
- i.expiration_time.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss",
- )}
+ i.expiration_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )}
</td>
<td
onClick={(): void => onSelect(i)}
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
index 14387c2a9..b26ff0000 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx
@@ -34,9 +34,10 @@ import {
useReservesAPI,
} from "../../../../hooks/reserves.js";
import { Notification } from "../../../../utils/types.js";
-import { AuthorizeTipModal } from "./AutorizeTipModal.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;
@@ -46,12 +47,12 @@ interface Props {
onCreate: () => void;
}
-interface TipConfirmation {
- response: MerchantBackend.Tips.TipCreateConfirmation;
- request: MerchantBackend.Tips.TipCreateRequest;
+interface RewardConfirmation {
+ response: MerchantBackend.Rewards.RewardCreateConfirmation;
+ request: MerchantBackend.Rewards.RewardCreateRequest;
}
-export default function ListTips({
+export default function ListRewards({
onUnauthorized,
onLoadError,
onNotFound,
@@ -59,14 +60,16 @@ export default function ListTips({
onCreate,
}: Props): VNode {
const result = useInstanceReserves();
- const { deleteReserve, authorizeTipReserve } = useReservesAPI();
+ const { deleteReserve, authorizeRewardReserve } = useReservesAPI();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const [reserveForTip, setReserveForTip] = useState<string | undefined>(
+ const [reserveForReward, setReserveForReward] = useState<string | undefined>(
undefined,
);
- const [tipAuthorized, setTipAuthorized] = useState<
- TipConfirmation | undefined
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Rewards.ReserveStatusEntry | null>(null);
+ const [rewardAuthorized, setRewardAuthorized] = useState<
+ RewardConfirmation | undefined
>(undefined);
if (result.loading) return <Loading />;
@@ -88,30 +91,30 @@ export default function ListTips({
<section class="section is-main-section">
<NotificationCard notification={notif} />
- {reserveForTip && (
- <AuthorizeTipModal
+ {reserveForReward && (
+ <AuthorizeRewardModal
onCancel={() => {
- setReserveForTip(undefined);
- setTipAuthorized(undefined);
+ setReserveForReward(undefined);
+ setRewardAuthorized(undefined);
}}
- tipAuthorized={tipAuthorized}
+ rewardAuthorized={rewardAuthorized}
onConfirm={async (request) => {
try {
- const response = await authorizeTipReserve(
- reserveForTip,
+ const response = await authorizeRewardReserve(
+ reserveForReward,
request,
);
- setTipAuthorized({
+ setRewardAuthorized({
request,
response: response.data,
});
} catch (error) {
setNotif({
- message: i18n.str`could not create the tip`,
+ message: i18n.str`could not create the reward`,
type: "ERROR",
description: error instanceof Error ? error.message : undefined,
});
- setReserveForTip(undefined);
+ setReserveForReward(undefined);
}
}}
/>
@@ -122,10 +125,47 @@ export default function ListTips({
.filter((r) => r.active)
.map((o) => ({ ...o, id: o.reserve_pub }))}
onCreate={onCreate}
- onDelete={(reserve) => deleteReserve(reserve.reserve_pub)}
+ onDelete={(reserve) => {
+ setDeleting(reserve)
+ }}
onSelect={(reserve) => onSelect(reserve.id)}
- onNewTip={(reserve) => setReserveForTip(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>&quot;{deleting.merchant_initial_amount}&quot;</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/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx
index e20b9bc27..8629d8dee 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
@@ -24,7 +24,7 @@ import {
MerchantTemplateContractDetails,
} 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 {
@@ -35,17 +35,16 @@ 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 { InputSelector } from "../../../../components/form/InputSelector.js";
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
import { useBackendContext } from "../../../../context/backend.js";
+import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js";
import {
- isBase32RFC3548Charset,
- randomBase32Key,
+ isBase32RFC3548Charset
} from "../../../../utils/crypto.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
-import { QR } from "../../../../components/exception/QR.js";
-import { useInstanceContext } from "../../../../context/instance.js";
+import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
+import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
type Entity = MerchantBackend.Template.TemplateAddDetails;
@@ -54,16 +53,11 @@ interface Props {
onBack?: () => void;
}
-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 { id: instanceId } = useInstanceContext();
- const issuer = new URL(backend.url).hostname;
+ const devices = useInstanceOtpDevices()
- const [showKey, setShowKey] = useState(false);
const [state, setState] = useState<Partial<Entity>>({
template_contract: {
minimum_age: 0,
@@ -78,7 +72,11 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
: Amounts.parse(state.template_contract?.amount);
const errors: FormErrors<Entity> = {
- template_id: !state.template_id ? i18n.str`should not be empty` : undefined,
+ template_id: !state.template_id
+ ? i18n.str`should not be empty`
+ : !/[a-zA-Z0-9]*/.test(state.template_id)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
template_description: !state.template_description
? i18n.str`should not be empty`
: undefined,
@@ -104,15 +102,6 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
? i18n.str`to short`
: undefined,
} as Partial<MerchantTemplateContractDetails>),
- pos_key: !state.pos_key
- ? !state.pos_algorithm
- ? undefined
- : i18n.str`required`
- : !isBase32RFC3548Charset(state.pos_key)
- ? i18n.str`just letters and numbers from 2 to 7`
- : state.pos_key.length !== 32
- ? i18n.str`size of the key should be 32`
- : undefined,
};
const hasErrors = Object.keys(errors).some(
@@ -124,7 +113,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
return onCreate(state as any);
};
- const qrText = `otpauth://totp/${instanceId}/${state.template_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`;
+ const deviceList = !devices.ok ? [] : devices.data.otp_devices
return (
<div>
@@ -139,7 +128,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
>
<InputWithAddon<Entity>
name="template_id"
- help={`${backend.url}/instances/templates/${state.template_id ?? ""}`}
+ help={`${backend.url}/templates/${state.template_id ?? ""}`}
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`}
/>
@@ -172,83 +161,21 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
help=""
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
/>
- <InputSelector<Entity>
- name="pos_algorithm"
- label={i18n.str`Verification algorithm`}
- tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
- values={algorithms}
- toStr={(v) => algorithmsNames[v]}
- fromStr={(v) => Number(v)}
+ <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
+ }))}
/>
- {state.pos_algorithm && state.pos_algorithm > 0 ? (
- <Fragment>
- <InputWithAddon<Entity>
- name="pos_key"
- label={i18n.str`Point-of-sale key`}
- inputType={showKey ? "text" : "password"}
- help="Be sure to be very hard to guess or use the random generator"
- tooltip={i18n.str`Useful to validate the purchase`}
- fromStr={(v) => v.toUpperCase()}
- addonAfter={
- <span class="icon">
- {showKey ? (
- <i class="mdi mdi-eye" />
- ) : (
- <i class="mdi mdi-eye-off" />
- )}
- </span>
- }
- side={
- <span style={{ display: "flex" }}>
- <button
- data-tooltip={i18n.str`generate random secret key`}
- class="button is-info mr-3"
- onClick={(e) => {
- const pos_key = randomBase32Key();
- setState((s) => ({ ...s, pos_key }));
- }}
- >
- <i18n.Translate>random</i18n.Translate>
- </button>
- <button
- data-tooltip={
- showKey
- ? i18n.str`show secret key`
- : i18n.str`hide secret key`
- }
- class="button is-info mr-3"
- onClick={(e) => {
- setShowKey(!showKey);
- }}
- >
- {showKey ? (
- <i18n.Translate>hide</i18n.Translate>
- ) : (
- <i18n.Translate>show</i18n.Translate>
- )}
- </button>
- </span>
- }
- />
- {showKey && (
- <Fragment>
- <QR text={qrText} />
- <div
- style={{
- color: "grey",
- fontSize: "small",
- width: 200,
- textAlign: "center",
- margin: "auto",
- wordBreak: "break-all",
- }}
- >
- {qrText}
- </div>
- </Fragment>
- )}
- </Fragment>
- ) : undefined}
+
</FormProvider>
<div class="buttons is-right mt-5">
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 2f91298bf..3c9bb231c 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
@@ -36,6 +36,7 @@ import {
import { Notification } from "../../../../utils/types.js";
import { ListPage } from "./ListPage.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConfirmModal } from "../../../../components/modal/index.js";
interface Props {
onUnauthorized: () => VNode;
@@ -61,6 +62,8 @@ export default function ListTemplates({
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { deleteTemplate } = useTemplateAPI();
const result = useInstanceTemplates({ position }, (id) => setPosition(id));
+ const [deleting, setDeleting] =
+ useState<MerchantBackend.Template.TemplateEntry | null>(null);
if (result.loading) return <Loading />;
if (!result.ok) {
@@ -97,23 +100,45 @@ export default function ListTemplates({
onQR={(e) => {
onQR(e.template_id);
}}
- onDelete={(e: MerchantBackend.Template.TemplateEntry) =>
- deleteTemplate(e.template_id)
- .then(() =>
+ onDelete={(e: MerchantBackend.Template.TemplateEntry) => {
+ setDeleting(e)
+ }
+ }
+ />
+
+ {deleting && (
+ <ConfirmModal
+ label={`Delete template`}
+ description={`Delete the template "${deleting.template_description}"`}
+ danger
+ active
+ onCancel={() => setDeleting(null)}
+ onConfirm={async (): Promise<void> => {
+ try {
+ await deleteTemplate(deleting.template_id);
setNotif({
- message: i18n.str`template delete successfully`,
+ message: i18n.str`Template "${deleting.template_description}" (ID: ${deleting.template_id}) has been deleted`,
type: "SUCCESS",
- }),
- )
- .catch((error) =>
+ });
+ } catch (error) {
setNotif({
- message: i18n.str`could not delete the template`,
+ message: i18n.str`Failed to delete template`,
type: "ERROR",
- description: error.message,
- }),
- )
- }
- />
+ description: error instanceof Error ? error.message : undefined,
+ });
+ }
+ setDeleting(null);
+ }}
+ >
+ <p>
+ If you delete the template <b>&quot;{deleting.template_description}&quot;</b> (ID:{" "}
+ <b>{deleting.template_id}</b>) you may loose information
+ </p>
+ <p class="warning">
+ Deleting an template <b>cannot be undone</b>.
+ </p>
+ </ConfirmModal>
+ )}
</Fragment>
);
}
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 0f30efafd..c65cf6a19 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
@@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { QR } from "../../../../components/exception/QR.js";
@@ -35,35 +35,32 @@ import { useConfigContext } from "../../../../context/config.js";
import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js";
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
+import { useOtpDeviceDetails } from "../../../../hooks/otp.js";
+import { Loading } from "../../../../components/exception/loading.js";
type Entity = MerchantBackend.Template.UsingTemplateDetails;
interface Props {
- template: MerchantBackend.Template.TemplateDetails;
+ contract: MerchantBackend.Template.TemplateContractDetails;
id: string;
onBack?: () => void;
}
-export function QrPage({ template, id: templateId, onBack }: Props): VNode {
+export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const { url: backendUrl } = useBackendContext();
const { id: instanceId } = useInstanceContext();
const config = useConfigContext();
- const [setupTOTP, setSetupTOTP] = useState(false);
const [state, setState] = useState<Partial<Entity>>({
- amount: template.template_contract.amount,
- summary: template.template_contract.summary,
+ amount: contract.amount,
+ summary: contract.summary,
});
const errors: FormErrors<Entity> = {};
- const hasErrors = Object.keys(errors).some(
- (k) => (errors as any)[k] !== undefined,
- );
-
- const fixedAmount = !!template.template_contract.amount;
- const fixedSummary = !!template.template_contract.summary;
+ const fixedAmount = !!contract.amount;
+ const fixedSummary = !!contract.summary;
const templateParams: Record<string, string> = {}
if (!fixedAmount) {
@@ -89,40 +86,9 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
const issuer = encodeURIComponent(
`${new URL(backendUrl).host}/${instanceId}`,
);
- const oauthUri = !template.pos_algorithm
- ? undefined
- : template.pos_algorithm === 1
- ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
- : template.pos_algorithm === 2
- ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
- : undefined;
-
- const keySlice = template.pos_key?.substring(0, 4);
-
- const oauthUriWithoutSecret = !template.pos_algorithm
- ? undefined
- : template.pos_algorithm === 1
- ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
- : template.pos_algorithm === 2
- ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
- : undefined;
+
return (
<div>
- {oauthUri && (
- <ConfirmModal
- description="Setup TOTP"
- active={setupTOTP}
- onCancel={() => {
- setSetupTOTP(false);
- }}
- >
- <p>Scan this qr code with your TOTP device</p>
- <QR text={oauthUri} />
- <pre style={{ textAlign: "center" }}>
- <a href={oauthUri}>{oauthUriWithoutSecret}</a>
- </pre>
- </ConfirmModal>
- )}
<section class="section is-main-section">
<div class="columns">
<div class="column" />
@@ -176,14 +142,6 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
>
<i18n.Translate>Print</i18n.Translate>
</button>
- {oauthUri && (
- <button
- class="button is-info"
- onClick={() => setSetupTOTP(true)}
- >
- <i18n.Translate>Setup TOTP</i18n.Translate>
- </button>
- )}
</div>
</div>
<div class="column" />
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 1f74afc2b..7db7478f7 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
@@ -74,7 +74,7 @@ export default function TemplateQrPage({
return (
<>
<NotificationCard notification={notif} />
- <QrPage template={result.data} id={tid} onBack={onBack} />
+ <QrPage contract={result.data.template_contract} id={tid} onBack={onBack} />
</>
);
}
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 30e5502bb..30d47385c 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
@@ -61,10 +61,7 @@ const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
const backend = useBackendContext();
- const { id: instanceId } = useInstanceContext();
- const issuer = new URL(backend.url).hostname;
- const [showKey, setShowKey] = useState(false);
const [state, setState] = useState<Partial<Entity>>(template);
const parsedPrice = !state.template_contract?.amount
@@ -78,34 +75,25 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
template_contract: !state.template_contract
? undefined
: undefinedIfEmpty({
- amount: !state.template_contract?.amount
- ? undefined
- : !parsedPrice
+ amount: !state.template_contract?.amount
+ ? undefined
+ : !parsedPrice
? i18n.str`not valid`
: Amounts.isZero(parsedPrice)
- ? i18n.str`must be greater than 0`
- : undefined,
- minimum_age:
- state.template_contract.minimum_age < 0
- ? i18n.str`should be greater that 0`
+ ? i18n.str`must be greater than 0`
: undefined,
- pay_duration: !state.template_contract.pay_duration
- ? i18n.str`can't be empty`
- : state.template_contract.pay_duration.d_us === "forever"
+ 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>),
- pos_key: !state.pos_key
- ? !state.pos_algorithm
- ? undefined
- : i18n.str`required`
- : !isBase32RFC3548Charset(state.pos_key)
- ? i18n.str`just letters and numbers from 2 to 7`
- : state.pos_key.length !== 32
- ? i18n.str`size of the key should be 32`
- : undefined,
+ ? i18n.str`to short`
+ : undefined,
+ } as Partial<MerchantTemplateContractDetails>),
};
const hasErrors = Object.keys(errors).some(
@@ -117,7 +105,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
return onUpdate(state as any);
};
- const qrText = `otpauth://totp/${instanceId}/${state.id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${state.pos_key}`;
return (
<div>
@@ -128,7 +115,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
- {backend.url}/instances/template/{template.id}
+ {backend.url}/templates/{template.id}
</span>
</div>
</div>
@@ -182,84 +169,6 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
help=""
tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`}
/>
- <InputSelector<Entity>
- name="pos_algorithm"
- label={i18n.str`Verification algorithm`}
- tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
- values={algorithms}
- toStr={(v) => algorithmsNames[v]}
- fromStr={(v) => Number(v)}
- />
- {state.pos_algorithm && state.pos_algorithm > 0 ? (
- <Fragment>
- <InputWithAddon<Entity>
- name="pos_key"
- label={i18n.str`Point-of-sale key`}
- inputType={showKey ? "text" : "password"}
- help="Be sure to be very hard to guess or use the random generator"
- expand
- tooltip={i18n.str`Useful to validate the purchase`}
- fromStr={(v) => v.toUpperCase()}
- addonAfter={
- <span class="icon">
- {showKey ? (
- <i class="mdi mdi-eye" />
- ) : (
- <i class="mdi mdi-eye-off" />
- )}
- </span>
- }
- side={
- <span style={{ display: "flex" }}>
- <button
- data-tooltip={i18n.str`generate random secret key`}
- class="button is-info mr-3"
- onClick={(e) => {
- const pos_key = randomBase32Key();
- setState((s) => ({ ...s, pos_key }));
- }}
- >
- <i18n.Translate>random</i18n.Translate>
- </button>
- <button
- data-tooltip={
- showKey
- ? i18n.str`show secret key`
- : i18n.str`hide secret key`
- }
- class="button is-info mr-3"
- onClick={(e) => {
- setShowKey(!showKey);
- }}
- >
- {showKey ? (
- <i18n.Translate>hide</i18n.Translate>
- ) : (
- <i18n.Translate>show</i18n.Translate>
- )}
- </button>
- </span>
- }
- />
- {showKey && (
- <Fragment>
- <QR text={qrText} />
- <div
- style={{
- color: "grey",
- fontSize: "small",
- width: 200,
- textAlign: "center",
- margin: "auto",
- wordBreak: "break-all",
- }}
- >
- {qrText}
- </div>
- </Fragment>
- )}
- </Fragment>
- ) : undefined}
</FormProvider>
<div class="buttons is-right mt-5">
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
new file mode 100644
index 000000000..6ab2a2df6
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -0,0 +1,165 @@
+/*
+ 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 { 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";
+
+interface Props {
+ instanceId: string;
+ currentToken: string | undefined;
+ onClearToken: () => void;
+ onNewToken: (s: string) => void;
+ onBack?: () => void;
+}
+
+export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewToken, onClearToken }: Props): VNode {
+ type State = { old_token: string; new_token: string; repeat_token: string };
+ const [form, setValue] = useState<Partial<State>>({
+ old_token: "",
+ new_token: "",
+ repeat_token: "",
+ });
+ const { i18n } = useTranslationContext();
+
+ const hasOldtoken = !!oldToken
+ const hasInputTheCorrectOldToken = hasOldtoken && oldToken !== form.old_token;
+ const errors = {
+ old_token: hasInputTheCorrectOldToken
+ ? i18n.str`is not the same as the current access token`
+ : undefined,
+ 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,
+ repeat_token:
+ form.new_token !== form.repeat_token
+ ? i18n.str`is not the same`
+ : undefined,
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const instance = useInstanceContext();
+
+ const text = i18n.str`You are updating the access token from instance with id ${instance.id}`;
+
+ async function submitForm() {
+ if (hasErrors) return;
+ onNewToken(form.new_token as any)
+ }
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ Instace id: <b>{instanceId}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider errors={errors} object={form} valueHandler={setValue}>
+ {hasOldtoken && (
+ <Input<State>
+ name="old_token"
+ label={i18n.str`Current access token`}
+ tooltip={i18n.str`access token currently in use`}
+ inputType="password"
+ />
+ )}
+ {!hasInputTheCorrectOldToken && <Fragment>
+ {hasOldtoken && <Fragment>
+ <p>
+ <i18n.Translate>
+ Clearing the access token will mean public access to the instance.
+ </i18n.Translate>
+ </p>
+ <div class="buttons is-right mt-5">
+ <button
+ disabled={!!hasInputTheCorrectOldToken}
+ class="button"
+ onClick={onClearToken}
+ >
+ <i18n.Translate>Clear token</i18n.Translate>
+ </button>
+ </div>
+ </Fragment>
+ }
+
+ <Input<State>
+ name="new_token"
+ label={i18n.str`New access token`}
+ tooltip={i18n.str`next access token to be used`}
+ inputType="password"
+ />
+ <Input<State>
+ name="repeat_token"
+ label={i18n.str`Repeat access token`}
+ tooltip={i18n.str`confirm the same access token`}
+ inputType="password"
+ />
+ </Fragment>}
+ </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
new file mode 100644
index 000000000..d5910361b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
@@ -0,0 +1,90 @@
+/*
+ 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 { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { Loading } from "../../../components/exception/loading.js";
+import { 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 { NotificationCard } from "../../../components/menu/index.js";
+import { Notification } from "../../../utils/types.js";
+import { useBackendContext } from "../../../context/backend.js";
+
+interface Props {
+ onUnauthorized: () => VNode;
+ onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode;
+ onChange: () => void;
+ onNotFound: () => VNode;
+}
+
+const PREFIX = "secret-token:"
+
+export default function Token({
+ onLoadError,
+ onChange,
+ onUnauthorized,
+ onNotFound,
+}: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { clearToken, setNewToken } = useInstanceAPI();
+ const { token: rootToken } = useBackendContext();
+ const { token: instanceToken, id, admin } = useInstanceContext();
+
+ const currentToken = !admin ? rootToken : instanceToken
+ const hasPrefix = currentToken !== undefined && currentToken.startsWith(PREFIX)
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <DetailPage
+ instanceId={id}
+ currentToken={hasPrefix ? currentToken.substring(PREFIX.length) : currentToken}
+ onClearToken={async (): Promise<void> => {
+ try {
+ await clearToken();
+ onChange();
+ } catch (error) {
+ if (error instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to clear token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ onNewToken={async (newToken): Promise<void> => {
+ try {
+ await setNewToken(`secret-token:${newToken}`);
+ onChange();
+ } catch (error) {
+ if (error instanceof Error) {
+ setNotif({
+ message: i18n.str`Failed to set new token`,
+ type: "ERROR",
+ description: error.message,
+ });
+ }
+ }
+ }}
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx
new file mode 100644
index 000000000..5f0f56f2d
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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 { DetailPage as TestedComponent } from "./DetailPage.js";
+
+export default {
+ title: "Pages/Token",
+ component: TestedComponent,
+};
+
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 f218f4ead..25551a031 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
@@ -28,6 +28,7 @@ import { useInstanceDetails } from "../../../../hooks/instance.js";
import { useTransferAPI } from "../../../../hooks/transfer.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;
interface Props {
@@ -39,7 +40,7 @@ export default function CreateTransfer({ onConfirm, onBack }: Props): VNode {
const { informTransfer } = useTransferAPI();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const instance = useInstanceDetails();
+ const instance = useInstanceBankAccounts();
const accounts = !instance.ok
? []
: instance.data.accounts.map((a) => a.payto_uri);
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 a2e93d598..1c464cbc7 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
@@ -24,6 +24,7 @@ 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";
type Entity = MerchantBackend.Transfers.TransferDetails & WithId;
@@ -56,7 +57,7 @@ export function CardTable({
<header class="card-header">
<p class="card-header-title">
<span class="icon">
- <i class="mdi mdi-bank" />
+ <i class="mdi mdi-arrow-left-right" />
</span>
<i18n.Translate>Transfers</i18n.Translate>
</p>
@@ -121,6 +122,7 @@ function Table({
hasMoreBefore,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
+ const [settings] = useSettings();
return (
<div class="table-container">
{onLoadMoreBefore && (
@@ -175,9 +177,9 @@ function Table({
? i.execution_time.t_s == "never"
? i18n.str`never`
: format(
- i.execution_time.t_s * 1000,
- "yyyy/MM/dd HH:mm:ss",
- )
+ i.execution_time.t_s * 1000,
+ datetimeFormatForSettings(settings),
+ )
: i18n.str`unknown`}
</td>
<td>
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 29e860342..1bc1673ba 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
@@ -28,6 +28,7 @@ import { useInstanceDetails } from "../../../../hooks/instance.js";
import { useInstanceTransfers } from "../../../../hooks/transfer.js";
import { ListPage } from "./ListPage.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
interface Props {
onUnauthorized: () => VNode;
@@ -51,7 +52,7 @@ export default function ListTransfer({
const [position, setPosition] = useState<string | undefined>(undefined);
- const instance = useInstanceDetails();
+ const instance = useInstanceBankAccounts();
const accounts = !instance.ok
? []
: instance.data.accounts.map((a) => a.payto_uri);
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 045c96c2c..817a7025c 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
@@ -42,17 +42,15 @@ function createExample<Props>(
export const Example = createExample(TestedComponent, {
selected: {
- accounts: [],
name: "name",
auth: { method: "external" },
address: {},
+ user_type: "business",
+ use_stefan: true,
jurisdiction: {},
- default_max_deposit_fee: "TESTKUDOS:2",
- default_max_wire_fee: "TESTKUDOS:1",
default_pay_delay: {
d_us: 1000 * 1000, //one second
},
- default_wire_fee_amortization: 1,
default_wire_transfer_delay: {
d_us: 1000 * 1000, //one second
},
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 547b40f07..a1c608f15 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
@@ -19,7 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { Amounts } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -29,10 +28,8 @@ import {
FormProvider,
} from "../../../components/form/FormProvider.js";
import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
-import { UpdateTokenModal } from "../../../components/modal/index.js";
import { useInstanceContext } from "../../../context/instance.js";
import { MerchantBackend } from "../../../declaration.js";
-import { PAYTO_REGEX } from "../../../utils/constants.js";
import { undefinedIfEmpty } from "../../../utils/table.js";
type Entity = MerchantBackend.Instances.InstanceReconfigurationMessage & {
@@ -53,23 +50,23 @@ interface Props {
function convert(
from: MerchantBackend.Instances.QueryInstancesResponse,
): Entity {
- const { accounts: qAccounts, ...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 { ...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 defaults = {
- default_wire_fee_amortization: 1,
+ 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
};
- return { ...defaults, ...rest, accounts };
+ return { ...defaults, ...rest };
}
function getTokenValuePart(t?: string): string | undefined {
@@ -85,21 +82,21 @@ export function UpdatePage({
selected,
onBack,
}: Props): VNode {
- const { id, token } = 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 { 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 [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
@@ -110,35 +107,7 @@ export function UpdatePage({
user_type: !value.user_type
? i18n.str`required`
: value.user_type !== "business" && value.user_type !== "individual"
- ? i18n.str`should be business or individual`
- : undefined,
- accounts:
- !value.accounts || !value.accounts.length
- ? i18n.str`required`
- : undefinedIfEmpty(
- value.accounts.map((p) => {
- return !PAYTO_REGEX.test(p.payto_uri)
- ? i18n.str`is not valid`
- : undefined;
- }),
- ),
- default_max_deposit_fee: !value.default_max_deposit_fee
- ? i18n.str`required`
- : !Amounts.parse(value.default_max_deposit_fee)
- ? i18n.str`invalid format`
- : undefined,
- default_max_wire_fee: !value.default_max_wire_fee
- ? i18n.str`required`
- : !Amounts.parse(value.default_max_wire_fee)
- ? i18n.str`invalid format`
- : undefined,
- default_wire_fee_amortization:
- value.default_wire_fee_amortization === undefined
- ? i18n.str`required`
- : isNaN(value.default_wire_fee_amortization)
- ? i18n.str`is not a number`
- : value.default_wire_fee_amortization < 1
- ? i18n.str`must be 1 or greater`
+ ? i18n.str`should be business or individual`
: undefined,
default_pay_delay: !value.default_pay_delay
? i18n.str`required`
@@ -163,10 +132,11 @@ export function UpdatePage({
const hasErrors = Object.keys(errors).some(
(k) => (errors as any)[k] !== undefined,
);
+
const submit = async (): Promise<void> => {
await onUpdate(value as Entity);
};
- const [active, setActive] = useState(false);
+ // const [active, setActive] = useState(false);
return (
<div>
@@ -181,7 +151,7 @@ export function UpdatePage({
</span>
</div>
</div>
- <div class="level-right">
+ {/* <div class="level-right">
<div class="level-item">
<h1 class="title">
<button
@@ -200,33 +170,11 @@ export function UpdatePage({
</button>
</h1>
</div>
- </div>
+ </div> */}
</div>
</div>
</section>
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- {active && (
- <UpdateTokenModal
- oldToken={currentTokenValue}
- onCancel={() => {
- setActive(false);
- }}
- onClear={() => {
- updateToken(null);
- setActive(false);
- }}
- onConfirm={(newToken) => {
- updateToken(newToken);
- setActive(false);
- }}
- />
- )}
- </div>
- <div class="column" />
- </div>
<hr />
<div class="columns">
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/Create.stories.tsx
new file mode 100644
index 000000000..56762db7b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/Create.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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/Validators/Create",
+ component: TestedComponent,
+};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx
new file mode 100644
index 000000000..bdc86d226
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx
@@ -0,0 +1,195 @@
+/*
+ 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 { 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 { 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;
+
+interface Props {
+ onCreate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+}
+
+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`
+ : !/[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)
+ ? 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_description: !state.otp_description ? i18n.str`required`
+ : !/[a-zA-Z0-9]*/.test(state.otp_description)
+ ? i18n.str`no valid. only characters and numbers`
+ : undefined,
+
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onCreate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="otp_device_id"
+ label={i18n.str`ID`}
+ tooltip={i18n.str`Internal id on the system`}
+ />
+ <Input<Entity>
+ name="otp_description"
+ label={i18n.str`Descripiton`}
+ tooltip={i18n.str`Useful to identify the device physically`}
+ />
+ <InputSelector<Entity>
+ name="otp_algorithm"
+ label={i18n.str`Verification algorithm`}
+ tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
+ values={algorithms}
+ toStr={(v) => algorithmsNames[v]}
+ fromStr={(v) => Number(v)}
+ />
+ {state.otp_algorithm && state.otp_algorithm > 0 ? (
+ <Fragment>
+ <InputWithAddon<Entity>
+ name="otp_key"
+ label={i18n.str`Device key`}
+ inputType={showKey ? "text" : "password"}
+ help="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()}
+ addonAfter={
+ <span class="icon">
+ {showKey ? (
+ <i class="mdi mdi-eye" />
+ ) : (
+ <i class="mdi mdi-eye-off" />
+ )}
+ </span>
+ }
+ side={
+ <span style={{ display: "flex" }}>
+ <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setState((s) => ({ ...s, otp_key: randomBase32Key() }));
+ }}
+ >
+ <i18n.Translate>random</i18n.Translate>
+ </button>
+ <button
+ data-tooltip={
+ showKey
+ ? i18n.str`show secret key`
+ : i18n.str`hide secret key`
+ }
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setShowKey(!showKey);
+ }}
+ >
+ {showKey ? (
+ <i18n.Translate>hide</i18n.Translate>
+ ) : (
+ <i18n.Translate>show</i18n.Translate>
+ )}
+ </button>
+ </span>
+ }
+ />
+ </Fragment>
+ ) : undefined}
+ </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</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx
new file mode 100644
index 000000000..3ad3cb3a3
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx
@@ -0,0 +1,104 @@
+/*
+ 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 { 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 { useBackendContext } from "../../../../context/backend.js";
+import { useInstanceContext } from "../../../../context/instance.js";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.OTP.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 backend = useBackendContext();
+ const { id: instanceId } = useInstanceContext();
+ const issuer = new URL(backend.url).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)}...`;
+
+ 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.
+ </i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">ID</label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ readonly
+ class="input"
+ value={entity.otp_device_id}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label"><i18n.Translate>Description</i18n.Translate></label>
+ </div>
+ <div class="field-body is-flex-grow-3">
+ <div class="field">
+ <p class="control">
+ <input
+ class="input"
+ readonly
+ value={entity.otp_description}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <QR
+ text={qrText}
+ />
+ <div
+ style={{
+ color: "grey",
+ fontSize: "small",
+ width: 200,
+ textAlign: "center",
+ margin: "auto",
+ wordBreak: "break-all",
+ }}
+ >
+ {qrTextSafe}
+ </div>
+ </Template>
+ );
+}
+
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/index.tsx
new file mode 100644
index 000000000..648846793
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/index.tsx
@@ -0,0 +1,70 @@
+/*
+ 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 { useWebhookAPI } from "../../../../hooks/webhooks.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;
+interface Props {
+ onBack?: () => void;
+ onConfirm: () => void;
+}
+
+export default function CreateValidator({ onConfirm, onBack }: Props): VNode {
+ const { createOtpDevice } = useOtpDeviceAPI();
+ const [notif, setNotif] = useState<Notification | undefined>(undefined);
+ const { i18n } = useTranslationContext();
+ const [created, setCreated] = useState<MerchantBackend.OTP.OtpDeviceAddDetails | null>(null)
+
+ if (created) {
+ return <CreatedSuccessfully entity={created} onConfirm={onConfirm} />
+ }
+
+ return (
+ <>
+ <NotificationCard notification={notif} />
+ <CreatePage
+ onBack={onBack}
+ onCreate={(request: Entity) => {
+ return createOtpDevice(request)
+ .then((d) => {
+ setCreated(request)
+ })
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not create device`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/List.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/List.stories.tsx
new file mode 100644
index 000000000..3aa491c53
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/List.stories.tsx
@@ -0,0 +1,28 @@
+/*
+ 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 { FunctionalComponent, h } from "preact";
+import { ListPage as TestedComponent } from "./ListPage.js";
+
+export default {
+ title: "Pages/Validators/List",
+ component: TestedComponent,
+};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/ListPage.tsx
new file mode 100644
index 000000000..4efee9781
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/ListPage.tsx
@@ -0,0 +1,64 @@
+/*
+ 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 } 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[];
+ onLoadMoreBefore?: () => void;
+ onLoadMoreAfter?: () => void;
+ onCreate: () => void;
+ onDelete: (e: MerchantBackend.OTP.OtpDeviceEntry) => void;
+ onSelect: (e: MerchantBackend.OTP.OtpDeviceEntry) => void;
+}
+
+export function ListPage({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ onLoadMoreAfter,
+}: Props): VNode {
+ const form = { payto_uri: "" };
+
+ const { i18n } = useTranslationContext();
+ return (
+ <section class="section is-main-section">
+ <CardTable
+ devices={devices.map((o) => ({
+ ...o,
+ id: String(o.otp_device_id),
+ }))}
+ onCreate={onCreate}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreBefore={!onLoadMoreBefore}
+ onLoadMoreAfter={onLoadMoreAfter}
+ hasMoreAfter={!onLoadMoreAfter}
+ />
+ </section>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx
new file mode 100644
index 000000000..b639a6134
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx
@@ -0,0 +1,213 @@
+/*
+ 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 { StateUpdater, useState } from "preact/hooks";
+import { MerchantBackend } from "../../../../declaration.js";
+
+type Entity = MerchantBackend.OTP.OtpDeviceEntry;
+
+interface Props {
+ devices: Entity[];
+ onDelete: (e: Entity) => void;
+ onSelect: (e: Entity) => void;
+ onCreate: () => void;
+ onLoadMoreBefore?: () => void;
+ hasMoreBefore?: boolean;
+ hasMoreAfter?: boolean;
+ onLoadMoreAfter?: () => void;
+}
+
+export function CardTable({
+ devices,
+ onCreate,
+ onDelete,
+ onSelect,
+ onLoadMoreAfter,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: Props): VNode {
+ const [rowSelection, rowSelectionHandler] = useState<string[]>([]);
+
+ const { i18n } = useTranslationContext();
+
+ return (
+ <div class="card has-table">
+ <header class="card-header">
+ <p class="card-header-title">
+ <span class="icon">
+ <i class="mdi mdi-newspaper" />
+ </span>
+ <i18n.Translate>OTP Devices</i18n.Translate>
+ </p>
+ <div class="card-header-icon" aria-label="more options">
+ <span
+ class="has-tooltip-left"
+ data-tooltip={i18n.str`add new devices`}
+ >
+ <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">
+ {devices.length > 0 ? (
+ <Table
+ instances={devices}
+ onDelete={onDelete}
+ onSelect={onSelect}
+ rowSelection={rowSelection}
+ rowSelectionHandler={rowSelectionHandler}
+ onLoadMoreAfter={onLoadMoreAfter}
+ onLoadMoreBefore={onLoadMoreBefore}
+ hasMoreAfter={hasMoreAfter}
+ hasMoreBefore={hasMoreBefore}
+ />
+ ) : (
+ <EmptyTable />
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+interface TableProps {
+ rowSelection: string[];
+ instances: Entity[];
+ 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({
+ instances,
+ onLoadMoreAfter,
+ onDelete,
+ onSelect,
+ onLoadMoreBefore,
+ hasMoreAfter,
+ hasMoreBefore,
+}: TableProps): VNode {
+ const { i18n } = useTranslationContext();
+ return (
+ <div class="table-container">
+ {onLoadMoreBefore && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices before the first one`}
+ disabled={!hasMoreBefore}
+ onClick={onLoadMoreBefore}
+ >
+ <i18n.Translate>load newer devices</i18n.Translate>
+ </button>
+ )}
+ <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
+ <thead>
+ <tr>
+ <th>
+ <i18n.Translate>ID</i18n.Translate>
+ </th>
+ <th>
+ <i18n.Translate>Description</i18n.Translate>
+ </th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ {instances.map((i) => {
+ return (
+ <tr key={i.otp_device_id}>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.otp_device_id}
+ </td>
+ <td
+ onClick={(): void => onSelect(i)}
+ style={{ cursor: "pointer" }}
+ >
+ {i.otp_device_id}
+ </td>
+ <td class="is-actions-cell right-sticky">
+ <div class="buttons is-right">
+ <button
+ class="button is-danger is-small has-tooltip-left"
+ data-tooltip={i18n.str`delete selected devices from the database`}
+ onClick={() => onDelete(i)}
+ >
+ Delete
+ </button>
+ </div>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ {onLoadMoreAfter && (
+ <button
+ class="button is-fullwidth"
+ data-tooltip={i18n.str`load more devices after the last one`}
+ disabled={!hasMoreAfter}
+ onClick={onLoadMoreAfter}
+ >
+ <i18n.Translate>load older devices</i18n.Translate>
+ </button>
+ )}
+ </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 devices yet, add more pressing the + sign
+ </i18n.Translate>
+ </p>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/index.tsx
new file mode 100644
index 000000000..8837c848b
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/list/index.tsx
@@ -0,0 +1,106 @@
+/*
+ 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,
+ HttpError,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } 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 { useInstanceOtpDevices, useOtpDeviceAPI } from "../../../../hooks/otp.js";
+import { Notification } from "../../../../utils/types.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 ListValidators({
+ 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 { deleteOtpDevice } = useOtpDeviceAPI();
+ const result = useInstanceOtpDevices({ position }, (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);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+
+ <ListPage
+ devices={result.data.otp_devices}
+ onLoadMoreBefore={
+ result.isReachingStart ? result.loadMorePrev : undefined
+ }
+ onLoadMoreAfter={result.isReachingEnd ? result.loadMore : undefined}
+ onCreate={onCreate}
+ onSelect={(e) => {
+ onSelect(e.otp_device_id);
+ }}
+ onDelete={(e: MerchantBackend.OTP.OtpDeviceEntry) =>
+ deleteOtpDevice(e.otp_device_id)
+ .then(() =>
+ setNotif({
+ message: i18n.str`validator delete successfully`,
+ type: "SUCCESS",
+ }),
+ )
+ .catch((error) =>
+ setNotif({
+ message: i18n.str`could not delete the validator`,
+ type: "ERROR",
+ description: error.message,
+ }),
+ )
+ }
+ />
+ </Fragment>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/Update.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/Update.stories.tsx
new file mode 100644
index 000000000..fcb77b820
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/Update.stories.tsx
@@ -0,0 +1,32 @@
+/*
+ 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 { UpdatePage as TestedComponent } from "./UpdatePage.js";
+
+export default {
+ title: "Pages/Validators/Update",
+ component: TestedComponent,
+ argTypes: {
+ onUpdate: { action: "onUpdate" },
+ onBack: { action: "onBack" },
+ },
+};
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx
new file mode 100644
index 000000000..585c12e11
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx
@@ -0,0 +1,185 @@
+/*
+ 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 { AsyncButton } from "../../../../components/exception/AsyncButton.js";
+import {
+ FormErrors,
+ 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;
+
+interface Props {
+ onUpdate: (d: Entity) => Promise<void>;
+ onBack?: () => void;
+ device: Entity;
+}
+const algorithms = [0, 1, 2];
+const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
+export function UpdatePage({ device, onUpdate, onBack }: Props): VNode {
+ const { i18n } = useTranslationContext();
+
+ const [state, setState] = useState<Partial<Entity>>(device);
+ const [showKey, setShowKey] = useState(false);
+
+ const errors: FormErrors<Entity> = {
+ };
+
+ const hasErrors = Object.keys(errors).some(
+ (k) => (errors as any)[k] !== undefined,
+ );
+
+ const submitForm = () => {
+ if (hasErrors) return Promise.reject();
+ return onUpdate(state as any);
+ };
+
+ return (
+ <div>
+ <section class="section">
+ <section class="hero is-hero-bar">
+ <div class="hero-body">
+ <div class="level">
+ <div class="level-left">
+ <div class="level-item">
+ <span class="is-size-4">
+ Device: <b>{device.id}</b>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ <hr />
+
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column is-four-fifths">
+ <FormProvider
+ object={state}
+ valueHandler={setState}
+ errors={errors}
+ >
+ <Input<Entity>
+ name="otp_description"
+ label={i18n.str`Description`}
+ tooltip={i18n.str`dddd`}
+ />
+ <InputSelector<Entity>
+ name="otp_algorithm"
+ label={i18n.str`Verification algorithm`}
+ tooltip={i18n.str`Algorithm to use to verify transaction in offline mode`}
+ values={algorithms}
+ toStr={(v) => algorithmsNames[v]}
+ fromStr={(v) => Number(v)}
+ />
+ {state.otp_algorithm && state.otp_algorithm > 0 ? (
+ <Fragment>
+ <InputWithAddon<Entity>
+ name="otp_key"
+ 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"}
+ tooltip={i18n.str`Your device need to have exactly the same value`}
+ fromStr={(v) => v.toUpperCase()}
+ addonAfter={
+ <span class="icon">
+ {showKey ? (
+ <i class="mdi mdi-eye" />
+ ) : (
+ <i class="mdi mdi-eye-off" />
+ )}
+ </span>
+ }
+ side={
+ state.otp_key === undefined ? <button
+
+ onClick={(e) => {
+ setState((s) => ({ ...s, otp_key: "" }));
+ }}
+ class="button">change key</button> :
+ <span style={{ display: "flex" }}>
+ <button
+ data-tooltip={i18n.str`generate random secret key`}
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setState((s) => ({ ...s, otp_key: randomBase32Key() }));
+ }}
+ >
+ <i18n.Translate>random</i18n.Translate>
+ </button>
+ <button
+ data-tooltip={
+ showKey
+ ? i18n.str`show secret key`
+ : i18n.str`hide secret key`
+ }
+ class="button is-info mr-3"
+ onClick={(e) => {
+ setShowKey(!showKey);
+ }}
+ >
+ {showKey ? (
+ <i18n.Translate>hide</i18n.Translate>
+ ) : (
+ <i18n.Translate>show</i18n.Translate>
+ )}
+ </button>
+ </span>
+ }
+ />
+ </Fragment>
+ ) : undefined} </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</i18n.Translate>
+ </AsyncButton>
+ </div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx
new file mode 100644
index 000000000..9a27ccfee
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx
@@ -0,0 +1,102 @@
+/*
+ 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 { Fragment, 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, WithId } from "../../../../declaration.js";
+import { Notification } from "../../../../utils/types.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;
+
+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 { 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);
+ }
+
+ return (
+ <Fragment>
+ <NotificationCard notification={notif} />
+ <UpdatePage
+ device={{
+ id: vid,
+ otp_algorithm: result.data.otp_algorithm,
+ otp_description: result.data.device_description,
+ otp_key: undefined,
+ otp_ctr: result.data.otp_ctr
+ }}
+ onBack={onBack}
+ onUpdate={(data) => {
+ return updateOtpDevice(vid, data)
+ .then(onConfirm)
+ .catch((error) => {
+ setNotif({
+ message: i18n.str`could not update template`,
+ type: "ERROR",
+ description: error.message,
+ });
+ });
+ }}
+ />
+ </Fragment>
+ );
+}
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 fd7b08875..124ced1f1 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
@@ -81,9 +81,6 @@ export function CardTable({
instances={webhooks}
onDelete={onDelete}
onSelect={onSelect}
- onNewOrder={(d) => {
- console.log("test", d);
- }}
rowSelection={rowSelection}
rowSelectionHandler={rowSelectionHandler}
onLoadMoreAfter={onLoadMoreAfter}
@@ -104,7 +101,6 @@ interface TableProps {
rowSelection: string[];
instances: Entity[];
onDelete: (e: Entity) => void;
- onNewOrder: (e: Entity) => void;
onSelect: (e: Entity) => void;
rowSelectionHandler: StateUpdater<string[]>;
onLoadMoreBefore?: () => void;
@@ -122,7 +118,6 @@ function Table({
instances,
onLoadMoreAfter,
onDelete,
- onNewOrder,
onSelect,
onLoadMoreBefore,
hasMoreAfter,
diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
index 128450553..0d514f2df 100644
--- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
@@ -1,10 +1,10 @@
-import { VNode, h } from "preact";
-import { LangSelector } from "../../components/menu/LangSelector.js";
import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
+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 { FormErrors, FormProvider } from "../../components/form/FormProvider.js";
-import { useState } from "preact/hooks";
function getBrowserLang(): string | undefined {
if (typeof window === "undefined") return undefined;
@@ -24,7 +24,11 @@ export function Settings(): VNode {
function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
const next = s(value)
- updateValue("advanceOrderMode", next.advanceOrderMode ?? false)
+ const v: Settings = {
+ advanceOrderMode: next.advanceOrderMode ?? false,
+ dateFormat: next.dateFormat ?? "ymd"
+ }
+ updateValue(v)
}
return <div>
@@ -32,41 +36,64 @@ export function Settings(): VNode {
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label" style={{ width: 200 }}>
- <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 has-addons">
- <LangSelector />
- &nbsp;
- {borwserLang !== undefined && <button
- data-tooltip={i18n.str`generate random secret key`}
- class="button is-info mr-3"
- onClick={(e) => {
- update(borwserLang.substring(0, 2))
+ <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 />
+ &nbsp;
+ {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>}
+ </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"
}}
- >
- <i18n.Translate>Set default</i18n.Translate>
- </button>}
- </div>
+ values={[
+ "ymd",
+ "mdy",
+ "dmy",
+ ]}
+ tooltip={i18n.str`how the date is going to be displayed`}
+ />
+ </FormProvider>
</div>
- <FormProvider<Settings>
- name="settings"
- errors={errors}
- object={value}
- valueHandler={valueHandler}
- >
- <InputToggle<Settings>
- label={i18n.str`Advance order creation`}
- tooltip={i18n.str`Shows more options in the order creation form`}
- name="advanceOrderMode"
- />
- </FormProvider>
</div>
diff --git a/packages/merchant-backoffice-ui/src/schemas/index.ts b/packages/merchant-backoffice-ui/src/schemas/index.ts
index 149761c55..4be77595b 100644
--- a/packages/merchant-backoffice-ui/src/schemas/index.ts
+++ b/packages/merchant-backoffice-ui/src/schemas/index.ts
@@ -123,7 +123,7 @@ export const InstanceSchema = yup.object().shape({
export const InstanceUpdateSchema = InstanceSchema.clone().omit(["id"]);
export const InstanceCreateSchema = InstanceSchema.clone();
-export const AuthorizeTipSchema = yup.object().shape({
+export const AuthorizeRewardSchema = yup.object().shape({
justification: yup.string().required(),
amount: yup
.string()
@@ -161,7 +161,7 @@ export const OrderCreateSchema = yup.object().shape({
currencyGreaterThan0,
),
}),
- extra: yup.string().test("extra", "is not a JSON format", stringIsValidJSON),
+ // extra: yup.object().test("extra", "is not a JSON format", stringIsValidJSON),
payments: yup
.object()
.required()
diff --git a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
index b32eb831a..6ade0718a 100644
--- a/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx
@@ -16,7 +16,6 @@
import {
Amounts,
- BackupBackupProviderTerms,
canonicalizeBaseUrl,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
@@ -41,6 +40,12 @@ interface Props {
onBack: () => Promise<void>;
}
+interface BackupBackupProviderTerms {
+ annual_fee: string;
+ storage_limit_in_megabytes: number;
+ supported_protocol_version: string;
+}
+
export function ProviderAddPage({ onBack }: Props): VNode {
const [verifying, setVerifying] = useState<
| { url: string; name: string; provider: BackupBackupProviderTerms }