aboutsummaryrefslogtreecommitdiff
path: root/packages/merchant-backoffice-ui/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/merchant-backoffice-ui/src')
-rw-r--r--packages/merchant-backoffice-ui/src/Application.tsx24
-rw-r--r--packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/InstanceRoutes.tsx7
-rw-r--r--packages/merchant-backoffice-ui/src/components/exception/login.tsx97
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx91
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx2
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx64
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/index.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/backend.ts8
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/index.ts27
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/useSettings.ts59
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/de.po15
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/es.po6
-rw-r--r--packages/merchant-backoffice-ui/src/i18n/it.po21
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx309
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx31
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx20
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/ListPage.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx38
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx40
-rw-r--r--packages/merchant-backoffice-ui/src/paths/notfound/index.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/settings/index.tsx77
-rw-r--r--packages/merchant-backoffice-ui/src/scss/main.scss14
-rw-r--r--packages/merchant-backoffice-ui/src/scss/toggle.scss51
24 files changed, 732 insertions, 302 deletions
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
index 23510c456..f6a81ff8d 100644
--- a/packages/merchant-backoffice-ui/src/Application.tsx
+++ b/packages/merchant-backoffice-ui/src/Application.tsx
@@ -26,7 +26,7 @@ import {
} from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact";
import { route } from "preact-router";
-import { useMemo } from "preact/hooks";
+import { useMemo, useState } from "preact/hooks";
import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
import { Loading } from "./components/exception/loading.js";
import {
@@ -42,6 +42,7 @@ 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 (
@@ -70,10 +71,19 @@ 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!" />
+ <NotYetReadyAppMenu title="Welcome!" onShowSettings={() => setShowSettings(true)} />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment>
);
@@ -87,7 +97,7 @@ function ApplicationStatusRoutes(): VNode {
) {
return (
<Fragment>
- <NotYetReadyAppMenu title="Login" />
+ <NotYetReadyAppMenu title="Login" onShowSettings={() => setShowSettings(true)} />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment>
);
@@ -98,7 +108,7 @@ function ApplicationStatusRoutes(): VNode {
) {
return (
<Fragment>
- <NotYetReadyAppMenu title="Error" />
+ <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard
notification={{
message: i18n.str`Server not found`,
@@ -112,7 +122,7 @@ function ApplicationStatusRoutes(): VNode {
}
if (result.type === ErrorType.SERVER) {
<Fragment>
- <NotYetReadyAppMenu title="Error" />
+ <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard
notification={{
message: i18n.str`Server response with an error code`,
@@ -125,7 +135,7 @@ function ApplicationStatusRoutes(): VNode {
}
if (result.type === ErrorType.UNREADABLE) {
<Fragment>
- <NotYetReadyAppMenu title="Error" />
+ <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard
notification={{
message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
@@ -138,7 +148,7 @@ function ApplicationStatusRoutes(): VNode {
}
return (
<Fragment>
- <NotYetReadyAppMenu title="Error" />
+ <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard
notification={{
message: i18n.str`Unexpected Error`,
diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
index 7731dac88..277c2b176 100644
--- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
@@ -33,6 +33,7 @@ import { InstanceRoutes } from "./InstanceRoutes.js";
import LoginPage from "./paths/login/index.js";
import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { Settings } from "./paths/settings/index.js";
export function ApplicationReadyRoutes(): VNode {
const { i18n } = useTranslationContext();
@@ -48,8 +49,15 @@ export function ApplicationReadyRoutes(): VNode {
clearAllTokens();
route("/");
};
+ const [showSettings, setShowSettings] = useState(false)
- if (result.loading) return <NotYetReadyAppMenu title="Loading..." />;
+ if (showSettings) {
+ return <Fragment>
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} />
+ <Settings/>
+ </Fragment>
+ }
+ if (result.loading) return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." />;
let admin = true;
let instanceNameByBackendURL;
@@ -61,7 +69,7 @@ export function ApplicationReadyRoutes(): VNode {
) {
return (
<Fragment>
- <NotYetReadyAppMenu title="Login" onLogout={clearTokenAndGoToRoot} />
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} />
<NotificationCard
notification={{
message: i18n.str`Access denied`,
@@ -81,7 +89,7 @@ export function ApplicationReadyRoutes(): VNode {
// does not match our pattern
return (
<Fragment>
- <NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} />
+ <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} />
<NotificationCard
notification={{
message: i18n.str`Couldn't access the server.`,
diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
index cb4abdd40..1547442ea 100644
--- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
@@ -68,6 +68,7 @@ import LoginPage from "./paths/login/index.js";
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";
export enum InstancePaths {
// details = '/',
@@ -100,6 +101,8 @@ export enum InstancePaths {
webhooks_list = "/webhooks",
webhooks_update = "/webhooks/:tid/update",
webhooks_new = "/webhooks/new",
+
+ settings = "/settings",
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
@@ -240,6 +243,9 @@ export function InstanceRoutes({
<Menu
instance={id}
admin={admin}
+ onShowSettings={() => {
+ route("/settings")
+ }}
path={path}
onLogout={clearTokenAndGoToRoot}
setInstanceName={setInstanceName}
@@ -558,6 +564,7 @@ export function InstanceRoutes({
}}
/>
<Route path={InstancePaths.kyc} component={ListKYCPage} />
+ <Route path={InstancePaths.settings} component={Settings} />
{/**
* Example pages
*/}
diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx
index 42c5e89d0..f2f94a7c5 100644
--- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx
+++ b/packages/merchant-backoffice-ui/src/components/exception/login.tsx
@@ -20,7 +20,7 @@
*/
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { ComponentChildren, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js";
import { useInstanceContext } from "../../context/instance.js";
@@ -40,7 +40,7 @@ function getTokenValuePart(t: string): string {
}
function normalizeToken(r: string): string {
- return `secret-token:${encodeURIComponent(r)}`;
+ return `secret-token:${r}`;
}
function cleanUp(s: string): string {
@@ -53,7 +53,7 @@ function cleanUp(s: string): string {
export function LoginModal({ onConfirm, withMessage }: Props): VNode {
const { url: backendUrl, token: baseToken } = useBackendContext();
- const { admin, token: instanceToken } = useInstanceContext();
+ const { admin, token: instanceToken, id } = useInstanceContext();
const testLogin = useCredentialsChecker();
const currentToken = getTokenValuePart(
(!admin ? baseToken : instanceToken) ?? "",
@@ -63,6 +63,78 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
const [url, setURL] = useState(cleanUp(backendUrl));
const { i18n } = useTranslationContext();
+ if (admin && id !== "default") {
+ //admin trying to access another instance
+ return (<div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Login required`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <p>
+ <i18n.Translate>Need the access token for the instance.</i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"set new access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? onConfirm(url, normalizeToken(token))
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "flex-end",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <AsyncButton
+ onClick={async () => {
+ const secretToken = normalizeToken(token);
+ const { valid, cause } = await testLogin(`${url}/instances/${id}`, secretToken);
+ if (valid) {
+ onConfirm(url, secretToken);
+ } else {
+ onConfirm(url);
+ }
+ }}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>)
+ }
+
return (
<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
@@ -137,8 +209,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
borderTop: 0,
}}
>
- <button
- class="button is-info"
+ <AsyncButton
onClick={async () => {
const secretToken = normalizeToken(token);
const { valid, cause } = await testLogin(url, secretToken);
@@ -150,10 +221,24 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
}}
>
<i18n.Translate>Confirm</i18n.Translate>
- </button>
+ </AsyncButton>
</footer>
</div>
</div>
</div>
);
}
+
+function AsyncButton({ onClick, children }: { onClick: () => Promise<void>, children: ComponentChildren }): VNode {
+ const [running, setRunning] = useState(false)
+ return <button class="button is-info" disabled={running} onClick={() => {
+ setRunning(true)
+ onClick().then(() => {
+ setRunning(false)
+ }).catch(() => {
+ setRunning(false)
+ })
+ }}>
+ {children}
+ </button>
+}
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
new file mode 100644
index 000000000..61ddf3c84
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx
@@ -0,0 +1,91 @@
+/*
+ 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 { InputProps, useField } from "./useField.js";
+
+interface Props<T> extends InputProps<T> {
+ name: T;
+ readonly?: boolean;
+ expand?: boolean;
+ threeState?: boolean;
+ toBoolean?: (v?: any) => boolean | undefined;
+ fromBoolean?: (s: boolean | undefined) => any;
+}
+
+const defaultToBoolean = (f?: any): boolean | undefined => f || "";
+const defaultFromBoolean = (v: boolean | undefined): any => v as any;
+
+export function InputToggle<T>({
+ name,
+ readonly,
+ placeholder,
+ tooltip,
+ label,
+ help,
+ threeState,
+ expand,
+ fromBoolean = defaultFromBoolean,
+ toBoolean = defaultToBoolean,
+}: Props<keyof T>): VNode {
+ const { error, value, onChange } = useField<T>(name);
+
+ const onCheckboxClick = (): void => {
+ const c = toBoolean(value);
+ if (c === false && threeState) return onChange(undefined as any);
+ return onChange(fromBoolean(!c));
+ };
+
+ return (
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label" style={{ width: 200 }}>
+ {label}
+ {tooltip && (
+ <span class="icon has-tooltip-right" data-tooltip={tooltip}>
+ <i class="mdi mdi-information" />
+ </span>
+ )}
+ </label>
+ </div>
+ <div class="field-body is-flex-grow-1">
+ <div class="field">
+ <p class={expand ? "control is-expanded" : "control"}>
+ <label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
+ <input
+ type="checkbox"
+ class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"}
+ checked={toBoolean(value)}
+ placeholder={placeholder}
+ readonly={readonly}
+ name={String(name)}
+ disabled={readonly}
+ onChange={onCheckboxClick}
+ />
+ <div class="toggle-switch"></div>
+ </label>
+ {help}
+ </p>
+ {error && <p class="help is-danger">{error}</p>}
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx
index 9624a2c38..9f1b33893 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/NavigationBar.tsx
@@ -20,7 +20,6 @@
*/
import { h, VNode } from "preact";
-import { LangSelector } from "./LangSelector.js";
import logo from "../../assets/logo-2021.svg";
interface Props {
@@ -65,7 +64,6 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
</a>
<div class="navbar-end">
<div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
- <LangSelector />
</div>
</div>
</div>
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index 6fee600eb..f3cf80b92 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -31,6 +31,7 @@ const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
interface Props {
onLogout: () => void;
+ onShowSettings: () => void;
mobile?: boolean;
instance: string;
admin?: boolean;
@@ -40,6 +41,7 @@ interface Props {
export function Sidebar({
mobile,
instance,
+ onShowSettings,
onLogout,
admin,
mimic,
@@ -78,21 +80,8 @@ export function Sidebar({
<div class="menu is-menu-main">
{instance ? (
<Fragment>
- <p class="menu-label">
- <i18n.Translate>Instance</i18n.Translate>
- </p>
<ul class="menu-list">
- <li>
- <a href={"/update"} class="has-icon">
- <span class="icon">
- <i class="mdi mdi-square-edit-outline" />
- </span>
- <span class="menu-item-label">
- <i18n.Translate>Settings</i18n.Translate>
- </span>
- </a>
- </li>
- <li>
+ <li>
<a href={"/orders"} class="has-icon">
<span class="icon">
<i class="mdi mdi-cash-register" />
@@ -132,6 +121,31 @@ export function Sidebar({
</span>
</a>
</li>
+ {needKYC && (
+ <li>
+ <a href={"/kyc"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-account-check" />
+ </span>
+ <span class="menu-item-label">KYC Status</span>
+ </a>
+ </li>
+ )}
+ </ul>
+ <p class="menu-label">
+ <i18n.Translate>Configuration</i18n.Translate>
+ </p>
+ <ul class="menu-list">
+ <li>
+ <a href={"/update"} class="has-icon">
+ <span class="icon">
+ <i class="mdi mdi-square-edit-outline" />
+ </span>
+ <span class="menu-item-label">
+ <i18n.Translate>Account</i18n.Translate>
+ </span>
+ </a>
+ </li>
<li>
<a href={"/reserves"} class="has-icon">
<span class="icon">
@@ -150,16 +164,6 @@ export function Sidebar({
</span>
</a>
</li>
- {needKYC && (
- <li>
- <a href={"/kyc"} class="has-icon">
- <span class="icon">
- <i class="mdi mdi-account-check" />
- </span>
- <span class="menu-item-label">KYC Status</span>
- </a>
- </li>
- )}
</ul>
</Fragment>
) : undefined}
@@ -168,6 +172,18 @@ export function Sidebar({
</p>
<ul class="menu-list">
<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>
+ </span>
+ </a>
+ </li>
+ <li>
<div>
<span style={{ width: "3rem" }} class="icon">
<i class="mdi mdi-currency-eur" />
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
index 56573b8ca..cdbae4ae0 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -75,6 +75,7 @@ interface MenuProps {
instance: string;
admin?: boolean;
onLogout?: () => void;
+ onShowSettings: () => void;
setInstanceName: (s: string) => void;
}
@@ -93,6 +94,7 @@ function WithTitle({
export function Menu({
onLogout,
+ onShowSettings,
title,
instance,
path,
@@ -121,6 +123,7 @@ export function Menu({
{onLogout && (
<Sidebar
+ onShowSettings={onShowSettings}
onLogout={onLogout}
admin={admin}
mimic={mimic}
@@ -130,7 +133,12 @@ export function Menu({
)}
{mimic && (
- <nav class="level">
+ <nav class="level" style={{
+ zIndex: 100,
+ position:"fixed",
+ width:"50%",
+ marginLeft: "20%"
+ }}>
<div class="level-item has-text-centered has-background-warning">
<p class="is-size-5">
You are viewing the instance <b>&quot;{instance}&quot;</b>.{" "}
@@ -154,6 +162,7 @@ export function Menu({
interface NotYetReadyAppMenuProps {
title: string;
onLogout?: () => void;
+ onShowSettings: () => void;
}
interface NotifProps {
@@ -194,6 +203,7 @@ export function NotificationCard({
export function NotYetReadyAppMenu({
onLogout,
+ onShowSettings,
title,
}: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
@@ -212,7 +222,7 @@ export function NotYetReadyAppMenu({
title={title}
/>
{onLogout && (
- <Sidebar onLogout={onLogout} instance="" mobile={mobileOpen} />
+ <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} />
)}
</div>
);
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts
index 90fd320a9..145a366f6 100644
--- a/packages/merchant-backoffice-ui/src/hooks/backend.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts
@@ -239,16 +239,16 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
searchDate?: Date,
delta?: number,
): Promise<HttpResponseOk<T>> {
- const date_ms =
+ const date_s =
delta && delta < 0 && searchDate
- ? searchDate.getTime() + 1
- : searchDate?.getTime();
+ ? (searchDate.getTime() / 1000) + 1
+ : searchDate !== undefined ? (searchDate.getTime() / 1000) : undefined;
const params: any = {};
if (paid !== undefined) params.paid = paid;
if (delta !== undefined) params.delta = delta;
if (refunded !== undefined) params.refunded = refunded;
if (wired !== undefined) params.wired = wired;
- if (date_ms !== undefined) params.date_ms = date_ms;
+ if (date_s !== undefined) params.date_s = date_s;
return requestHandler<T>(baseUrl, endpoint, { params, token });
},
[baseUrl, token],
diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts
index 316620cf7..b77b9dea8 100644
--- a/packages/merchant-backoffice-ui/src/hooks/index.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/index.ts
@@ -21,6 +21,7 @@
import { StateUpdater, useCallback, useState } from "preact/hooks";
import { ValueOrFunction } from "../utils/types.js";
+import { useMemoryStorage } from "@gnu-taler/web-util/browser";
const calculateRootPath = () => {
const rootPath =
@@ -52,14 +53,17 @@ export function useBackendURL(
export function useBackendDefaultToken(
initialValue?: string,
-): [string | undefined, StateUpdater<string | undefined>] {
- return useLocalStorage("backend-token", initialValue);
+): [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];
}
export function useBackendInstanceToken(
id: string,
-): [string | undefined, StateUpdater<string | undefined>] {
- const [token, setToken] = useLocalStorage(`backend-token-${id}`);
+): [string | undefined, ((d: string | undefined) => void)] {
+ const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`)
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token
@@ -67,15 +71,16 @@ export function useBackendInstanceToken(
return [defaultToken, defaultSetToken];
}
function updateToken(
- value:
- | (string | undefined)
- | ((s: string | undefined) => string | undefined),
+ value: (string | undefined)
): void {
- setToken((p) => {
- const toStore = value instanceof Function ? value(p) : value;
- return toStore;
- });
+ 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/useSettings.ts b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
new file mode 100644
index 000000000..5c0932f27
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForBoolean,
+} from "@gnu-taler/taler-util";
+
+function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
+ if (str === undefined) return undefined;
+ try {
+ return JSON.parse(str);
+ } catch {
+ return undefined;
+ }
+}
+
+export interface Settings {
+ advanceOrderMode: boolean
+}
+
+const defaultSettings: Settings = {
+ advanceOrderMode: false,
+}
+
+export const codecForSettings = (): Codec<Settings> =>
+ buildCodecForObject<Settings>()
+ .property("advanceOrderMode", codecForBoolean())
+ .build("Settings");
+
+const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings());
+
+export function useSettings(): [
+ Readonly<Settings>,
+ <T extends keyof Settings>(key: T, value: Settings[T]) => void,
+] {
+ const { value, update } = useLocalStorage(SETTINGS_KEY);
+
+ const parsed: Settings = value ?? defaultSettings;
+ function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
+ update({ ...parsed, [k]: v });
+ }
+ return [parsed, updateField];
+}
diff --git a/packages/merchant-backoffice-ui/src/i18n/de.po b/packages/merchant-backoffice-ui/src/i18n/de.po
index d8d0bae29..19f8f283b 100644
--- a/packages/merchant-backoffice-ui/src/i18n/de.po
+++ b/packages/merchant-backoffice-ui/src/i18n/de.po
@@ -12,20 +12,21 @@
# You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2023-08-15 07:28+0000\n"
+"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/de/>\n"
+"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
#: src/components/modal/index.tsx:71
#, c-format
@@ -1252,7 +1253,7 @@ msgstr ""
#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
msgid "Refunded"
-msgstr ""
+msgstr "Rückerstattet"
#: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format
diff --git a/packages/merchant-backoffice-ui/src/i18n/es.po b/packages/merchant-backoffice-ui/src/i18n/es.po
index 8571ef17b..10ec0cf3b 100644
--- a/packages/merchant-backoffice-ui/src/i18n/es.po
+++ b/packages/merchant-backoffice-ui/src/i18n/es.po
@@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: 2023-04-24 06:43+0000\n"
-"Last-Translator: Stefan Kügel <skuegel@web.de>\n"
+"PO-Revision-Date: 2023-08-13 10:14+0000\n"
+"Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"merchant-backoffice/es/>\n"
"Language: es\n"
@@ -1273,7 +1273,7 @@ msgstr "No se pudo create el reembolso"
#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
msgid "Refunded"
-msgstr "Reembolzado"
+msgstr "Reembolsado"
#: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format
diff --git a/packages/merchant-backoffice-ui/src/i18n/it.po b/packages/merchant-backoffice-ui/src/i18n/it.po
index d8d0bae29..05f1e7002 100644
--- a/packages/merchant-backoffice-ui/src/i18n/it.po
+++ b/packages/merchant-backoffice-ui/src/i18n/it.po
@@ -12,20 +12,21 @@
# You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
#
-#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"PO-Revision-Date: 2023-08-15 07:28+0000\n"
+"Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
+"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
+"merchant-backoffice/it/>\n"
+"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 4.13.1\n"
#: src/components/modal/index.tsx:71
#, c-format
@@ -439,7 +440,7 @@ msgstr ""
#: src/components/form/InputTaxes.tsx:119
#, c-format
msgid "Amount"
-msgstr ""
+msgstr "Importo"
#: src/components/form/InputTaxes.tsx:120
#, c-format
@@ -887,7 +888,7 @@ msgstr ""
#: src/paths/instance/orders/list/Table.tsx:154
#, c-format
msgid "Date"
-msgstr ""
+msgstr "Data"
#: src/paths/instance/orders/list/Table.tsx:200
#, c-format
@@ -1252,7 +1253,7 @@ msgstr ""
#: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format
msgid "Refunded"
-msgstr ""
+msgstr "Rimborsato"
#: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format
@@ -1633,7 +1634,7 @@ msgstr ""
#: src/paths/instance/reserves/details/DetailPage.tsx:119
#, c-format
msgid "Subject"
-msgstr ""
+msgstr "Soggetto"
#: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format
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 c8cc20ae0..fa9347c6e 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
@@ -43,6 +43,7 @@ import { Duration, MerchantBackend, WithId } from "../../../../declaration.js";
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";
interface Props {
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
@@ -62,8 +63,8 @@ function with_defaults(config: InstanceConfig): Partial<Entity> {
!config.default_pay_delay || config.default_pay_delay.d_us === "forever"
? undefined
: add(new Date(), {
- seconds: config.default_pay_delay.d_us / (1000 * 1000),
- });
+ seconds: config.default_pay_delay.d_us / (1000 * 1000),
+ });
return {
inventoryProducts: {},
@@ -138,7 +139,7 @@ export function CreatePage({
const [value, valueHandler] = useState(with_defaults(instanceConfig));
const config = useConfigContext();
const zero = Amounts.zeroOfCurrency(config.currency);
-
+ const [settings] = useSettings()
const inventoryList = Object.values(value.inventoryProducts || {});
const productList = Object.values(value.products || {});
@@ -154,10 +155,10 @@ export function CreatePage({
order_price: !value.pricing?.order_price
? i18n.str`required`
: !parsedPrice
- ? i18n.str`not valid`
- : Amounts.isZero(parsedPrice)
- ? i18n.str`must be greater than 0`
- : undefined,
+ ? i18n.str`not valid`
+ : Amounts.isZero(parsedPrice)
+ ? i18n.str`must be greater than 0`
+ : undefined,
}),
extra:
value.extra && !stringIsValidJSON(value.extra)
@@ -167,47 +168,47 @@ export function CreatePage({
refund_deadline: !value.payments?.refund_deadline
? undefined
: !isFuture(value.payments.refund_deadline)
- ? i18n.str`should be in the future`
- : value.payments.pay_deadline &&
- isBefore(value.payments.refund_deadline, value.payments.pay_deadline)
- ? i18n.str`refund deadline cannot be before pay deadline`
- : value.payments.wire_transfer_deadline &&
- isBefore(
- value.payments.wire_transfer_deadline,
- value.payments.refund_deadline,
- )
- ? i18n.str`wire transfer deadline cannot be before refund deadline`
- : undefined,
+ ? i18n.str`should be in the future`
+ : value.payments.pay_deadline &&
+ isBefore(value.payments.refund_deadline, value.payments.pay_deadline)
+ ? i18n.str`refund deadline cannot be before pay deadline`
+ : value.payments.wire_transfer_deadline &&
+ isBefore(
+ value.payments.wire_transfer_deadline,
+ value.payments.refund_deadline,
+ )
+ ? i18n.str`wire transfer deadline cannot be before refund deadline`
+ : undefined,
pay_deadline: !value.payments?.pay_deadline
? undefined
: !isFuture(value.payments.pay_deadline)
- ? i18n.str`should be in the future`
- : value.payments.wire_transfer_deadline &&
- isBefore(
- value.payments.wire_transfer_deadline,
- value.payments.pay_deadline,
- )
- ? i18n.str`wire transfer deadline cannot be before pay deadline`
- : undefined,
+ ? i18n.str`should be in the future`
+ : value.payments.wire_transfer_deadline &&
+ isBefore(
+ value.payments.wire_transfer_deadline,
+ value.payments.pay_deadline,
+ )
+ ? i18n.str`wire transfer deadline cannot be before pay deadline`
+ : undefined,
auto_refund_deadline: !value.payments?.auto_refund_deadline
? undefined
: !isFuture(value.payments.auto_refund_deadline)
- ? i18n.str`should be in the future`
- : !value.payments?.refund_deadline
- ? i18n.str`should have a refund deadline`
- : !isAfter(
- value.payments.refund_deadline,
- value.payments.auto_refund_deadline,
- )
- ? i18n.str`auto refund cannot be after refund deadline`
- : undefined,
+ ? i18n.str`should be in the future`
+ : !value.payments?.refund_deadline
+ ? i18n.str`should have a refund deadline`
+ : !isAfter(
+ value.payments.refund_deadline,
+ value.payments.auto_refund_deadline,
+ )
+ ? i18n.str`auto refund cannot be after refund deadline`
+ : undefined,
}),
shipping: undefinedIfEmpty({
delivery_date: !value.shipping?.delivery_date
? undefined
: !isFuture(value.shipping.delivery_date)
- ? i18n.str`should be in the future`
- : undefined,
+ ? i18n.str`should be in the future`
+ : undefined,
}),
};
const hasErrors = Object.keys(errors).some(
@@ -227,27 +228,27 @@ export function CreatePage({
extra: value.extra,
pay_deadline: value.payments.pay_deadline
? {
- t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000),
- }
+ t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000),
+ }
: undefined,
wire_transfer_deadline: value.payments.wire_transfer_deadline
? {
- t_s: Math.floor(
- value.payments.wire_transfer_deadline.getTime() / 1000,
- ),
- }
+ t_s: Math.floor(
+ value.payments.wire_transfer_deadline.getTime() / 1000,
+ ),
+ }
: undefined,
refund_deadline: value.payments.refund_deadline
? {
- t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000),
- }
+ t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000),
+ }
: undefined,
auto_refund: value.payments.auto_refund_deadline
? {
- d_us: Math.floor(
- value.payments.auto_refund_deadline.getTime() * 1000,
- ),
- }
+ d_us: Math.floor(
+ value.payments.auto_refund_deadline.getTime() * 1000,
+ ),
+ }
: undefined,
wire_fee_amortization: value.payments.wire_fee_amortization as number,
max_fee: value.payments.max_fee as string,
@@ -374,13 +375,15 @@ export function CreatePage({
inventory={instanceInventory}
/>
- <NonInventoryProductFrom
- productToEdit={editingProduct}
- onAddProduct={(p) => {
- setEditingProduct(undefined);
- return addNewProduct(p);
- }}
- />
+ {settings.advanceOrderMode &&
+ <NonInventoryProductFrom
+ productToEdit={editingProduct}
+ onAddProduct={(p) => {
+ setEditingProduct(undefined);
+ return addNewProduct(p);
+ }}
+ />
+ }
{allProducts.length > 0 && (
<ProductList
@@ -423,8 +426,8 @@ export function CreatePage({
discountOrRise > 0 &&
(discountOrRise < 1
? `discount of %${Math.round(
- (1 - discountOrRise) * 100,
- )}`
+ (1 - discountOrRise) * 100,
+ )}`
: `rise of %${Math.round((discountOrRise - 1) * 100)}`)
}
tooltip={i18n.str`Amount to be paid by the customer`}
@@ -445,102 +448,108 @@ export function CreatePage({
tooltip={i18n.str`Title of the order to be shown to the customer`}
/>
- <InputGroup
- name="shipping"
- label={i18n.str`Shipping and Fulfillment`}
- initialActive
- >
- <InputDate
- name="shipping.delivery_date"
- label={i18n.str`Delivery date`}
- tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`}
- />
- {value.shipping?.delivery_date && (
- <InputGroup
- name="shipping.delivery_location"
- label={i18n.str`Location`}
- tooltip={i18n.str`address where the products will be delivered`}
- >
- <InputLocation name="shipping.delivery_location" />
- </InputGroup>
- )}
- <Input
- name="shipping.fullfilment_url"
- label={i18n.str`Fulfillment URL`}
- tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
- />
- </InputGroup>
+ {settings.advanceOrderMode &&
+ <InputGroup
+ name="shipping"
+ label={i18n.str`Shipping and Fulfillment`}
+ initialActive
+ >
+ <InputDate
+ name="shipping.delivery_date"
+ label={i18n.str`Delivery date`}
+ tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`}
+ />
+ {value.shipping?.delivery_date && (
+ <InputGroup
+ name="shipping.delivery_location"
+ label={i18n.str`Location`}
+ tooltip={i18n.str`address where the products will be delivered`}
+ >
+ <InputLocation name="shipping.delivery_location" />
+ </InputGroup>
+ )}
+ <Input
+ name="shipping.fullfilment_url"
+ label={i18n.str`Fulfillment URL`}
+ tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
+ />
+ </InputGroup>
+ }
- <InputGroup
- name="payments"
- label={i18n.str`Taler payment options`}
- tooltip={i18n.str`Override default Taler payment settings for this order`}
- >
- <InputDate
- name="payments.pay_deadline"
- label={i18n.str`Payment deadline`}
- tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`}
- />
- <InputDate
- name="payments.refund_deadline"
- label={i18n.str`Refund deadline`}
- tooltip={i18n.str`Time until which the order can be refunded by the merchant.`}
- />
- <InputDate
- name="payments.wire_transfer_deadline"
- label={i18n.str`Wire transfer deadline`}
- tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`}
- />
- <InputDate
- name="payments.auto_refund_deadline"
- label={i18n.str`Auto-refund deadline`}
- tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
- />
+ {settings.advanceOrderMode &&
+ <InputGroup
+ name="payments"
+ label={i18n.str`Taler payment options`}
+ tooltip={i18n.str`Override default Taler payment settings for this order`}
+ >
+ <InputDate
+ name="payments.pay_deadline"
+ label={i18n.str`Payment deadline`}
+ tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`}
+ />
+ <InputDate
+ name="payments.refund_deadline"
+ label={i18n.str`Refund deadline`}
+ tooltip={i18n.str`Time until which the order can be refunded by the merchant.`}
+ />
+ <InputDate
+ name="payments.wire_transfer_deadline"
+ label={i18n.str`Wire transfer deadline`}
+ tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`}
+ />
+ <InputDate
+ name="payments.auto_refund_deadline"
+ label={i18n.str`Auto-refund deadline`}
+ tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
+ />
- <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.`}
- />
- <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
- 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.`}
- />
- <InputNumber
- name="payments.minimum_age"
- label={i18n.str`Minimum age required`}
- tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
- help={
- minAgeByProducts > 0
- ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
- : undefined
- }
- />
- </InputGroup>
+ <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.`}
+ />
+ <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
+ 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.`}
+ />
+ <InputNumber
+ name="payments.minimum_age"
+ label={i18n.str`Minimum age required`}
+ tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
+ help={
+ minAgeByProducts > 0
+ ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
+ : undefined
+ }
+ />
+ </InputGroup>
+ }
- <InputGroup
- name="extra"
- label={i18n.str`Additional information`}
- tooltip={i18n.str`Custom information to be included in the contract for this order.`}
- >
- <Input
+ {settings.advanceOrderMode &&
+ <InputGroup
name="extra"
- inputType="multiline"
- label={`Value`}
- tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
- />
- </InputGroup>
+ 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).`}
+ />
+ </InputGroup>
+ }
</FormProvider>
<div class="buttons is-right mt-5">
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 8dabfbe12..8965d41c9 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
@@ -21,7 +21,7 @@
import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { format } from "date-fns";
+import { format, formatDistance } from "date-fns";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { FormProvider } from "../../../../components/form/FormProvider.js";
@@ -223,6 +223,7 @@ function ClaimedPage({
</div>
</div>
</div>
+
<div class="level">
<div class="level-left">
<div class="level-item">
@@ -419,6 +420,11 @@ function PaidPage({
}
}
+ const now = new Date()
+ const nextEvent = events.find((e) => {
+ return e.when.getTime() > now.getTime()
+ })
+
const [value, valueHandler] = useState<Partial<Paid>>(order);
const { url } = useBackendContext();
const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part
@@ -504,22 +510,13 @@ function PaidPage({
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
- // maxWidth: '100%',
}}
>
<p>
- <a
- href={order.contract_terms.fulfillment_url}
- rel="nofollow"
- target="new"
- >
- {order.contract_terms.fulfillment_url}
- </a>
- </p>
- <p>
- {format(
- new Date(order.contract_terms.timestamp.t_s * 1000),
- "yyyy/MM/dd HH:mm:ss",
+ <i18n.Translate>Next event in </i18n.Translate> {formatDistance(
+ nextEvent!.when,
+ new Date(),
+ // "yyyy/MM/dd HH:mm:ss",
)}
</p>
</div>
@@ -668,9 +665,9 @@ function UnpaidPage({
{order.creation_time.t_s === "never"
? "never"
: format(
- new Date(order.creation_time.t_s * 1000),
- "yyyy-MM-dd HH:mm:ss",
- )}
+ new Date(order.creation_time.t_s * 1000),
+ "yyyy-MM-dd HH:mm:ss",
+ )}
</p>
</div>
</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 d73ba3acc..e68889a92 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
@@ -67,7 +67,7 @@ export function Timeline({ events: e }: Props) {
);
case "start":
return (
- <div class="timeline-marker is-icon is-success">
+ <div class="timeline-marker is-icon">
<i class="mdi mdi-flag " />
</div>
);
@@ -104,7 +104,7 @@ export function Timeline({ events: e }: Props) {
}
})()}
<div class="timeline-content">
- <p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p>
+ {e.description !== "now" && <p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p>}
<p>{e.description}</p>
</div>
</div>
@@ -117,12 +117,12 @@ export interface Event {
when: Date;
description: string;
type:
- | "start"
- | "refund"
- | "refund-taken"
- | "wired"
- | "wired-range"
- | "deadline"
- | "delivery"
- | "now";
+ | "start"
+ | "refund"
+ | "refund-taken"
+ | "wired"
+ | "wired-range"
+ | "deadline"
+ | "delivery"
+ | "now";
}
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 56d9dda74..37770d273 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
@@ -164,7 +164,7 @@ export function ListPage({
<div class="field has-addons">
{jumpToDate && (
<div class="control">
- <a class="button" onClick={() => onSelectDate(undefined)}>
+ <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}>
<span
class="icon"
data-tooltip={i18n.str`clear date filter`}
@@ -191,7 +191,7 @@ export function ListPage({
<div class="control">
<span class="has-tooltip-left" data-tooltip={dateTooltip}>
<a
- class="button"
+ class="button is-fullwidth"
onClick={() => {
setPickDate(true);
}}
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 4dde202c4..e20b9bc27 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
@@ -85,34 +85,34 @@ export function CreatePage({ onCreate, 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>),
+ ? 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`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(
@@ -139,7 +139,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
>
<InputWithAddon<Entity>
name="template_id"
- addonBefore={`${backend.url}/instances/templates/`}
+ help={`${backend.url}/instances/templates/${state.template_id ?? ""}`}
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`}
/>
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 90084f113..0f30efafd 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
@@ -34,6 +34,7 @@ import { useBackendContext } from "../../../../context/backend.js";
import { useConfigContext } from "../../../../context/config.js";
import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js";
+import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
type Entity = MerchantBackend.Template.UsingTemplateDetails;
@@ -64,46 +65,47 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
const fixedAmount = !!template.template_contract.amount;
const fixedSummary = !!template.template_contract.summary;
- const params = new URLSearchParams();
+ const templateParams: Record<string, string> = {}
if (!fixedAmount) {
if (state.amount) {
- params.append("amount", state.amount);
+ templateParams.amount = state.amount
} else {
- params.append("amount", config.currency);
+ templateParams.amount = config.currency
}
}
+
if (!fixedSummary) {
- params.append("summary", state.summary ?? "");
+ templateParams.summary = state.summary ?? ""
}
- const paramsStr = fixedAmount && fixedSummary ? "" : "?" + params.toString();
- const merchantURL = new URL(backendUrl);
-
- const talerProto =
- merchantURL.protocol === "http:" ? "taler+http:" : "taler:";
+ const merchantBaseUrl = new URL(backendUrl).href;
- const payTemplateUri = `${talerProto}//pay-template/${merchantURL.hostname}/${templateId}${paramsStr}`;
+ const payTemplateUri = stringifyPayTemplateUri({
+ merchantBaseUrl,
+ templateId,
+ templateParams
+ })
const issuer = encodeURIComponent(
- `${new URL(backendUrl).hostname}/${instanceId}`,
+ `${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;
+ ? `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;
+ ? `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 && (
diff --git a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
index b58948dbd..061a67025 100644
--- a/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/notfound/index.tsx
@@ -25,7 +25,6 @@ import { Link } from "preact-router";
export default function NotFoundPage(): VNode {
return (
<div>
- <h1>Error 404</h1>
<p>That page doesn&apos;t exist.</p>
<Link href="/">
<h4>Back to Home</h4>
diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
new file mode 100644
index 000000000..128450553
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
@@ -0,0 +1,77 @@
+import { VNode, h } from "preact";
+import { LangSelector } from "../../components/menu/LangSelector.js";
+import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { InputToggle } from "../../components/form/InputToggle.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;
+ if (window.navigator.languages) return window.navigator.languages[0];
+ if (window.navigator.language) return window.navigator.language;
+ return undefined;
+}
+
+export function Settings(): VNode {
+ const { i18n } = useTranslationContext()
+ const borwserLang = getBrowserLang()
+ const { update } = useLang()
+
+ const [value, updateValue] = useSettings()
+ const errors: FormErrors<Settings> = {
+ }
+
+ function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
+ const next = s(value)
+ updateValue("advanceOrderMode", next.advanceOrderMode ?? false)
+ }
+
+ return <div>
+ <section class="section is-main-section">
+ <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))
+ }}
+ >
+ <i18n.Translate>Set default</i18n.Translate>
+ </button>}
+ </div>
+ </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>
+ <div class="column" />
+ </div>
+ </section>
+ </div>
+} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/scss/main.scss b/packages/merchant-backoffice-ui/src/scss/main.scss
index ad698eb26..c4be8aa73 100644
--- a/packages/merchant-backoffice-ui/src/scss/main.scss
+++ b/packages/merchant-backoffice-ui/src/scss/main.scss
@@ -52,6 +52,8 @@ $tooltip-color: red;
@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
+@import "toggle";
+
.notification {
background-color: transparent;
}
@@ -82,7 +84,7 @@ $tooltip-color: red;
pointer-events: none;
}
-.toast > .message {
+.toast>.message {
white-space: pre-wrap;
opacity: 80%;
}
@@ -92,6 +94,7 @@ div {
position: relative;
pointer-events: none;
opacity: 0.5;
+
&:after {
// @include loader;
position: absolute;
@@ -104,7 +107,7 @@ div {
}
}
-input[type="checkbox"]:indeterminate + .check {
+input[type="checkbox"]:indeterminate+.check {
background: red !important;
}
@@ -125,6 +128,7 @@ input[type="checkbox"]:indeterminate + .check {
tr:hover .right-sticky {
background-color: hsl(0, 0%, 80%);
}
+
.table.is-striped tbody tr:nth-child(even):hover .right-sticky {
background-color: hsl(0, 0%, 95%);
}
@@ -181,11 +185,11 @@ div[data-tooltip]::before {
position: absolute;
}
-.modal-card-body > p {
+.modal-card-body>p {
padding: 1em;
}
-.modal-card-body > p.warning {
+.modal-card-body>p.warning {
background-color: #fffbdd;
border: solid 1px #f2e9bf;
-}
+} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/scss/toggle.scss b/packages/merchant-backoffice-ui/src/scss/toggle.scss
new file mode 100644
index 000000000..24636da2f
--- /dev/null
+++ b/packages/merchant-backoffice-ui/src/scss/toggle.scss
@@ -0,0 +1,51 @@
+$green: #56c080;
+
+.toggle {
+ cursor: pointer;
+ display: inline-block;
+}
+.toggle-switch {
+ display: inline-block;
+ background: #ccc;
+ border-radius: 16px;
+ width: 58px;
+ height: 32px;
+ position: relative;
+ vertical-align: middle;
+ transition: background 0.25s;
+ &:before,
+ &:after {
+ content: "";
+ }
+ &:before {
+ display: block;
+ background: linear-gradient(to bottom, #fff 0%, #eee 100%);
+ border-radius: 50%;
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
+ width: 24px;
+ height: 24px;
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ transition: left 0.25s;
+ }
+ .toggle:hover &:before {
+ background: linear-gradient(to bottom, #fff 0%, #fff 100%);
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
+ }
+ .toggle-checkbox:checked + & {
+ background: $green;
+ &:before {
+ left: 30px;
+ }
+ }
+}
+.toggle-checkbox {
+ position: absolute;
+ visibility: hidden;
+}
+.toggle-label {
+ margin-left: 5px;
+ position: relative;
+ top: 2px;
+}