summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2024-03-22 13:56:16 -0300
committerSebastian <sebasjm@gmail.com>2024-03-26 16:57:58 -0300
commite2bfbced7ab027c901913e83ff7dd82240661990 (patch)
tree33752605ccaf19498f8c2a64e0117db16f22ce26 /packages
parent0c265558c4b7b78a13272abf1c4c84e3cf93c987 (diff)
downloadwallet-core-e2bfbced7ab027c901913e83ff7dd82240661990.tar.gz
wallet-core-e2bfbced7ab027c901913e83ff7dd82240661990.tar.bz2
wallet-core-e2bfbced7ab027c901913e83ff7dd82240661990.zip
work in progress, new api being used. merchant now should move into using the full API
Diffstat (limited to 'packages')
-rw-r--r--packages/merchant-backoffice-ui/src/Routing.tsx16
-rw-r--r--packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx52
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/index.tsx21
-rw-r--r--packages/merchant-backoffice-ui/src/components/modal/index.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx25
-rw-r--r--packages/merchant-backoffice-ui/src/context/backend.test.ts163
-rw-r--r--packages/merchant-backoffice-ui/src/context/backend.ts69
-rw-r--r--packages/merchant-backoffice-ui/src/context/config.ts29
-rw-r--r--packages/merchant-backoffice-ui/src/context/session.ts (renamed from packages/merchant-backoffice-ui/src/hooks/session.ts)151
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts3
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/backend.ts245
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.test.ts3
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.ts87
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/preference.ts3
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/testing.tsx24
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx31
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx48
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx33
-rw-r--r--packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx33
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx525
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx13
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx35
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx3
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx17
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx218
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx34
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx14
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx49
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx12
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx8
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx1
-rw-r--r--packages/merchant-backoffice-ui/src/paths/login/index.tsx82
-rw-r--r--packages/merchant-backoffice-ui/src/paths/settings/index.tsx206
-rw-r--r--packages/taler-util/src/http-client/authentication.ts30
-rw-r--r--packages/taler-util/src/http-client/merchant.ts14
-rw-r--r--packages/taler-util/src/http-client/types.ts17
43 files changed, 1264 insertions, 1106 deletions
diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx
index 50a91c060..4ed5850e7 100644
--- a/packages/merchant-backoffice-ui/src/Routing.tsx
+++ b/packages/merchant-backoffice-ui/src/Routing.tsx
@@ -39,7 +39,10 @@ import { MerchantBackend } from "./declaration.js";
import { useInstanceBankAccounts } from "./hooks/bank.js";
import { useInstanceKYCDetails } from "./hooks/instance.js";
import { usePreference } from "./hooks/preference.js";
-import { DEFAULT_ADMIN_USERNAME, useSessionState } from "./hooks/session.js";
+import {
+ DEFAULT_ADMIN_USERNAME,
+ useSessionContext,
+} from "./context/session.js";
import InstanceCreatePage from "./paths/admin/create/index.js";
import InstanceListPage from "./paths/admin/list/index.js";
import BankAccountCreatePage from "./paths/instance/accounts/create/index.js";
@@ -74,6 +77,7 @@ import { LoginPage } from "./paths/login/index.js";
import NotFoundPage from "./paths/notfound/index.js";
import { Settings } from "./paths/settings/index.js";
import { Notification } from "./utils/types.js";
+import { createHashHistory } from "history";
export enum InstancePaths {
error = "/error",
@@ -138,9 +142,10 @@ export const publicPages = {
go: urlPattern(/\/home/, () => "#/home"),
};
+const history = createHashHistory();
export function Routing(_p: Props): VNode {
const { i18n } = useTranslationContext();
- const { state } = useSessionState();
+ const { state } = useSessionContext();
type GlobalNotifState =
| (Notification & { to: string | undefined })
@@ -152,8 +157,10 @@ export function Routing(_p: Props): VNode {
const instance = useInstanceBankAccounts();
const accounts = !instance.ok ? undefined : instance.data.accounts;
- const shouldWarnAboutMissingBankAccounts = !state.isAdmin && accounts !== undefined && accounts.length < 1
- const shouldLogin = state.status === "loggedOut" || state.status === "expired";
+ const shouldWarnAboutMissingBankAccounts =
+ !state.isAdmin && accounts !== undefined && accounts.length < 1;
+ const shouldLogin =
+ state.status === "loggedOut" || state.status === "expired";
function ServerErrorRedirectTo(to: InstancePaths | AdminPaths) {
return function ServerErrorRedirectToImpl(
@@ -275,6 +282,7 @@ export function Routing(_p: Props): VNode {
)}
<Router
+ history={history}
onChange={(e) => {
const movingOutFromNotification =
globalNotification && e.url !== globalNotification.to;
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
index c1359e641..f60508504 100644
--- a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
+++ b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
@@ -18,8 +18,8 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { useMerchantApiContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, h, VNode } from "preact";
-import { useConfigContext } from "../../context/config.js";
import { Amount } from "../../declaration.js";
import { InputWithAddon } from "./InputWithAddon.js";
import { InputProps } from "./useField.js";
@@ -43,7 +43,7 @@ export function InputCurrency<T>({
children,
side,
}: Props<keyof T>): VNode {
- const config = useConfigContext();
+ const { config } = useMerchantApiContext();
return (
<InputWithAddon<T>
name={name}
diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index e36549e76..cb4442897 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -19,9 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useBackendContext } from "../../context/backend.js";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useSessionContext } from "../../context/session.js";
import { Entity } from "../../paths/admin/create/CreatePage.js";
import { Input } from "../form/Input.js";
import { InputDuration } from "../form/InputDuration.js";
@@ -40,13 +42,15 @@ export function DefaultInstanceFormFields({
showId: boolean;
}): VNode {
const { i18n } = useTranslationContext();
- const { url: backendURL } = useBackendContext()
+ const {
+ state: { backendUrl },
+ } = useSessionContext();
return (
<Fragment>
{showId && (
<InputWithAddon<Entity>
name="id"
- addonBefore={`${backendURL}/instances/`}
+ addonBefore={new URL("instances/", backendUrl).href}
readonly={readonlyId}
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the instance in URLs. The 'default' instance is special in that it is used to administer other instances.`}
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
index a9b9618bb..adc47b216 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -19,12 +19,14 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { useMerchantApiContext, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
-import { useConfigContext } from "../../context/config.js";
+import {
+ useMerchantApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
+import { useSessionContext } from "../../context/session.js";
import { useInstanceKYCDetails } from "../../hooks/instance.js";
import { LangSelector } from "./LangSelector.js";
-import { useSessionState } from "../../hooks/session.js";
// const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -33,20 +35,21 @@ interface Props {
mobile?: boolean;
}
-export function Sidebar({
- mobile,
-}: Props): VNode {
- const config = useConfigContext();
- // const { url: backendURL } = useBackendContext()
+export function Sidebar({ mobile }: Props): VNode {
const { i18n } = useTranslationContext();
const kycStatus = useInstanceKYCDetails();
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
- const { state, logOut } = useSessionState();
- const { url } = useMerchantApiContext();
- const isLoggedIn = state.status === "loggedIn" || state.status === "impersonate"
- const hasToken = isLoggedIn && state.token !== undefined
+ const { state, logOut } = useSessionContext();
+ const isLoggedIn = state.status === "loggedIn";
+ const hasToken = isLoggedIn && state.token !== undefined;
+ const backendURL = state.backendUrl;
+ const { config } = useMerchantApiContext();
+
return (
- <aside class="aside is-placed-left is-expanded" style={{ overflowY: "scroll" }}>
+ <aside
+ class="aside is-placed-left is-expanded"
+ style={{ overflowY: "scroll" }}
+ >
{mobile && (
<div
class="footer"
@@ -187,9 +190,10 @@ export function Sidebar({
</p>
<ul class="menu-list">
<li>
- <a class="has-icon is-state-info is-hoverable"
+ <a
+ class="has-icon is-state-info is-hoverable"
onClick={(e): void => {
- e.preventDefault()
+ e.preventDefault();
}}
>
<span class="icon">
@@ -206,7 +210,7 @@ export function Sidebar({
<i class="mdi mdi-web" />
</span>
<span class="menu-item-label">
- {url.hostname}
+ {new URL(backendURL).hostname}
</span>
</div>
</li>
@@ -215,12 +219,10 @@ export function Sidebar({
<span style={{ width: "3rem" }} class="icon">
ID
</span>
- <span class="menu-item-label">
- {state.instance}
- </span>
+ <span class="menu-item-label">{state.instance}</span>
</div>
</li>
- {state.isAdmin && state.status !== "impersonate" && (
+ {state.isAdmin && (
<Fragment>
<p class="menu-label">
<i18n.Translate>Instances</i18n.Translate>
@@ -247,12 +249,12 @@ export function Sidebar({
</li>
</Fragment>
)}
- {hasToken ?
+ {hasToken ? (
<li>
<a
class="has-icon is-state-info is-hoverable"
onClick={(e): void => {
- logOut()
+ logOut();
e.preventDefault();
}}
>
@@ -263,8 +265,8 @@ export function Sidebar({
<i18n.Translate>Log out</i18n.Translate>
</span>
</a>
- </li> : undefined
- }
+ </li>
+ ) : undefined}
</ul>
</div>
</aside>
diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
index fa2de563e..aa955db4e 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx
@@ -21,7 +21,7 @@ import { InstancePaths } from "../../Routing.js";
import { Notification } from "../../utils/types.js";
import { NavigationBar } from "./NavigationBar.js";
import { Sidebar } from "./SideBar.js";
-import { useSessionState } from "../../hooks/session.js";
+import { useSessionContext } from "../../context/session.js";
import { useNavigationContext } from "@gnu-taler/web-util/browser";
function getInstanceTitle(path: string, id: string): string {
@@ -97,15 +97,14 @@ function WithTitle({
export function Menu(_p: MenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
- const { state, logIn } = useSessionState();
+ const { state, deImpersonate } = useSessionContext();
const { path } = useNavigationContext();
const titleWithSubtitle = !state.isAdmin
? getInstanceTitle(path, state.instance)
: getAdminTitle(path, state.instance);
- const isLoggedIn =
- state.status === "loggedIn" || state.status === "impersonate";
+ const isLoggedIn =state.status === "loggedIn";
return (
<WithTitle title={titleWithSubtitle}>
@@ -119,10 +118,10 @@ export function Menu(_p: MenuProps): VNode {
/>
{isLoggedIn && (
- <Sidebar mobile={mobileOpen} mimic={state.status === "impersonate"} />
+ <Sidebar mobile={mobileOpen} />
)}
- {state.status === "impersonate" && (
+ {state.status !== "loggedOut" && state.impersonate !== undefined && (
<nav
class="level"
style={{
@@ -139,10 +138,7 @@ export function Menu(_p: MenuProps): VNode {
<a
href="#/instances"
onClick={(e) => {
- logIn({
- instance: state.originalInstance,
- token: state.originalToken,
- });
+ deImpersonate();
e.preventDefault();
}}
>
@@ -227,14 +223,13 @@ export function NotConnectedAppMenu({
export function NotYetReadyAppMenu({ title }: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false);
- const { state } = useSessionState();
+ const { state } = useSessionContext();
useEffect(() => {
document.title = `Taler Backoffice: ${title}`;
}, [title]);
- const isLoggedIn =
- state.status === "loggedIn" || state.status === "impersonate";
+ const isLoggedIn = state.status === "loggedIn";
return (
<div
diff --git a/packages/merchant-backoffice-ui/src/components/modal/index.tsx b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
index c684ba7a3..1335d0f77 100644
--- a/packages/merchant-backoffice-ui/src/components/modal/index.tsx
+++ b/packages/merchant-backoffice-ui/src/components/modal/index.tsx
@@ -22,11 +22,11 @@
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
-import { useInstanceContext } from "../../context/instance.js";
import { DEFAULT_REQUEST_TIMEOUT } from "../../utils/constants.js";
import { Spinner } from "../exception/loading.js";
import { FormProvider } from "../form/FormProvider.js";
import { Input } from "../form/Input.js";
+import { useSessionContext } from "../../context/session.js";
interface Props {
active?: boolean;
@@ -298,8 +298,8 @@ export function UpdateTokenModal({
new_token: !form.new_token
? i18n.str`cannot be empty`
: form.new_token === form.old_token
- ? i18n.str`cannot be the same as the old token`
- : undefined,
+ ? i18n.str`cannot be the same as the old token`
+ : undefined,
repeat_token:
form.new_token !== form.repeat_token
? i18n.str`is not the same`
@@ -310,9 +310,9 @@ export function UpdateTokenModal({
(k) => (errors as any)[k] !== undefined,
);
- const instance = useInstanceContext();
+ const { state } = useSessionContext();
- const text = i18n.str`You are updating the access token from instance with id ${instance.id}`;
+ const text = i18n.str`You are updating the access token from instance with id ${state.instance}`;
return (
<ClearConfirmModal
@@ -374,8 +374,8 @@ export function SetTokenNewInstanceModal({
new_token: !form.new_token
? i18n.str`cannot be empty`
: form.new_token === form.old_token
- ? i18n.str`cannot be the same as the old access token`
- : undefined,
+ ? i18n.str`cannot be the same as the old access token`
+ : undefined,
repeat_token:
form.new_token !== form.repeat_token
? i18n.str`is not the same`
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index 47e3431e2..11344cde3 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -19,11 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ useMerchantApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { h } from "preact";
import { useCallback, useEffect, useState } from "preact/hooks";
import * as yup from "yup";
-import { useBackendContext } from "../../context/backend.js";
import { MerchantBackend } from "../../declaration.js";
import {
ProductCreateSchema as createSchema,
@@ -37,6 +39,7 @@ import { InputNumber } from "../form/InputNumber.js";
import { InputStock, Stock } from "../form/InputStock.js";
import { InputTaxes } from "../form/InputTaxes.js";
import { InputWithAddon } from "../form/InputWithAddon.js";
+import { useSessionContext } from "../../context/session.js";
type Entity = MerchantBackend.Products.ProductDetail & { product_id: string };
@@ -58,12 +61,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> = {};
@@ -114,7 +117,9 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
onSubscribe(hasErrors ? undefined : submit);
}, [submit, hasErrors]);
- const { url: backendURL } = useBackendContext()
+ const {
+ state: { backendUrl },
+ } = useSessionContext();
const { i18n } = useTranslationContext();
return (
@@ -128,7 +133,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
{alreadyExist ? undefined : (
<InputWithAddon<Entity>
name="product_id"
- addonBefore={`${backendURL}/product/`}
+ addonBefore={new URL("product/", backendUrl).href}
label={i18n.str`ID`}
tooltip={i18n.str`product identification to use in URLs (for internal use only)`}
/>
diff --git a/packages/merchant-backoffice-ui/src/context/backend.test.ts b/packages/merchant-backoffice-ui/src/context/backend.test.ts
deleted file mode 100644
index 74530e750..000000000
--- a/packages/merchant-backoffice-ui/src/context/backend.test.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import * as tests from "@gnu-taler/web-util/testing";
-import { ComponentChildren, h, VNode } from "preact";
-import { AccessToken, MerchantBackend } from "../declaration.js";
-import {
- useAdminAPI,
- useInstanceAPI,
- useManagementAPI,
-} from "../hooks/instance.js";
-import { expect } from "chai";
-import { ApiMockEnvironment } from "../hooks/testing.js";
-import {
- API_CREATE_INSTANCE,
- API_NEW_LOGIN,
- API_UPDATE_CURRENT_INSTANCE_AUTH,
- API_UPDATE_INSTANCE_AUTH_BY_ID,
-} from "../hooks/urls.js";
-
-interface TestingContextProps {
- children?: ComponentChildren;
-}
-
-describe("backend context api ", () => {
- it("should use new token after updating the instance token in the settings as user", async () => {
- const env = new ApiMockEnvironment();
-
- const hookBehavior = await tests.hookBehaveLikeThis(
- () => {
- const instance = useInstanceAPI();
- const management = useManagementAPI("default");
- const admin = useAdminAPI();
-
- return { instance, management, admin };
- },
- {},
- [
- ({ instance, management, admin }) => {
- env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), {
- request: {
- method: "token",
- token: "another_token",
- },
- response: {
- name: "instance_name",
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
- env.addRequestExpectation(API_NEW_LOGIN, {
- auth: "another_token",
- request: {
- scope: "write",
- duration: {
- "d_us": "forever",
- },
- refreshable: true,
- },
-
- });
-
- management.setNewAccessToken(undefined,"another_token" as AccessToken);
- },
- ({ instance, management, admin }) => {
- expect(env.assertJustExpectedRequestWereMade()).deep.eq({
- result: "ok",
- });
-
- env.addRequestExpectation(API_CREATE_INSTANCE, {
- // auth: "another_token",
- request: {
- id: "new_instance_id",
- } as MerchantBackend.Instances.InstanceConfigurationMessage,
- });
-
- admin.createInstance({
- id: "new_instance_id",
- } as MerchantBackend.Instances.InstanceConfigurationMessage);
- },
- ],
- env.buildTestingContext(),
- );
-
- expect(hookBehavior).deep.eq({ result: "ok" });
- expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
- });
-
- it("should use new token after updating the instance token in the settings as admin", async () => {
- const env = new ApiMockEnvironment();
-
- const hookBehavior = await tests.hookBehaveLikeThis(
- () => {
- const instance = useInstanceAPI();
- const management = useManagementAPI("default");
- const admin = useAdminAPI();
-
- return { instance, management, admin };
- },
- {},
- [
- ({ instance, management, admin }) => {
- env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
- request: {
- method: "token",
- token: "another_token",
- },
- response: {
- name: "instance_name",
- } as MerchantBackend.Instances.QueryInstancesResponse,
- });
- env.addRequestExpectation(API_NEW_LOGIN, {
- auth: "another_token",
- request: {
- scope: "write",
- duration: {
- "d_us": "forever",
- },
- refreshable: true,
- },
- });
- instance.setNewAccessToken(undefined, "another_token" as AccessToken);
- },
- ({ instance, management, admin }) => {
- expect(env.assertJustExpectedRequestWereMade()).deep.eq({
- result: "ok",
- });
-
- env.addRequestExpectation(API_CREATE_INSTANCE, {
- // auth: "another_token",
- request: {
- id: "new_instance_id",
- } as MerchantBackend.Instances.InstanceConfigurationMessage,
- });
-
- admin.createInstance({
- id: "new_instance_id",
- } as MerchantBackend.Instances.InstanceConfigurationMessage);
- },
- ],
- env.buildTestingContext(),
- );
-
- expect(hookBehavior).deep.eq({ result: "ok" });
- expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
- });
-});
diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts
deleted file mode 100644
index f78236216..000000000
--- a/packages/merchant-backoffice-ui/src/context/backend.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { useMemoryStorage } from "@gnu-taler/web-util/browser";
-import { createContext, h, VNode } from "preact";
-import { useContext } from "preact/hooks";
-import { LoginToken } from "../declaration.js";
-import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
-
-interface BackendContextType {
- url: string,
- alreadyTriedLogin: boolean;
- token?: LoginToken;
- updateToken: (token: LoginToken | undefined) => void;
-}
-
-const BackendContext = createContext<BackendContextType>({
- url: "",
- alreadyTriedLogin: false,
- token: undefined,
- updateToken: () => null,
-});
-
-function useBackendContextState(
- defaultUrl?: string,
-): BackendContextType {
- const [url] = useBackendURL(defaultUrl);
- const [token, updateToken] = useBackendDefaultToken();
-
- return {
- url,
- token,
- alreadyTriedLogin: token !== undefined,
- updateToken,
- };
-}
-
-const BackendContextProvider = ({
- children,
- defaultUrl,
-}: {
- children: any;
- defaultUrl?: string;
-}): VNode => {
- const value = useBackendContextState(defaultUrl);
-
- return h(BackendContext.Provider, { value, children });
-};
-
-const useBackendContext = (): BackendContextType =>
- useContext(BackendContext);
diff --git a/packages/merchant-backoffice-ui/src/context/config.ts b/packages/merchant-backoffice-ui/src/context/config.ts
deleted file mode 100644
index 8c562b3c1..000000000
--- a/packages/merchant-backoffice-ui/src/context/config.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { createContext } from "preact";
-import { useContext } from "preact/hooks";
-import { MerchantBackend } from "../declaration.js";
-
-const Context = createContext<MerchantBackend.VersionResponse>(null!);
-
-export const ConfigContextProvider = Context.Provider;
-export const useConfigContext = (): MerchantBackend.VersionResponse => useContext(Context);
diff --git a/packages/merchant-backoffice-ui/src/hooks/session.ts b/packages/merchant-backoffice-ui/src/context/session.ts
index 8bf075e94..83f3f113a 100644
--- a/packages/merchant-backoffice-ui/src/hooks/session.ts
+++ b/packages/merchant-backoffice-ui/src/context/session.ts
@@ -21,40 +21,45 @@ import {
buildCodecForUnion,
codecForBoolean,
codecForConstString,
- codecForConstTrue,
codecForString,
codecOptional,
} from "@gnu-taler/taler-util";
-import { buildStorageKey, useLocalStorage, useMerchantApiContext } from "@gnu-taler/web-util/browser";
+import {
+ buildStorageKey,
+ useLocalStorage,
+ useMerchantApiContext,
+} from "@gnu-taler/web-util/browser";
import { mutate } from "swr";
/**
* Has the information to reach and
* authenticate at the bank's backend.
*/
-export type SessionState = LoggedIn | LoggedOut | Expired | Impersonate;
+export type SessionState = LoggedIn | LoggedOut | Expired;
interface LoggedIn {
status: "loggedIn";
- instance: string;
+ backendUrl: string;
isAdmin: boolean;
+ instance: string;
token: AccessToken | undefined;
+ impersonate: Impersonate | undefined;
+}
+interface Impersonate {
+ originalInstance: string;
+ originalToken: AccessToken | undefined;
+ originalBackendUrl: string;
}
interface Expired {
status: "expired";
- instance: string;
+ backendUrl: string;
isAdmin: boolean;
-}
-interface Impersonate {
- status: "impersonate";
instance: string;
- isAdmin: true;
- token: AccessToken | undefined;
- originalInstance: string;
- originalToken: AccessToken | undefined;
+ impersonate: Impersonate | undefined;
}
interface LoggedOut {
status: "loggedOut";
+ backendUrl: string;
instance: string;
isAdmin: boolean;
}
@@ -62,7 +67,9 @@ interface LoggedOut {
export const codecForSessionStateLoggedIn = (): Codec<LoggedIn> =>
buildCodecForObject<LoggedIn>()
.property("status", codecForConstString("loggedIn"))
+ .property("backendUrl", codecForString())
.property("instance", codecForString())
+ .property("impersonate", codecOptional(codecForImpresonate()))
.property("token", codecOptional(codecForString() as Codec<AccessToken>))
.property("isAdmin", codecForBoolean())
.build("SessionState.LoggedIn");
@@ -70,54 +77,87 @@ export const codecForSessionStateLoggedIn = (): Codec<LoggedIn> =>
export const codecForSessionStateExpired = (): Codec<Expired> =>
buildCodecForObject<Expired>()
.property("status", codecForConstString("expired"))
+ .property("backendUrl", codecForString())
.property("instance", codecForString())
+ .property("impersonate", codecOptional(codecForImpresonate()))
.property("isAdmin", codecForBoolean())
.build("SessionState.Expired");
export const codecForSessionStateLoggedOut = (): Codec<LoggedOut> =>
buildCodecForObject<LoggedOut>()
.property("status", codecForConstString("loggedOut"))
+ .property("backendUrl", codecForString())
.property("instance", codecForString())
.property("isAdmin", codecForBoolean())
.build("SessionState.LoggedOut");
-export const codecForSessionStateImpresonate = (): Codec<Impersonate> =>
+export const codecForImpresonate = (): Codec<Impersonate> =>
buildCodecForObject<Impersonate>()
- .property("status", codecForConstString("impersonate"))
- .property("instance", codecForString())
- .property("isAdmin", codecForConstTrue())
- .property("token", codecOptional(codecForString() as Codec<AccessToken>))
.property("originalInstance", codecForString())
- .property("originalToken", codecOptional(codecForString() as Codec<AccessToken>))
+ .property(
+ "originalToken",
+ codecOptional(codecForString() as Codec<AccessToken>),
+ )
+ .property("originalBackendUrl", codecForString())
.build("SessionState.Impersonate");
export const codecForSessionState = (): Codec<SessionState> =>
buildCodecForUnion<SessionState>()
.discriminateOn("status")
.alternative("loggedIn", codecForSessionStateLoggedIn())
- .alternative("impersonate", codecForSessionStateImpresonate())
.alternative("loggedOut", codecForSessionStateLoggedOut())
.alternative("expired", codecForSessionStateExpired())
.build("SessionState");
-export const defaultState = (instance: string): SessionState => ({
- status: "loggedIn",
- instance,
- isAdmin: instance === DEFAULT_ADMIN_USERNAME,
- token: undefined,
-});
+function inferInstanceName(url: URL) {
+ const match = INSTANCE_ID_LOOKUP.exec(url.href);
+ return !match || !match[1] ? DEFAULT_ADMIN_USERNAME : match[1];
+}
+
+export const defaultState = (url: URL): SessionState => {
+ const instance = inferInstanceName(url);
+ return {
+ status: "loggedIn",
+ instance,
+ backendUrl: url.href,
+ isAdmin: instance === DEFAULT_ADMIN_USERNAME,
+ token: undefined,
+ impersonate: undefined,
+ };
+};
export interface SessionStateHandler {
state: SessionState;
+ /**
+ * from every state to logout state
+ */
logOut(): void;
+ /**
+ * from impersonate to loggedIn
+ */
+ deImpersonate(): void;
+ /**
+ * from non-loggedOut state to expired
+ */
expired(): void;
- logIn(info: { instance: string; token?: AccessToken }): void;
+ /**
+ * from any to loggedIn
+ * @param info
+ */
+ logIn(info: { token?: AccessToken }): void;
+ /**
+ * from loggedIn to impersonate
+ * @param info
+ */
impersonate(info: { instance: string; token?: AccessToken }): void;
}
-const SESSION_STATE_KEY = buildStorageKey("merchant-session", codecForSessionState());
+const SESSION_STATE_KEY = buildStorageKey(
+ "merchant-session",
+ codecForSessionState(),
+);
-export const DEFAULT_ADMIN_USERNAME = "default"
+export const DEFAULT_ADMIN_USERNAME = "default";
export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/;
@@ -126,21 +166,43 @@ export const INSTANCE_ID_LOOKUP = /\/instances\/([^/]*)\/?$/;
* login credentials and backend's
* base URL.
*/
-export function useSessionState(): SessionStateHandler {
+export function useSessionContext(): SessionStateHandler {
const { url } = useMerchantApiContext();
- const match = INSTANCE_ID_LOOKUP.exec(url.href);
- const instanceName = !match || !match[1] ? DEFAULT_ADMIN_USERNAME : match[1];
-
const { value: state, update } = useLocalStorage(
SESSION_STATE_KEY,
- defaultState(instanceName),
+ defaultState(url),
);
return {
state,
logOut() {
- update(defaultState(instanceName));
+ const instance = inferInstanceName(url);
+ const nextState: SessionState = {
+ status: "loggedOut",
+ backendUrl: url.href,
+ instance,
+ isAdmin: instance === DEFAULT_ADMIN_USERNAME,
+ };
+ update(nextState);
+ },
+ deImpersonate() {
+ if (state.status === "loggedOut" || state.status === "expired") {
+ // can't impersonate if not loggedin
+ return;
+ }
+ if (state.impersonate === undefined) {
+ return;
+ }
+ const nextState: SessionState = {
+ status: "loggedIn",
+ backendUrl: state.impersonate.originalBackendUrl,
+ isAdmin: state.impersonate.originalInstance === DEFAULT_ADMIN_USERNAME,
+ instance: state.impersonate.originalInstance,
+ token: state.impersonate.originalToken,
+ impersonate: undefined,
+ };
+ update(nextState);
},
impersonate(info) {
if (state.status === "loggedOut" || state.status === "expired") {
@@ -148,31 +210,36 @@ export function useSessionState(): SessionStateHandler {
return;
}
const nextState: SessionState = {
- status: "impersonate",
- originalToken: state.token,
- originalInstance: state.instance,
- isAdmin: true,
+ status: "loggedIn",
+ backendUrl: new URL(`instances/${info.instance}`, state.backendUrl)
+ .href,
+ isAdmin: info.instance === DEFAULT_ADMIN_USERNAME,
instance: info.instance,
token: info.token,
+ impersonate: {
+ originalBackendUrl: state.backendUrl,
+ originalToken: state.token,
+ originalInstance: state.instance,
+ },
};
update(nextState);
},
expired() {
if (state.status === "loggedOut") return;
+
const nextState: SessionState = {
+ ...state,
status: "expired",
- instance: state.instance,
- isAdmin: state.instance === DEFAULT_ADMIN_USERNAME,
};
update(nextState);
},
logIn(info) {
// admin is defined by the username
const nextState: SessionState = {
+ impersonate: undefined,
+ ...state,
status: "loggedIn",
- instance: info.instance,
token: info.token,
- isAdmin: state.instance === DEFAULT_ADMIN_USERNAME,
};
update(nextState);
cleanAllCache();
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index ff526282a..e39257a79 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -117,9 +117,6 @@ interface LoginToken {
// token used to get loginToken
// must forget after used
declare const __ac_token: unique symbol;
-type AccessToken = string & {
- [__ac_token]: true;
-};
export namespace ExchangeBackend {
interface WireResponse {
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts
index 4305a9309..37dfd8fd6 100644
--- a/packages/merchant-backoffice-ui/src/hooks/backend.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts
@@ -19,8 +19,13 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
import {
+ AbsoluteTime,
+ AccessToken,
+ HttpStatusCode,
+} from "@gnu-taler/taler-util";
+import {
+ EmptyObject,
ErrorType,
HttpError,
HttpResponse,
@@ -31,10 +36,8 @@ import {
} from "@gnu-taler/web-util/browser";
import { useCallback, useEffect, useState } from "preact/hooks";
import { useSWRConfig } from "swr";
-import { useBackendContext } from "../context/backend.js";
-import { useInstanceContext } from "../context/instance.js";
-import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js";
-
+import { useSessionContext } from "../context/session.js";
+import { LoginToken, MerchantBackend, Timestamp } from "../declaration.js";
export function useMatchMutate(): (
re?: RegExp,
@@ -49,18 +52,22 @@ export function useMatchMutate(): (
}
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 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,
+ },
+ );
};
}
@@ -97,30 +104,36 @@ export function useBackendConfig(): HttpResponse<
const { request } = useBackendBaseRequest();
type Type = MerchantBackend.VersionResponse;
- type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number }
- const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 });
+ type State = {
+ data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>;
+ timer: number;
+ };
+ const [result, setResult] = useState<State>({
+ data: { loading: true },
+ timer: 0,
+ });
useEffect(() => {
if (result.timer) {
- clearTimeout(result.timer)
+ clearTimeout(result.timer);
}
function tryConfig(): void {
request<Type>(`/config`)
.then((data) => {
const timer: any = setTimeout(() => {
- tryConfig()
- }, CHECK_CONFIG_INTERVAL_OK)
- setResult({ data, timer })
+ tryConfig();
+ }, CHECK_CONFIG_INTERVAL_OK);
+ setResult({ data, timer });
})
.catch((error) => {
const timer: any = setTimeout(() => {
- tryConfig()
- }, CHECK_CONFIG_INTERVAL_FAIL)
- const data = error.cause
- setResult({ data, timer })
+ tryConfig();
+ }, CHECK_CONFIG_INTERVAL_FAIL);
+ const data = error.cause;
+ setResult({ data, timer });
});
}
- tryConfig()
+ tryConfig();
}, [request]);
return result.data;
@@ -134,29 +147,29 @@ interface useBackendInstanceRequestType {
fetcher: <T>(endpoint: string) => Promise<HttpResponseOk<T>>;
multiFetcher: <T>(params: [url: string[]]) => Promise<HttpResponseOk<T>[]>;
orderFetcher: <T>(
- params: [endpoint: string,
+ params: [
+ endpoint: string,
paid?: YesOrNo,
refunded?: YesOrNo,
wired?: YesOrNo,
searchDate?: Date,
- delta?: number,]
+ delta?: number,
+ ],
) => Promise<HttpResponseOk<T>>;
transferFetcher: <T>(
- params: [endpoint: string,
+ params: [
+ endpoint: string,
payto_uri?: string,
verified?: string,
position?: string,
- delta?: number,]
+ delta?: number,
+ ],
) => Promise<HttpResponseOk<T>>;
templateFetcher: <T>(
- params: [endpoint: string,
- position?: string,
- delta?: number]
+ params: [endpoint: string, position?: string, delta?: number],
) => Promise<HttpResponseOk<T>>;
webhookFetcher: <T>(
- params: [endpoint: string,
- position?: string,
- delta?: number]
+ params: [endpoint: string, position?: string, delta?: number],
) => Promise<HttpResponseOk<T>>;
}
interface useBackendBaseRequestType {
@@ -167,14 +180,16 @@ interface useBackendBaseRequestType {
}
type YesOrNo = "yes" | "no";
-type LoginResult = {
- valid: true;
- token: string;
- expiration: Timestamp;
-} | {
- valid: false;
- cause: HttpError<{}>;
-}
+type LoginResult =
+ | {
+ valid: true;
+ token: string;
+ expiration: Timestamp;
+ }
+ | {
+ valid: false;
+ cause: HttpError<EmptyObject>;
+ };
export function useCredentialsChecker() {
const { request } = useApiContext();
@@ -187,24 +202,34 @@ export function useCredentialsChecker() {
const data: MerchantBackend.Instances.LoginTokenRequest = {
scope: "write",
duration: {
- d_us: "forever"
+ d_us: "forever",
},
refreshable: true,
- }
+ };
try {
- const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, {
- method: "POST",
- token,
- data
- });
- return { valid: true, token: response.data.token, expiration: response.data.expiration };
+ const response =
+ await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(
+ baseUrl,
+ `/private/token`,
+ {
+ method: "POST",
+ token,
+ data,
+ },
+ );
+ return {
+ valid: true,
+ token: response.data.token,
+ expiration: response.data.expiration,
+ };
} catch (error) {
if (error instanceof RequestError) {
return { valid: false, cause: error.cause };
}
return {
- valid: false, cause: {
+ valid: false,
+ cause: {
type: ErrorType.UNEXPECTED,
loading: false,
info: {
@@ -212,23 +237,28 @@ export function useCredentialsChecker() {
status: 0,
options: {},
url: `/private/token`,
- payload: {}
+ payload: {},
},
exception: error,
- message: (error instanceof Error ? error.message : "unpexepected error")
- }
+ message:
+ error instanceof Error ? error.message : "unpexepected error",
+ },
};
}
- };
+ }
async function refreshLoginToken(
baseUrl: string,
- token: LoginToken
+ token: LoginToken,
): Promise<LoginResult> {
-
- if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) {
+ if (
+ AbsoluteTime.isExpired(
+ AbsoluteTime.fromProtocolTimestamp(token.expiration),
+ )
+ ) {
return {
- valid: false, cause: {
+ valid: false,
+ cause: {
type: ErrorType.CLIENT,
status: HttpStatusCode.Unauthorized,
message: "login token expired, login again.",
@@ -237,16 +267,16 @@ export function useCredentialsChecker() {
status: 401,
options: {},
url: `/private/token`,
- payload: {}
+ payload: {},
},
- payload: {}
+ payload: {},
},
- }
+ };
}
- return requestNewLoginToken(baseUrl, token.token as AccessToken)
+ return requestNewLoginToken(baseUrl, token.token as AccessToken);
}
- return { requestNewLoginToken, refreshLoginToken }
+ return { requestNewLoginToken, refreshLoginToken };
}
/**
@@ -255,37 +285,36 @@ export function useCredentialsChecker() {
* @returns request handler to
*/
export function useBackendBaseRequest(): useBackendBaseRequestType {
- const { url: backend, token: loginToken } = useBackendContext();
const { request: requestHandler } = useApiContext();
- const token = loginToken?.token;
+ const { state } = useSessionContext();
+ const token = state.status === "loggedIn" ? state.token : undefined;
+ const baseUrl = state.backendUrl;
const request = useCallback(
function requestImpl<T>(
endpoint: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(backend, endpoint, { ...options, token }).then(res => {
- return res
- }).catch(err => {
- throw err
- });
+ return requestHandler<T>(baseUrl, endpoint, { ...options, token })
+ .then((res) => {
+ return res;
+ })
+ .catch((err) => {
+ throw err;
+ });
},
- [backend, token],
+ [baseUrl, token],
);
return { request };
}
export function useBackendInstanceRequest(): useBackendInstanceRequestType {
- const { url: rootBackendUrl, token: rootToken } = useBackendContext();
- const { token: instanceToken, admin } = useInstanceContext();
const { request: requestHandler } = useApiContext();
- const { baseUrl, token: loginToken } = !admin
- ? { baseUrl: rootBackendUrl, token: rootToken }
- : { baseUrl: rootBackendUrl, token: instanceToken };
-
- const token = loginToken?.token;
+ const { state } = useSessionContext();
+ const token = state.status === "loggedIn" ? state.token : undefined;
+ const baseUrl = state.backendUrl;
const request = useCallback(
function requestImpl<T>(
@@ -301,7 +330,7 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
function multiFetcherImpl<T>(
args: [endpoints: string[]],
): Promise<HttpResponseOk<T>[]> {
- const [endpoints] = args
+ const [endpoints] = args;
return Promise.all(
endpoints.map((endpoint) =>
requestHandler<T>(baseUrl, endpoint, { token }),
@@ -320,18 +349,22 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const orderFetcher = useCallback(
function orderFetcherImpl<T>(
- args: [endpoint: string,
+ args: [
+ endpoint: string,
paid?: YesOrNo,
refunded?: YesOrNo,
wired?: YesOrNo,
searchDate?: Date,
- delta?: number,]
+ delta?: number,
+ ],
): Promise<HttpResponseOk<T>> {
- const [endpoint, paid, refunded, wired, searchDate, delta] = args
+ const [endpoint, paid, refunded, wired, searchDate, delta] = args;
const date_s =
delta && delta < 0 && searchDate
? Math.floor(searchDate.getTime() / 1000) + 1
- : searchDate !== undefined ? Math.floor(searchDate.getTime() / 1000) : undefined;
+ : searchDate !== undefined
+ ? Math.floor(searchDate.getTime() / 1000)
+ : undefined;
const params: any = {};
if (paid !== undefined) params.paid = paid;
if (delta !== undefined) params.delta = delta;
@@ -339,12 +372,12 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
if (wired !== undefined) params.wired = wired;
if (date_s !== undefined) params.date_s = date_s;
if (delta === 0) {
- //in this case we can already assume the response
+ //in this case we can already assume the response
//and avoid network
return Promise.resolve({
ok: true,
data: { orders: [] } as T,
- })
+ });
}
return requestHandler<T>(baseUrl, endpoint, { params, token });
},
@@ -353,23 +386,25 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const transferFetcher = useCallback(
function transferFetcherImpl<T>(
- args: [endpoint: string,
+ args: [
+ endpoint: string,
payto_uri?: string,
verified?: string,
position?: string,
- delta?: number,]
+ delta?: number,
+ ],
): Promise<HttpResponseOk<T>> {
- const [endpoint, payto_uri, verified, position, delta] = args
+ const [endpoint, payto_uri, verified, position, delta] = args;
const params: any = {};
if (payto_uri !== undefined) params.payto_uri = payto_uri;
if (verified !== undefined) params.verified = verified;
if (delta === 0) {
- //in this case we can already assume the response
+ //in this case we can already assume the response
//and avoid network
return Promise.resolve({
ok: true,
data: { transfers: [] } as T,
- })
+ });
}
if (delta !== undefined) {
params.limit = delta;
@@ -383,19 +418,17 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const templateFetcher = useCallback(
function templateFetcherImpl<T>(
- args: [endpoint: string,
- position?: string,
- delta?: number,]
+ args: [endpoint: string, position?: string, delta?: number],
): Promise<HttpResponseOk<T>> {
- const [endpoint, position, delta] = args
+ const [endpoint, position, delta] = args;
const params: any = {};
if (delta === 0) {
- //in this case we can already assume the response
+ //in this case we can already assume the response
//and avoid network
return Promise.resolve({
ok: true,
data: { templates: [] } as T,
- })
+ });
}
if (delta !== undefined) {
params.limit = delta;
@@ -409,19 +442,17 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const webhookFetcher = useCallback(
function webhookFetcherImpl<T>(
- args: [endpoint: string,
- position?: string,
- delta?: number,]
+ args: [endpoint: string, position?: string, delta?: number],
): Promise<HttpResponseOk<T>> {
- const [endpoint, position, delta] = args
+ const [endpoint, position, delta] = args;
const params: any = {};
if (delta === 0) {
- //in this case we can already assume the response
+ //in this case we can already assume the response
//and avoid network
return Promise.resolve({
ok: true,
data: { webhooks: [] } as T,
- })
+ });
}
if (delta !== undefined) {
params.limit = delta;
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
index 4f6cabc9e..a1bb3d5d4 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
@@ -21,7 +21,7 @@
import * as tests from "@gnu-taler/web-util/testing";
import { expect } from "chai";
-import { AccessToken, MerchantBackend } from "../declaration.js";
+import { MerchantBackend } from "../declaration.js";
import {
useAdminAPI,
useBackendInstances,
@@ -40,6 +40,7 @@ import {
API_UPDATE_CURRENT_INSTANCE_AUTH,
API_UPDATE_INSTANCE_BY_ID,
} from "./urls.js";
+import { AccessToken } from "@gnu-taler/taler-util";
describe("instance api interaction with details", () => {
it("should evict cache when updating an instance", async () => {
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
index 352f54982..dfe97fd61 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -17,9 +17,9 @@ import {
HttpResponse,
HttpResponseOk,
RequestError,
+ useMerchantApiContext,
} from "@gnu-taler/web-util/browser";
-import { useBackendContext } from "../context/backend.js";
-import { AccessToken, MerchantBackend } from "../declaration.js";
+import { MerchantBackend } from "../declaration.js";
import {
useBackendBaseRequest,
useBackendInstanceRequest,
@@ -29,6 +29,8 @@ import {
// FIX default import https://github.com/microsoft/TypeScript/issues/49189
import _useSWR, { SWRHook, useSWRConfig } from "swr";
+import { useSessionContext } from "../context/session.js";
+import { AccessToken } from "@gnu-taler/taler-util";
const useSWR = _useSWR as unknown as SWRHook;
interface InstanceAPI {
@@ -37,7 +39,10 @@ interface InstanceAPI {
) => Promise<void>;
deleteInstance: () => Promise<void>;
clearAccessToken: (currentToken: AccessToken | undefined) => Promise<void>;
- setNewAccessToken: (currentToken: AccessToken | undefined, token: AccessToken) => Promise<void>;
+ setNewAccessToken: (
+ currentToken: AccessToken | undefined,
+ token: AccessToken,
+ ) => Promise<void>;
}
export function useAdminAPI(): AdminAPI {
@@ -87,10 +92,13 @@ export interface AdminAPI {
export function useManagementAPI(instanceId: string): InstanceAPI {
const mutateAll = useMatchMutate();
- const { url: backendURL } = useBackendContext()
- const { updateToken } = useBackendContext();
+ const {
+ state: { backendUrl },
+ logIn,
+ logOut,
+ } = useSessionContext();
const { request } = useBackendBaseRequest();
- const { requestNewLoginToken } = useCredentialsChecker()
+ const { requestNewLoginToken } = useCredentialsChecker();
const updateInstance = async (
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
@@ -111,7 +119,9 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
mutateAll(/\/management\/instances/);
};
- const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => {
+ const clearAccessToken = async (
+ currentToken: AccessToken | undefined,
+ ): Promise<void> => {
await request(`/management/instances/${instanceId}/auth`, {
method: "POST",
token: currentToken,
@@ -121,36 +131,46 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
mutateAll(/\/management\/instances/);
};
- const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => {
+ const setNewAccessToken = async (
+ currentToken: AccessToken | undefined,
+ newToken: AccessToken,
+ ): Promise<void> => {
await request(`/management/instances/${instanceId}/auth`, {
method: "POST",
token: currentToken,
data: { method: "token", token: newToken },
});
- const resp = await requestNewLoginToken(backendURL, newToken)
+ const resp = await requestNewLoginToken(backendUrl, newToken);
if (resp.valid) {
- const { token, expiration } = resp
- updateToken({ token, expiration });
+ logIn({ token: resp.token as AccessToken });
} else {
- updateToken(undefined)
+ logOut();
}
mutateAll(/\/management\/instances/);
};
- return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken };
+ return {
+ updateInstance,
+ deleteInstance,
+ setNewAccessToken,
+ clearAccessToken,
+ };
}
export function useInstanceAPI(): InstanceAPI {
const { mutate } = useSWRConfig();
- const { url: backendURL, updateToken } = useBackendContext()
-
const {
- token: adminToken,
- } = useBackendContext();
+ state: { backendUrl },
+ } = useSessionContext();
+
const { request } = useBackendInstanceRequest();
- const { requestNewLoginToken } = useCredentialsChecker()
+ const { requestNewLoginToken } = useCredentialsChecker();
+ const { state, logIn, logOut } = useSessionContext();
+
+ const adminToken =
+ state.status === "loggedIn" && state.isAdmin ? state.token : undefined;
const updateInstance = async (
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
@@ -160,7 +180,9 @@ export function useInstanceAPI(): InstanceAPI {
data: instance,
});
- if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
+ if (adminToken) {
+ mutate(["/private/instances", adminToken, backendUrl], null);
+ }
mutate([`/private/`], null);
};
@@ -170,11 +192,15 @@ export function useInstanceAPI(): InstanceAPI {
// token: adminToken,
});
- if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
+ if (adminToken) {
+ mutate(["/private/instances", adminToken, backendUrl], null);
+ }
mutate([`/private/`], null);
};
- const clearAccessToken = async (currentToken: AccessToken | undefined): Promise<void> => {
+ const clearAccessToken = async (
+ currentToken: AccessToken | undefined,
+ ): Promise<void> => {
await request(`/private/auth`, {
method: "POST",
token: currentToken,
@@ -184,25 +210,32 @@ export function useInstanceAPI(): InstanceAPI {
mutate([`/private/`], null);
};
- const setNewAccessToken = async (currentToken: AccessToken | undefined, newToken: AccessToken): Promise<void> => {
+ const setNewAccessToken = async (
+ currentToken: AccessToken | undefined,
+ newToken: AccessToken,
+ ): Promise<void> => {
await request(`/private/auth`, {
method: "POST",
token: currentToken,
data: { method: "token", token: newToken },
});
- const resp = await requestNewLoginToken(backendURL, newToken)
+ const resp = await requestNewLoginToken(backendUrl, newToken);
if (resp.valid) {
- const { token, expiration } = resp
- updateToken({ token, expiration });
+ logIn({ token: resp.token as AccessToken });
} else {
- updateToken(undefined)
+ logOut();
}
mutate([`/private/`], null);
};
- return { updateInstance, deleteInstance, setNewAccessToken, clearAccessToken };
+ return {
+ updateInstance,
+ deleteInstance,
+ setNewAccessToken,
+ clearAccessToken,
+ };
}
export function useInstanceDetails(): HttpResponse<
diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts
index 4570ff679..5a50eb378 100644
--- a/packages/merchant-backoffice-ui/src/hooks/preference.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/preference.ts
@@ -59,6 +59,7 @@ const PREFERENCES_KEY = buildStorageKey(
export function usePreference(): [
Readonly<Preferences>,
<T extends keyof Preferences>(key: T, value: Preferences[T]) => void,
+ (s: Preferences) => void,
] {
const { value, update } = useLocalStorage(PREFERENCES_KEY, defaultSettings);
function updateField<T extends keyof Preferences>(k: T, v: Preferences[T]) {
@@ -66,7 +67,7 @@ export function usePreference(): [
update(newValue);
}
- return [value, updateField];
+ return [value, updateField, update];
}
export function dateFormatForSettings(s: Preferences): string {
diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
index d9a70e794..bebf7716b 100644
--- a/packages/merchant-backoffice-ui/src/hooks/testing.tsx
+++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
@@ -24,8 +24,6 @@ import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
import { HttpRequestLibrary, HttpRequestOptions, HttpResponse } from "@gnu-taler/taler-util/http";
import { SWRConfig } from "swr";
import { ApiContextProvider } from "@gnu-taler/web-util/browser";
-import { BackendContextProvider } from "../context/backend.js";
-import { InstanceContextProvider } from "../context/instance.js";
import { HttpResponseOk, RequestOptions } from "@gnu-taler/web-util/browser";
import { TalerBankIntegrationHttpClient, TalerCoreBankHttpClient, TalerRevenueHttpClient, TalerWireGatewayHttpClient } from "@gnu-taler/taler-util";
@@ -149,15 +147,15 @@ export class ApiMockEnvironment extends MockEnvironment {
const bankWire = new TalerWireGatewayHttpClient(bankCore.getWireGatewayAPI("b").href, "b", mockHttpClient)
return (
- <BackendContextProvider defaultUrl="http://backend">
- <InstanceContextProvider
- value={{
- token: undefined,
- id: "default",
- admin: true,
- changeToken: () => null,
- }}
- >
+ // <BackendContextProvider defaultUrl="http://backend">
+ // <InstanceContextProvider
+ // value={{
+ // token: undefined,
+ // id: "default",
+ // admin: true,
+ // changeToken: () => null,
+ // }}
+ // >
<ApiContextProvider value={{ request, bankCore, bankIntegration, bankRevenue, bankWire }}>
<SC
value={{
@@ -172,8 +170,8 @@ export class ApiMockEnvironment extends MockEnvironment {
{children}
</SC>
</ApiContextProvider>
- </InstanceContextProvider>
- </BackendContextProvider>
+ // </InstanceContextProvider>
+ // </BackendContextProvider>
);
};
}
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx
index ec54dc150..39fdb6bdc 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/Create.stories.tsx
@@ -20,8 +20,8 @@
*/
import { h, VNode, FunctionalComponent } from "preact";
-import { ConfigContextProvider } from "../../../context/config.js";
import { CreatePage as TestedComponent } from "./CreatePage.js";
+import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser";
export default {
title: "Pages/Instance/Create",
@@ -37,19 +37,32 @@ function createExample<Props>(
props: Partial<Props>,
) {
const r = (args: any) => (
- <ConfigContextProvider
+ <MerchantApiProviderTesting
value={{
- currency: "ARS",
- version: "1",
- currencies: {
- currency: "TESTKUDOS"
+ cancelRequest: () => {},
+ config: {
+ currency: "ARS",
+ version: "1",
+ currencies: {
+ "ASD": {
+ name: "testkudos",
+ alt_unit_names: {},
+ num_fractional_input_digits: 1,
+ num_fractional_normal_digits: 1,
+ num_fractional_trailing_zero_digits: 1,
+ }
+ },
+ exchanges: [],
+ name: "taler-merchant"
},
- exchanges: [],
- name: "taler-merchant"
+ hints: [],
+ lib: {} as any,
+ onActivity: (() => {}) as any,
+ url: new URL("asdasd"),
}}
>
<Component {...args} />
- </ConfigContextProvider>
+ </MerchantApiProviderTesting>
);
r.args = props;
return r;
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
index cbda65bfe..440cd2b07 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/index.tsx
@@ -17,16 +17,18 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import {
+ useMerchantApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../../components/menu/index.js";
-import { AccessToken, MerchantBackend } from "../../../declaration.js";
-import { useAdminAPI, useInstanceAPI } from "../../../hooks/instance.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { useAdminAPI } from "../../../hooks/instance.js";
+import { useSessionContext } from "../../../context/session.js";
import { Notification } from "../../../utils/types.js";
import { CreatePage } from "./CreatePage.js";
-import { useCredentialsChecker } from "../../../hooks/backend.js";
-import { useBackendContext } from "../../../context/backend.js";
interface Props {
onBack?: () => void;
@@ -39,8 +41,8 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
const { createInstance } = useAdminAPI();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
- const { requestNewLoginToken } = useCredentialsChecker()
- const { url: backendURL, updateToken } = useBackendContext()
+ const { lib } = useMerchantApiContext();
+ const { state, logIn } = useSessionContext();
return (
<Fragment>
@@ -53,15 +55,29 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
d: MerchantBackend.Instances.InstanceConfigurationMessage,
) => {
try {
- await createInstance(d)
+ await createInstance(d);
if (d.auth.token) {
- const resp = await requestNewLoginToken(backendURL, d.auth.token as AccessToken)
- if (resp.valid) {
- const { token, expiration } = resp
- updateToken({ token, expiration });
- } else {
- updateToken(undefined)
+ const result = await lib.authenticate.createAccessToken(
+ d.auth.token,
+ {
+ scope: "write",
+ duration: {
+ d_us: "forever",
+ },
+ refreshable: true,
+ },
+ );
+ if (result.type === "ok") {
+ const { access_token } = result.body;
+ logIn({ token: access_token });
}
+ // const resp = await requestNewLoginToken(backendURL.href, d.auth.token as AccessToken)
+ // if (resp.valid) {
+ // const { token, expiration } = resp
+ // updateToken({ token, expiration });
+ // } else {
+ // updateToken(undefined)
+ // }
}
onConfirm();
} catch (ex) {
@@ -72,7 +88,7 @@ export default function Create({ onBack, onConfirm, forceId }: Props): VNode {
description: ex.message,
});
} else {
- console.error(ex)
+ console.error(ex);
}
}
}}
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx
index 9a947c9d5..8166dc739 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/create/stories.tsx
@@ -19,9 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode, FunctionalComponent } from "preact";
-import { ConfigContextProvider } from "../../../context/config.js";
+import { FunctionalComponent, h } from "preact";
import { CreatePage as TestedComponent } from "./CreatePage.js";
+import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser";
export default {
title: "Pages/Instance/Create",
@@ -37,19 +37,32 @@ function createExample<Props>(
props: Partial<Props>,
) {
const component = (args: any) => (
- <ConfigContextProvider
+ <MerchantApiProviderTesting
value={{
- currency: "TESTKUDOS",
- version: "1",
- currencies: {
- currency: "TESTKUDOS"
+ cancelRequest: () => {},
+ config: {
+ currency: "ARS",
+ version: "1",
+ currencies: {
+ "ASD": {
+ name: "testkudos",
+ alt_unit_names: {},
+ num_fractional_input_digits: 1,
+ num_fractional_normal_digits: 1,
+ num_fractional_trailing_zero_digits: 1,
+ }
+ },
+ exchanges: [],
+ name: "taler-merchant"
},
- exchanges: [],
- name: "taler-merchant"
+ hints: [],
+ lib: {} as any,
+ onActivity: (() => {}) as any,
+ url: new URL("asdasd"),
}}
>
<Internal {...(props as any)} />
- </ConfigContextProvider>
+ </MerchantApiProviderTesting>
);
return { component, props };
}
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
index 711a5a4f0..bc18bb352 100644
--- a/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/admin/list/TableActive.tsx
@@ -23,7 +23,7 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { StateUpdater, useEffect, useState } from "preact/hooks";
import { MerchantBackend } from "../../../declaration.js";
-import { useSessionState } from "../../../hooks/session.js";
+import { useSessionContext } from "../../../context/session.js";
interface Props {
instances: MerchantBackend.Instances.Instance[];
@@ -149,7 +149,7 @@ function Table({
onPurge,
}: TableProps): VNode {
const { i18n } = useTranslationContext();
- const { impersonate } = useSessionState()
+ const { impersonate } = useSessionContext()
return (
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
index 13dd3a2f6..2a37ee588 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx
@@ -18,11 +18,11 @@ import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Loading } from "../../../components/exception/loading.js";
import { DeleteModal } from "../../../components/modal/index.js";
-import { useInstanceContext } from "../../../context/instance.js";
import { MerchantBackend } from "../../../declaration.js";
import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
import { DetailPage } from "./DetailPage.js";
import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { useSessionContext } from "../../../context/session.js";
interface Props {
onUnauthorized: () => VNode;
@@ -39,7 +39,7 @@ export default function Detail({
onDelete,
onNotFound,
}: Props): VNode {
- const { id } = useInstanceContext();
+ const { state } = useSessionContext();
const result = useInstanceDetails();
const [deleting, setDeleting] = useState<boolean>(false);
@@ -69,7 +69,7 @@ export default function Detail({
/>
{deleting && (
<DeleteModal
- element={{ name: result.data.name, id }}
+ element={{ name: result.data.name, id: state.instance }}
onCancel={() => setDeleting(false)}
onConfirm={async (): Promise<void> => {
try {
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 aabe67e00..94e19bb6e 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/details/stories.tsx
@@ -19,8 +19,8 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode, FunctionalComponent } from "preact";
-import { ConfigContextProvider } from "../../../context/config.js";
+import { MerchantApiProviderTesting } from "@gnu-taler/web-util/browser";
+import { FunctionalComponent, h } from "preact";
import { DetailPage as TestedComponent } from "./DetailPage.js";
export default {
@@ -37,19 +37,32 @@ function createExample<Props>(
props: Partial<Props>,
) {
const component = (args: any) => (
- <ConfigContextProvider
+ <MerchantApiProviderTesting
value={{
- currency: "TESTKUDOS",
- version: "1",
- currencies: {
- currency: "TESTKUDOS"
+ cancelRequest: () => {},
+ config: {
+ currency: "ARS",
+ version: "1",
+ currencies: {
+ "ASD": {
+ name: "testkudos",
+ alt_unit_names: {},
+ num_fractional_input_digits: 1,
+ num_fractional_normal_digits: 1,
+ num_fractional_trailing_zero_digits: 1,
+ }
+ },
+ exchanges: [],
+ name: "taler-merchant"
},
- exchanges: [],
- name: "taler-merchant"
+ hints: [],
+ lib: {} as any,
+ onActivity: (() => {}) as any,
+ url: new URL("asdasd"),
}}
>
<Internal {...(props as any)} />
- </ConfigContextProvider>
+ </MerchantApiProviderTesting>
);
return { component, props };
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
index 5633d93ab..fca123773 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
@@ -19,10 +19,18 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { AbsoluteTime, Amounts, Duration, TalerProtocolDuration } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ AbsoluteTime,
+ Amounts,
+ Duration,
+ TalerProtocolDuration,
+} from "@gnu-taler/taler-util";
+import {
+ useMerchantApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { format, isFuture } from "date-fns";
-import { ComponentChildren, Fragment, VNode, h } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useEffect, useState } from "preact/hooks";
import {
FormErrors,
@@ -39,10 +47,8 @@ import { InputToggle } from "../../../../components/form/InputToggle.js";
import { InventoryProductForm } from "../../../../components/product/InventoryProductForm.js";
import { NonInventoryProductFrom } from "../../../../components/product/NonInventoryProductForm.js";
import { ProductList } from "../../../../components/product/ProductList.js";
-import { useConfigContext } from "../../../../context/config.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
import { usePreference } from "../../../../hooks/preference.js";
-import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
@@ -58,9 +64,16 @@ interface InstanceConfig {
default_wire_transfer_delay: TalerProtocolDuration;
}
-function with_defaults(config: InstanceConfig, currency: string): Partial<Entity> {
- const defaultPayDeadline = Duration.fromTalerProtocolDuration(config.default_pay_delay);
- const defaultWireDeadline = Duration.fromTalerProtocolDuration(config.default_wire_transfer_delay);
+function with_defaults(
+ config: InstanceConfig,
+ _currency: string,
+): Partial<Entity> {
+ const defaultPayDeadline = Duration.fromTalerProtocolDuration(
+ config.default_pay_delay,
+ );
+ const defaultWireDeadline = Duration.fromTalerProtocolDuration(
+ config.default_wire_transfer_delay,
+ );
return {
inventoryProducts: {},
@@ -69,9 +82,9 @@ function with_defaults(config: InstanceConfig, currency: string): Partial<Entity
payments: {
max_fee: undefined,
createToken: true,
- pay_deadline: (defaultPayDeadline),
- refund_deadline: (defaultPayDeadline),
- wire_transfer_deadline: (defaultWireDeadline),
+ pay_deadline: defaultPayDeadline,
+ refund_deadline: defaultPayDeadline,
+ wire_transfer_deadline: defaultWireDeadline,
},
shipping: {},
extra: {},
@@ -114,26 +127,17 @@ interface Entity {
extra: Record<string, string>;
}
-const stringIsValidJSON = (value: string) => {
- try {
- JSON.parse(value.trim());
- return true;
- } catch {
- return false;
- }
-};
-
export function CreatePage({
onCreate,
onBack,
instanceConfig,
instanceInventory,
}: Props): VNode {
- const config = useConfigContext();
- const instance_default = with_defaults(instanceConfig, config.currency)
+ const { config } = useMerchantApiContext();
+ const instance_default = with_defaults(instanceConfig, config.currency);
const [value, valueHandler] = useState(instance_default);
const zero = Amounts.zeroOfCurrency(config.currency);
- const [settings, updateSettings] = usePreference()
+ const [settings, updateSettings] = usePreference();
const inventoryList = Object.values(value.inventoryProducts || {});
const productList = Object.values(value.products || {});
@@ -158,22 +162,25 @@ export function CreatePage({
refund_deadline: !value.payments?.refund_deadline
? undefined
: value.payments.pay_deadline &&
- Duration.cmp(value.payments.refund_deadline, value.payments.pay_deadline) === -1
- ? i18n.str`refund deadline cannot be before pay deadline`
- : value.payments.wire_transfer_deadline &&
Duration.cmp(
- value.payments.wire_transfer_deadline,
value.payments.refund_deadline,
+ value.payments.pay_deadline,
) === -1
+ ? i18n.str`refund deadline cannot be before pay deadline`
+ : value.payments.wire_transfer_deadline &&
+ Duration.cmp(
+ value.payments.wire_transfer_deadline,
+ value.payments.refund_deadline,
+ ) === -1
? i18n.str`wire transfer deadline cannot be before refund deadline`
: undefined,
pay_deadline: !value.payments?.pay_deadline
? i18n.str`required`
: value.payments.wire_transfer_deadline &&
- Duration.cmp(
- value.payments.wire_transfer_deadline,
- value.payments.pay_deadline,
- ) === -1
+ Duration.cmp(
+ value.payments.wire_transfer_deadline,
+ value.payments.pay_deadline,
+ ) === -1
? i18n.str`wire transfer deadline cannot be before pay deadline`
: undefined,
wire_transfer_deadline: !value.payments?.wire_transfer_deadline
@@ -184,12 +191,11 @@ export function CreatePage({
: !value.payments?.refund_deadline
? i18n.str`should have a refund deadline`
: Duration.cmp(
- value.payments.refund_deadline,
- value.payments.auto_refund_deadline,
- ) == -1
+ value.payments.refund_deadline,
+ value.payments.auto_refund_deadline,
+ ) == -1
? i18n.str`auto refund cannot be after refund deadline`
: undefined,
-
}),
shipping: undefinedIfEmpty({
delivery_date: !value.shipping?.delivery_date
@@ -214,18 +220,34 @@ export function CreatePage({
summary: order.pricing.summary,
products: productList,
extra: undefinedIfEmpty(value.extra),
- pay_deadline: !value.payments.pay_deadline ?
- i18n.str`required` :
- AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.pay_deadline))
- ,// : undefined,
+ pay_deadline: !value.payments.pay_deadline
+ ? i18n.str`required`
+ : AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ value.payments.pay_deadline,
+ ),
+ ), // : undefined,
wire_transfer_deadline: value.payments.wire_transfer_deadline
- ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.wire_transfer_deadline))
+ ? AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ value.payments.wire_transfer_deadline,
+ ),
+ )
: undefined,
refund_deadline: value.payments.refund_deadline
- ? AbsoluteTime.toProtocolTimestamp(AbsoluteTime.addDuration(AbsoluteTime.now(), value.payments.refund_deadline))
+ ? AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ value.payments.refund_deadline,
+ ),
+ )
: undefined,
auto_refund: value.payments.auto_refund_deadline
- ? Duration.toTalerProtocolDuration(value.payments.auto_refund_deadline)
+ ? Duration.toTalerProtocolDuration(
+ value.payments.auto_refund_deadline,
+ )
: undefined,
max_fee: value.payments.max_fee as string,
@@ -301,7 +323,7 @@ export function CreatePage({
const totalAsString = Amounts.stringify(totalPrice.amount);
const allProducts = productList.concat(inventoryList.map(asProduct));
- const [newField, setNewField] = useState("")
+ const [newField, setNewField] = useState("");
useEffect(() => {
valueHandler((v) => {
@@ -328,37 +350,43 @@ export function CreatePage({
);
// if there is no default pay deadline
- const noDefault_payDeadline = !instance_default.payments || !instance_default.payments.pay_deadline
+ const noDefault_payDeadline =
+ !instance_default.payments || !instance_default.payments.pay_deadline;
// and there is no default wire deadline
- const noDefault_wireDeadline = !instance_default.payments || !instance_default.payments.wire_transfer_deadline
+ const noDefault_wireDeadline =
+ !instance_default.payments ||
+ !instance_default.payments.wire_transfer_deadline;
// user required to set the taler options
- const requiresSomeTalerOptions = noDefault_payDeadline || noDefault_wireDeadline
-
+ const requiresSomeTalerOptions =
+ noDefault_payDeadline || noDefault_wireDeadline;
return (
<div>
-
<section class="section is-main-section">
<div class="tabs is-toggle is-fullwidth is-small">
<ul>
- <li class={!settings.advanceOrderMode ? "is-active" : ""} onClick={() => {
- updateSettings({
- ...settings,
- advanceOrderMode: false
- })
- }}>
- <a >
- <span><i18n.Translate>Simple</i18n.Translate></span>
+ <li
+ class={!settings.advanceOrderMode ? "is-active" : ""}
+ onClick={() => {
+ updateSettings("advanceOrderMode", false);
+ }}
+ >
+ <a>
+ <span>
+ <i18n.Translate>Simple</i18n.Translate>
+ </span>
</a>
</li>
- <li class={settings.advanceOrderMode ? "is-active" : ""} onClick={() => {
- updateSettings({
- ...settings,
- advanceOrderMode: true
- })
- }}>
- <a >
- <span><i18n.Translate>Advanced</i18n.Translate></span>
+ <li
+ class={settings.advanceOrderMode ? "is-active" : ""}
+ onClick={() => {
+ updateSettings("advanceOrderMode", true);
+ }}
+ >
+ <a>
+ <span>
+ <i18n.Translate>Advanced</i18n.Translate>
+ </span>
</a>
</li>
</ul>
@@ -386,7 +414,7 @@ export function CreatePage({
inventory={instanceInventory}
/>
- {settings.advanceOrderMode &&
+ {settings.advanceOrderMode && (
<NonInventoryProductFrom
productToEdit={editingProduct}
onAddProduct={(p) => {
@@ -394,7 +422,7 @@ export function CreatePage({
return addNewProduct(p);
}}
/>
- }
+ )}
{allProducts.length > 0 && (
<ProductList
@@ -437,8 +465,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`}
@@ -459,7 +487,7 @@ export function CreatePage({
tooltip={i18n.str`Title of the order to be shown to the customer`}
/>
- {settings.advanceOrderMode &&
+ {settings.advanceOrderMode && (
<InputGroup
name="shipping"
label={i18n.str`Shipping and Fulfillment`}
@@ -485,146 +513,201 @@ export function CreatePage({
tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
/>
</InputGroup>
- }
+ )}
- {(settings.advanceOrderMode || requiresSomeTalerOptions) &&
+ {(settings.advanceOrderMode || requiresSomeTalerOptions) && (
<InputGroup
name="payments"
label={i18n.str`Taler payment options`}
tooltip={i18n.str`Override default Taler payment settings for this order`}
>
- {(settings.advanceOrderMode || noDefault_payDeadline) && <InputDuration
- name="payments.pay_deadline"
- label={i18n.str`Payment time`}
- help={<DeadlineHelp duration={value.payments?.pay_deadline} />}
- withForever
- withoutClear
- tooltip={i18n.str`Time for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run after the order is created.`}
- side={
- <span>
- <button class="button" onClick={() => {
- const c = {
- ...value,
- payments: {
- ...(value.payments ?? {}),
- pay_deadline: instance_default.payments?.pay_deadline
- }
- }
- valueHandler(c)
- }}>
- <i18n.Translate>default</i18n.Translate>
- </button>
- </span>
- }
- />}
- {settings.advanceOrderMode && <InputDuration
- name="payments.refund_deadline"
- label={i18n.str`Refund time`}
- help={<DeadlineHelp duration={value.payments?.refund_deadline} />}
- withForever
- withoutClear
- tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`}
- side={
- <span>
- <button class="button" onClick={() => {
- valueHandler({
- ...value,
- payments: {
- ...(value.payments ?? {}),
- refund_deadline: instance_default.payments?.refund_deadline
- }
- })
- }}>
- <i18n.Translate>default</i18n.Translate>
- </button>
- </span>
- }
- />}
- {(settings.advanceOrderMode || noDefault_wireDeadline) && <InputDuration
- name="payments.wire_transfer_deadline"
- label={i18n.str`Wire transfer time`}
- help={<DeadlineHelp duration={value.payments?.wire_transfer_deadline} />}
- withoutClear
- withForever
- tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`}
- side={
- <span>
- <button class="button" onClick={() => {
- valueHandler({
- ...value,
- payments: {
- ...(value.payments ?? {}),
- wire_transfer_deadline: instance_default.payments?.wire_transfer_deadline
- }
- })
- }}>
- <i18n.Translate>default</i18n.Translate>
- </button>
- </span>
- }
- />}
- {settings.advanceOrderMode && <InputDuration
- name="payments.auto_refund_deadline"
- label={i18n.str`Auto-refund time`}
- help={<DeadlineHelp duration={value.payments?.auto_refund_deadline} />}
- tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
- withForever
- />}
-
- {settings.advanceOrderMode && <InputCurrency
- name="payments.max_fee"
- label={i18n.str`Maximum fee`}
- tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
- />}
- {settings.advanceOrderMode && <InputToggle
- name="payments.createToken"
- label={i18n.str`Create token`}
- tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`}
- />}
- {settings.advanceOrderMode && <InputNumber
- name="payments.minimum_age"
- label={i18n.str`Minimum age required`}
- tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
- help={
- minAgeByProducts > 0
- ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
- : i18n.str`No product with age restriction in this order`
- }
- />}
+ {(settings.advanceOrderMode || noDefault_payDeadline) && (
+ <InputDuration
+ name="payments.pay_deadline"
+ label={i18n.str`Payment time`}
+ help={
+ <DeadlineHelp duration={value.payments?.pay_deadline} />
+ }
+ withForever
+ withoutClear
+ tooltip={i18n.str`Time for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline. Time start to run after the order is created.`}
+ side={
+ <span>
+ <button
+ class="button"
+ onClick={() => {
+ const c = {
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ pay_deadline:
+ instance_default.payments?.pay_deadline,
+ },
+ };
+ valueHandler(c);
+ }}
+ >
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
+ />
+ )}
+ {settings.advanceOrderMode && (
+ <InputDuration
+ name="payments.refund_deadline"
+ label={i18n.str`Refund time`}
+ help={
+ <DeadlineHelp
+ duration={value.payments?.refund_deadline}
+ />
+ }
+ withForever
+ withoutClear
+ tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`}
+ side={
+ <span>
+ <button
+ class="button"
+ onClick={() => {
+ valueHandler({
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ refund_deadline:
+ instance_default.payments?.refund_deadline,
+ },
+ });
+ }}
+ >
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
+ />
+ )}
+ {(settings.advanceOrderMode || noDefault_wireDeadline) && (
+ <InputDuration
+ name="payments.wire_transfer_deadline"
+ label={i18n.str`Wire transfer time`}
+ help={
+ <DeadlineHelp
+ duration={value.payments?.wire_transfer_deadline}
+ />
+ }
+ withoutClear
+ withForever
+ tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`}
+ side={
+ <span>
+ <button
+ class="button"
+ onClick={() => {
+ valueHandler({
+ ...value,
+ payments: {
+ ...(value.payments ?? {}),
+ wire_transfer_deadline:
+ instance_default.payments
+ ?.wire_transfer_deadline,
+ },
+ });
+ }}
+ >
+ <i18n.Translate>default</i18n.Translate>
+ </button>
+ </span>
+ }
+ />
+ )}
+ {settings.advanceOrderMode && (
+ <InputDuration
+ name="payments.auto_refund_deadline"
+ label={i18n.str`Auto-refund time`}
+ help={
+ <DeadlineHelp
+ duration={value.payments?.auto_refund_deadline}
+ />
+ }
+ tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
+ withForever
+ />
+ )}
+
+ {settings.advanceOrderMode && (
+ <InputCurrency
+ name="payments.max_fee"
+ label={i18n.str`Maximum fee`}
+ tooltip={i18n.str`Maximum fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
+ />
+ )}
+ {settings.advanceOrderMode && (
+ <InputToggle
+ name="payments.createToken"
+ label={i18n.str`Create token`}
+ tooltip={i18n.str`If the order ID is easy to guess the token will prevent user to steal orders from others.`}
+ />
+ )}
+ {settings.advanceOrderMode && (
+ <InputNumber
+ name="payments.minimum_age"
+ label={i18n.str`Minimum age required`}
+ tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
+ help={
+ minAgeByProducts > 0
+ ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
+ : i18n.str`No product with age restriction in this order`
+ }
+ />
+ )}
</InputGroup>
- }
+ )}
- {settings.advanceOrderMode &&
+ {settings.advanceOrderMode && (
<InputGroup
name="extra"
label={i18n.str`Additional information`}
tooltip={i18n.str`Custom information to be included in the contract for this order.`}
>
- {Object.keys(value.extra ?? {}).map((key) => {
-
- return <Input
- name={`extra.${key}`}
- inputType="multiline"
- label={key}
- tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
- side={
- <button class="button" onClick={(e) => {
- if (value.extra && value.extra[key] !== undefined) {
- console.log(value.extra)
- delete value.extra[key]
- }
- valueHandler({
- ...value,
- })
- }}>remove</button>
- }
- />
+ {Object.keys(value.extra ?? {}).map((key, idx) => {
+ return (
+ <Input
+ name={`extra.${key}`}
+ key={String(idx)}
+ inputType="multiline"
+ label={key}
+ tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
+ side={
+ <button
+ class="button"
+ onClick={(e) => {
+ if (
+ value.extra &&
+ value.extra[key] !== undefined
+ ) {
+ console.log(value.extra);
+ delete value.extra[key];
+ }
+ valueHandler({
+ ...value,
+ });
+ e.preventDefault();
+ }}
+ >
+ remove
+ </button>
+ }
+ />
+ );
})}
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
<i18n.Translate>Custom field name</i18n.Translate>
- <span class="icon has-tooltip-right" data-tooltip={"new extra field"}>
+ <span
+ class="icon has-tooltip-right"
+ data-tooltip={"new extra field"}
+ >
<i class="mdi mdi-information" />
</span>
</label>
@@ -632,23 +715,33 @@ export function CreatePage({
<div class="field-body is-flex-grow-3">
<div class="field">
<p class="control">
- <input class="input " value={newField} onChange={(e) => setNewField(e.currentTarget.value)} />
+ <input
+ class="input "
+ value={newField}
+ onChange={(e) => setNewField(e.currentTarget.value)}
+ />
</p>
</div>
</div>
- <button class="button" onClick={(e) => {
- setNewField("")
- valueHandler({
- ...value,
- extra: {
- ...(value.extra ?? {}),
- [newField]: ""
- }
- })
- }}>add</button>
+ <button
+ class="button"
+ onClick={(e) => {
+ setNewField("");
+ valueHandler({
+ ...value,
+ extra: {
+ ...(value.extra ?? {}),
+ [newField]: "",
+ },
+ });
+ e.preventDefault();
+ }}
+ >
+ add
+ </button>
</div>
</InputGroup>
- }
+ )}
</FormProvider>
<div class="buttons is-right mt-5">
@@ -686,20 +779,24 @@ function asProduct(p: ProductAndQuantity): MerchantBackend.Product {
};
}
-
function DeadlineHelp({ duration }: { duration?: Duration }): VNode {
const { i18n } = useTranslationContext();
- const [now, setNow] = useState(AbsoluteTime.now())
+ const [now, setNow] = useState(AbsoluteTime.now());
useEffect(() => {
const iid = setInterval(() => {
- setNow(AbsoluteTime.now())
- }, 60 * 1000)
+ setNow(AbsoluteTime.now());
+ }, 60 * 1000);
return () => {
- clearInterval(iid)
- }
- })
- if (!duration) return <i18n.Translate>Disabled</i18n.Translate>
- const when = AbsoluteTime.addDuration(now, duration)
- if (when.t_ms === "never") return <i18n.Translate>No deadline</i18n.Translate>
- return <i18n.Translate>Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}</i18n.Translate>
+ clearInterval(iid);
+ };
+ });
+ if (!duration) return <i18n.Translate>Disabled</i18n.Translate>;
+ const when = AbsoluteTime.addDuration(now, duration);
+ if (when.t_ms === "never")
+ return <i18n.Translate>No deadline</i18n.Translate>;
+ return (
+ <i18n.Translate>
+ Deadline at {format(when.t_ms, "dd/MM/yy HH:mm")}
+ </i18n.Translate>
+ );
}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx
index 1efaaf6e0..69e9df52e 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
@@ -20,7 +20,7 @@
*/
import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
-import { useMerchantApiContext, useTranslationContext } from "@gnu-taler/web-util/browser";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format, formatDistance } from "date-fns";
import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
@@ -33,6 +33,7 @@ import { InputGroup } from "../../../../components/form/InputGroup.js";
import { InputLocation } from "../../../../components/form/InputLocation.js";
import { TextField } from "../../../../components/form/TextField.js";
import { ProductList } from "../../../../components/product/ProductList.js";
+import { useSessionContext } from "../../../../context/session.js";
import { MerchantBackend } from "../../../../declaration.js";
import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js";
import { mergeRefunds } from "../../../../utils/amount.js";
@@ -415,10 +416,12 @@ function PaidPage({
})
const [value, valueHandler] = useState<Partial<Paid>>(order);
- const { url: backendURL } = useMerchantApiContext();
+ const {
+ state: { backendUrl },
+ } = useSessionContext();
const refundurl = stringifyRefundUri({
- merchantBaseUrl: backendURL.href,
+ merchantBaseUrl: backendUrl,
orderId: order.contract_terms.order_id
})
const refundable =
@@ -764,7 +767,3 @@ export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode {
</Fragment>
);
}
-
-async function copyToClipboard(text: string) {
- return navigator.clipboard.writeText(text);
-}
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/Table.tsx
index 87e84945c..cebc4afe6 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
@@ -20,7 +20,10 @@
*/
import { Amounts } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ useMerchantApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { format } from "date-fns";
import { h, VNode } from "preact";
import { StateUpdater, useState } from "preact/hooks";
@@ -33,10 +36,12 @@ import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputGroup } from "../../../../components/form/InputGroup.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
import { ConfirmModal } from "../../../../components/modal/index.js";
-import { useConfigContext } from "../../../../context/config.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
import { mergeRefunds } from "../../../../utils/amount.js";
-import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js";
+import {
+ datetimeFormatForSettings,
+ usePreference,
+} from "../../../../hooks/preference.js";
type Entity = MerchantBackend.Orders.OrderHistoryEntry & WithId;
interface Props {
@@ -141,10 +146,7 @@ function Table({
return (
<div class="table-container">
{hasMoreBefore && (
- <button
- class="button is-fullwidth"
- onClick={onLoadMoreBefore}
- >
+ <button class="button is-fullwidth" onClick={onLoadMoreBefore}>
<i18n.Translate>load newer orders</i18n.Translate>
</button>
)}
@@ -174,9 +176,9 @@ function Table({
{i.timestamp.t_s === "never"
? "never"
: format(
- new Date(i.timestamp.t_s * 1000),
- datetimeFormatForSettings(settings),
- )}
+ new Date(i.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
</td>
<td
onClick={(): void => onSelect(i)}
@@ -218,10 +220,7 @@ function Table({
</tbody>
</table>
{hasMoreAfter && (
- <button
- class="button is-fullwidth"
- onClick={onLoadMoreAfter}
- >
+ <button class="button is-fullwidth" onClick={onLoadMoreAfter}>
<i18n.Translate>load older orders</i18n.Translate>
</button>
)}
@@ -268,7 +267,7 @@ export function RefundModal({
order.order_status === "paid" ? order.refund_details : []
).reduce(mergeRefunds, []);
- const config = useConfigContext();
+ const { config } = useMerchantApiContext();
const totalRefunded = refunds
.map((r) => r.amount)
.reduce(
@@ -362,9 +361,9 @@ export function RefundModal({
{r.timestamp.t_s === "never"
? "never"
: format(
- new Date(r.timestamp.t_s * 1000),
- datetimeFormatForSettings(settings),
- )}
+ new Date(r.timestamp.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
</td>
<td>{r.amount}</td>
<td>{r.reason}</td>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
index 83345de3e..930a0d82c 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatePage.tsx
@@ -34,7 +34,6 @@ import {
import { Input } from "../../../../components/form/Input.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
-import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend } from "../../../../declaration.js";
type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
@@ -49,7 +48,6 @@ 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>>({});
@@ -145,6 +143,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
...s,
otp_key: randomRfc3548Base32Key(),
}));
+ e.preventDefault();
}}
>
<i18n.Translate>random</i18n.Translate>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
index c6591cdbe..60abc3ca6 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/otp_devices/create/CreatedSuccessfully.tsx
@@ -15,12 +15,11 @@
*/
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import { VNode, h } from "preact";
import { QR } from "../../../../components/exception/QR.js";
import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
-import { useInstanceContext } from "../../../../context/instance.js";
+import { useSessionContext } from "../../../../context/session.js";
import { MerchantBackend } from "../../../../declaration.js";
-import { useBackendContext } from "../../../../context/backend.js";
type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
@@ -34,11 +33,13 @@ export function CreatedSuccessfully({
onConfirm,
}: Props): VNode {
const { i18n } = useTranslationContext();
- const { url: backendURL } = useBackendContext()
- const { id: instanceId } = useInstanceContext();
- const issuer = new URL(backendURL).hostname;
- const qrText = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`;
- const qrTextSafe = `otpauth://totp/${instanceId}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`;
+ const {
+ state: { backendUrl },
+ } = useSessionContext();
+ const { state } = useSessionContext();
+ const issuer = backendUrl;
+ const qrText = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key}`;
+ const qrTextSafe = `otpauth://totp/${state.instance}/${entity.otp_device_id}?issuer=${issuer}&algorithm=SHA1&digits=8&period=30&secret=${entity.otp_key.substring(0, 6)}...`;
return (
<Template onConfirm={onConfirm} >
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 d27f6a022..b07582252 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
@@ -23,11 +23,12 @@ import {
AmountString,
Amounts,
Duration,
- MerchantTemplateContractDetails,
assertUnreachable,
} from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import {
+ useTranslationContext
+} from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -39,12 +40,11 @@ import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputDuration } from "../../../../components/form/InputDuration.js";
import { InputNumber } from "../../../../components/form/InputNumber.js";
import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
+import { InputTab } from "../../../../components/form/InputTab.js";
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
-import { useBackendContext } from "../../../../context/backend.js";
+import { useSessionContext } from "../../../../context/session.js";
import { MerchantBackend } from "../../../../declaration.js";
import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
-import { undefinedIfEmpty } from "../../../../utils/table.js";
-import { InputTab } from "../../../../components/form/InputTab.js";
enum Steps {
BOTH_FIXED,
@@ -55,14 +55,14 @@ enum Steps {
// type Entity = MerchantBackend.Template.TemplateAddDetails & { type: Steps };
type Entity = {
- id?: string,
- description?: string,
- otpId?: string,
- summary?: string,
- amount?: AmountString,
- minimum_age?: number,
- pay_duration?: Duration,
- type: Steps,
+ id?: string;
+ description?: string;
+ otpId?: string;
+ summary?: string;
+ amount?: AmountString;
+ minimum_age?: number;
+ pay_duration?: Duration;
+ type: Steps;
};
interface Props {
@@ -72,8 +72,10 @@ interface Props {
export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const { url: backendURL } = useBackendContext()
- const devices = useInstanceOtpDevices()
+ const {
+ state: { backendUrl },
+ } = useSessionContext();
+ const devices = useInstanceOtpDevices();
const [state, setState] = useState<Partial<Entity>>({
minimum_age: 0,
@@ -83,9 +85,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
type: Steps.NON_FIXED,
});
- const parsedPrice = !state.amount
- ? undefined
- : Amounts.parse(state.amount);
+ const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount);
const errors: FormErrors<Entity> = {
id: !state.id
@@ -93,10 +93,10 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
: !/[a-zA-Z0-9]*/.test(state.id)
? i18n.str`no valid. only characters and numbers`
: undefined,
- description: !state.description
- ? i18n.str`should not be empty`
- : undefined,
- amount: !(state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED)
+ description: !state.description ? i18n.str`should not be empty` : undefined,
+ amount: !(
+ state.type === Steps.FIXED_PRICE || state.type === Steps.BOTH_FIXED
+ )
? undefined
: !state.amount
? i18n.str`required`
@@ -105,7 +105,9 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
: Amounts.isZero(parsedPrice)
? i18n.str`must be greater than 0`
: undefined,
- summary: !(state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED)
+ summary: !(
+ state.type === Steps.FIXED_SUMMARY || state.type === Steps.BOTH_FIXED
+ )
? undefined
: !state.summary
? i18n.str`required`
@@ -130,55 +132,60 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
const submitForm = () => {
if (hasErrors || state.type === undefined) return Promise.reject();
switch (state.type) {
- case Steps.FIXED_PRICE: return onCreate({
- template_id: state.id!,
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- amount: state.amount!,
- // summary: state.summary,
- },
- otp_id: state.otpId!
- })
- case Steps.FIXED_SUMMARY: return onCreate({
- template_id: state.id!,
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- // amount: state.amount!,
- summary: state.summary,
- },
- otp_id: state.otpId!,
- })
- case Steps.NON_FIXED: return onCreate({
- template_id: state.id!,
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- // amount: state.amount!,
- // summary: state.summary,
- },
- otp_id: state.otpId!,
- })
- case Steps.BOTH_FIXED: return onCreate({
- template_id: state.id!,
- template_description: state.description!,
- template_contract: {
- minimum_age: state.minimum_age!,
- pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
- amount: state.amount!,
- summary: state.summary,
- },
- otp_id: state.otpId!,
- })
- default: assertUnreachable(state.type)
+ case Steps.FIXED_PRICE:
+ return onCreate({
+ template_id: state.id!,
+ template_description: state.description!,
+ template_contract: {
+ minimum_age: state.minimum_age!,
+ pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
+ amount: state.amount!,
+ // summary: state.summary,
+ },
+ otp_id: state.otpId!,
+ });
+ case Steps.FIXED_SUMMARY:
+ return onCreate({
+ template_id: state.id!,
+ template_description: state.description!,
+ template_contract: {
+ minimum_age: state.minimum_age!,
+ pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
+ // amount: state.amount!,
+ summary: state.summary,
+ },
+ otp_id: state.otpId!,
+ });
+ case Steps.NON_FIXED:
+ return onCreate({
+ template_id: state.id!,
+ template_description: state.description!,
+ template_contract: {
+ minimum_age: state.minimum_age!,
+ pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
+ // amount: state.amount!,
+ // summary: state.summary,
+ },
+ otp_id: state.otpId!,
+ });
+ case Steps.BOTH_FIXED:
+ return onCreate({
+ template_id: state.id!,
+ template_description: state.description!,
+ template_contract: {
+ minimum_age: state.minimum_age!,
+ pay_duration: Duration.toTalerProtocolDuration(state.pay_duration!),
+ amount: state.amount!,
+ summary: state.summary,
+ },
+ otp_id: state.otpId!,
+ });
+ default:
+ assertUnreachable(state.type);
// return onCreate(state);
- };
- }
- const deviceList = !devices.ok ? [] : devices.data.otp_devices
+ }
+ };
+ const deviceList = !devices.ok ? [] : devices.data.otp_devices;
return (
<div>
@@ -193,7 +200,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
>
<InputWithAddon<Entity>
name="id"
- help={`${backendURL}/templates/${state.id ?? ""}`}
+ help={new URL(`templates/${state.id ?? ""}`, backendUrl).href}
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`}
/>
@@ -207,12 +214,16 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
name="type"
label={i18n.str`Type`}
help={(() => {
- if (state.type === undefined) return ""
+ if (state.type === undefined) return "";
switch (state.type) {
- case Steps.NON_FIXED: return i18n.str`User will be able to input price and summary before payment.`
- case Steps.FIXED_PRICE: return i18n.str`User will be able to add a summary before payment.`
- case Steps.FIXED_SUMMARY: return i18n.str`User will be able to set the price before payment.`
- case Steps.BOTH_FIXED: return i18n.str`User will not be able to change the price or the summary.`
+ case Steps.NON_FIXED:
+ return i18n.str`User will be able to input price and summary before payment.`;
+ case Steps.FIXED_PRICE:
+ return i18n.str`User will be able to add a summary before payment.`;
+ case Steps.FIXED_SUMMARY:
+ return i18n.str`User will be able to set the price before payment.`;
+ case Steps.BOTH_FIXED:
+ return i18n.str`User will not be able to change the price or the summary.`;
}
})()}
tooltip={i18n.str`Define what the user be allowed to modify`}
@@ -224,28 +235,34 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
]}
toStr={(v: Steps): string => {
switch (v) {
- case Steps.NON_FIXED: return i18n.str`Simple`
- case Steps.FIXED_PRICE: return i18n.str`With price`
- case Steps.FIXED_SUMMARY: return i18n.str`With summary`
- case Steps.BOTH_FIXED: return i18n.str`With price and summary`
+ case Steps.NON_FIXED:
+ return i18n.str`Simple`;
+ case Steps.FIXED_PRICE:
+ return i18n.str`With price`;
+ case Steps.FIXED_SUMMARY:
+ return i18n.str`With summary`;
+ case Steps.BOTH_FIXED:
+ return i18n.str`With price and summary`;
}
}}
/>
- {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_SUMMARY ?
+ {state.type === Steps.BOTH_FIXED ||
+ state.type === Steps.FIXED_SUMMARY ? (
<Input<Entity>
name="summary"
inputType="multiline"
label={i18n.str`Fixed summary`}
tooltip={i18n.str`If specified, this template will create order with the same summary`}
/>
- : undefined}
- {state.type === Steps.BOTH_FIXED || state.type === Steps.FIXED_PRICE ?
+ ) : undefined}
+ {state.type === Steps.BOTH_FIXED ||
+ state.type === Steps.FIXED_PRICE ? (
<InputCurrency<Entity>
name="amount"
label={i18n.str`Fixed price`}
tooltip={i18n.str`If specified, this template will create order with the same price`}
/>
- : undefined}
+ ) : undefined}
<InputNumber<Entity>
name="minimum_age"
label={i18n.str`Minimum age`}
@@ -262,28 +279,29 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
name="otpId"
label={i18n.str`OTP device`}
readonly
- side={<button
- class="button is-danger"
- data-tooltip={i18n.str`without otp device`}
- onClick={(): void => {
- setState((v) => ({ ...v, otpId: undefined }));
- }}
- >
- <span>
- <i18n.Translate>remove</i18n.Translate>
- </span>
- </button>}
+ side={
+ <button
+ class="button is-danger"
+ data-tooltip={i18n.str`without otp device`}
+ onClick={(): void => {
+ setState((v) => ({ ...v, otpId: undefined }));
+ }}
+ >
+ <span>
+ <i18n.Translate>remove</i18n.Translate>
+ </span>
+ </button>
+ }
tooltip={i18n.str`Use to verify transaction in offline mode.`}
/>
<InputSearchOnList
label={i18n.str`Search device`}
onChange={(p) => setState((v) => ({ ...v, otpId: p?.id }))}
- list={deviceList.map(e => ({
+ list={deviceList.map((e) => ({
description: e.device_description,
- id: e.otp_device_id
+ id: e.otp_device_id,
}))}
/>
-
</FormProvider>
<div class="buttons is-right mt-5">
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 809151565..1aa5bc317 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
@@ -20,7 +20,10 @@
*/
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ useMerchantApiContext,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { QR } from "../../../../components/exception/QR.js";
@@ -30,9 +33,7 @@ import {
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
-import { useBackendContext } from "../../../../context/backend.js";
-import { useConfigContext } from "../../../../context/config.js";
-import { useInstanceContext } from "../../../../context/instance.js";
+import { useSessionContext } from "../../../../context/session.js";
import { MerchantBackend } from "../../../../declaration.js";
type Entity = MerchantBackend.Template.UsingTemplateDetails;
@@ -45,9 +46,10 @@ interface Props {
export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const { url: backendURL } = useBackendContext()
- const { id: instanceId } = useInstanceContext();
- const config = useConfigContext();
+ const {
+ state: { backendUrl },
+ } = useSessionContext();
+ const { config } = useMerchantApiContext();
const [state, setState] = useState<Partial<Entity>>({
amount: contract.amount,
@@ -59,30 +61,26 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
const fixedAmount = !!contract.amount;
const fixedSummary = !!contract.summary;
- const templateParams: Record<string, string> = {}
+ const templateParams: Record<string, string> = {};
if (!fixedAmount) {
if (state.amount) {
- templateParams.amount = state.amount
+ templateParams.amount = state.amount;
} else {
- templateParams.amount = config.currency
+ templateParams.amount = config.currency;
}
}
if (!fixedSummary) {
- templateParams.summary = state.summary ?? ""
+ templateParams.summary = state.summary ?? "";
}
- const merchantBaseUrl = new URL(backendURL).href;
+ const merchantBaseUrl = backendUrl;
const payTemplateUri = stringifyPayTemplateUri({
merchantBaseUrl,
templateId,
- templateParams
- })
-
- const issuer = encodeURIComponent(
- `${new URL(backendURL).host}/${instanceId}`,
- );
+ templateParams,
+ });
return (
<div>
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 cdf2ebab4..ae11ad991 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
@@ -26,7 +26,7 @@ import {
assertUnreachable
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, VNode, h } from "preact";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -37,11 +37,10 @@ import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputDuration } from "../../../../components/form/InputDuration.js";
import { InputNumber } from "../../../../components/form/InputNumber.js";
+import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
import { InputTab } from "../../../../components/form/InputTab.js";
-import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
-import { useBackendContext } from "../../../../context/backend.js";
+import { useSessionContext } from "../../../../context/session.js";
import { MerchantBackend } from "../../../../declaration.js";
-import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
enum Steps {
@@ -68,7 +67,10 @@ interface Props {
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const { url: backendURL } = useBackendContext()
+ const {
+ state: { backendUrl },
+ } = useSessionContext();
+
const intialStep =
template.template_contract.amount === undefined && template.template_contract.summary === undefined
@@ -187,7 +189,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
- {backendURL}/templates/{template.otp_id}
+ {new URL(`templates/${template.otp_id}`,backendUrl).href}
</span>
</div>
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
index 1e9186624..f2b1db29b 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -25,19 +25,23 @@ import { useState } from "preact/hooks";
import { AsyncButton } from "../../../components/exception/AsyncButton.js";
import { FormProvider } from "../../../components/form/FormProvider.js";
import { Input } from "../../../components/form/Input.js";
-import { useInstanceContext } from "../../../context/instance.js";
-import { AccessToken } from "../../../declaration.js";
import { NotificationCard } from "../../../components/menu/index.js";
+import { useSessionContext } from "../../../context/session.js";
+import { AccessToken } from "@gnu-taler/taler-util";
interface Props {
- instanceId: string;
hasToken: boolean | undefined;
onClearToken: (c: AccessToken | undefined) => void;
onNewToken: (c: AccessToken | undefined, s: AccessToken) => void;
onBack?: () => void;
}
-export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearToken }: Props): VNode {
+export function DetailPage({
+ hasToken,
+ onBack,
+ onNewToken,
+ onClearToken,
+}: Props): VNode {
type State = { old_token: string; new_token: string; repeat_token: string };
const [form, setValue] = useState<Partial<State>>({
old_token: "",
@@ -47,9 +51,10 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo
const { i18n } = useTranslationContext();
const errors = {
- old_token: hasToken && !form.old_token
- ? i18n.str`you need your access token to perform the operation`
- : undefined,
+ old_token:
+ hasToken && !form.old_token
+ ? i18n.str`you need your access token to perform the operation`
+ : undefined,
new_token: !form.new_token
? i18n.str`cannot be empty`
: form.new_token === form.old_token
@@ -65,15 +70,17 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo
(k) => (errors as any)[k] !== undefined,
);
- const instance = useInstanceContext();
+ const { state } = useSessionContext();
- const text = i18n.str`You are updating the access token from instance with id "${instance.id}"`;
+ const text = i18n.str`You are updating the access token from instance with id "${state.instance}"`;
async function submitForm() {
if (hasErrors) return;
- const oldToken = hasToken ? `secret-token:${form.old_token}` as AccessToken : undefined;
+ const oldToken = hasToken
+ ? (`secret-token:${form.old_token}` as AccessToken)
+ : undefined;
const newToken = `secret-token:${form.new_token}` as AccessToken;
- onNewToken(oldToken, newToken)
+ onNewToken(oldToken, newToken);
}
return (
@@ -84,9 +91,7 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo
<div class="level">
<div class="level-left">
<div class="level-item">
- <span class="is-size-4">
- {text}
- </span>
+ <span class="is-size-4">{text}</span>
</div>
</div>
</div>
@@ -94,7 +99,7 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo
</section>
<hr />
- {!hasToken &&
+ {!hasToken && (
<NotificationCard
notification={{
message: i18n.str`This instance doesn't have authentication token.`,
@@ -102,7 +107,7 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo
type: "WARN",
}}
/>
- }
+ )}
<div class="columns">
<div class="column" />
@@ -119,7 +124,8 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo
/>
<p>
<i18n.Translate>
- Clearing the access token will mean public access to the instance.
+ Clearing the access token will mean public access to the
+ instance.
</i18n.Translate>
</p>
<div class="buttons is-right mt-5">
@@ -127,10 +133,11 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo
class="button"
onClick={() => {
if (hasToken) {
- const oldToken = `secret-token:${form.old_token}` as AccessToken;
- onClearToken(oldToken)
+ const oldToken =
+ `secret-token:${form.old_token}` as AccessToken;
+ onClearToken(oldToken);
} else {
- onClearToken(undefined)
+ onClearToken(undefined);
}
}}
>
@@ -140,7 +147,6 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo
</Fragment>
)}
-
<Input<State>
name="new_token"
label={i18n.str`New access token`}
@@ -176,7 +182,6 @@ export function DetailPage({ instanceId, hasToken, onBack, onNewToken, onClearTo
</div>
<div class="column" />
</div>
-
</section>
</div>
);
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
index 13642ec22..d7bf7a6d5 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
@@ -16,15 +16,13 @@
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 { AccessToken, MerchantBackend } from "../../../declaration.js";
-import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
-import { DetailPage } from "./DetailPage.js";
-import { useInstanceContext } from "../../../context/instance.js";
import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading.js";
import { NotificationCard } from "../../../components/menu/index.js";
+import { MerchantBackend } from "../../../declaration.js";
+import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
import { Notification } from "../../../utils/types.js";
-import { useBackendContext } from "../../../context/backend.js";
+import { DetailPage } from "./DetailPage.js";
interface Props {
onUnauthorized: () => VNode;
@@ -45,7 +43,6 @@ export default function Token({
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { clearAccessToken, setNewAccessToken } = useInstanceAPI();
- const { id } = useInstanceContext();
const result = useInstanceDetails()
if (result.loading) return <Loading />;
@@ -69,7 +66,6 @@ export default function Token({
<Fragment>
<NotificationCard notification={notif} />
<DetailPage
- instanceId={id}
onBack={onCancel}
hasToken={hasToken}
onClearToken={async (currentToken): Promise<void> => {
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
index eb25045a0..576c21cd2 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx
@@ -20,7 +20,7 @@
*/
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -30,7 +30,6 @@ import {
import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
-import { useConfigContext } from "../../../../context/config.js";
import { MerchantBackend } from "../../../../declaration.js";
import {
CROCKFORD_BASE32_REGEX,
@@ -47,7 +46,6 @@ interface Props {
export function CreatePage({ accounts, onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const { currency } = useConfigContext();
const [state, setState] = useState<Partial<Entity>>({
wtid: "",
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 ff0d55d2d..f0f0bfac9 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/UpdatePage.tsx
@@ -19,6 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { Duration } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -28,10 +29,9 @@ import {
FormProvider,
} from "../../../components/form/FormProvider.js";
import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js";
-import { useInstanceContext } from "../../../context/instance.js";
import { MerchantBackend } from "../../../declaration.js";
import { undefinedIfEmpty } from "../../../utils/table.js";
-import { Duration } from "@gnu-taler/taler-util";
+import { useSessionContext } from "../../../context/session.js";
export type Entity = Omit<Omit<MerchantBackend.Instances.InstanceReconfigurationMessage, "default_pay_delay">, "default_wire_transfer_delay"> & {
default_pay_delay: Duration,
@@ -64,7 +64,7 @@ export function UpdatePage({
selected,
onBack,
}: Props): VNode {
- const { id } = useInstanceContext();
+ const { state } = useSessionContext();
const [value, valueHandler] = useState<Partial<Entity>>(convert(selected));
@@ -125,7 +125,7 @@ export function UpdatePage({
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
- <i18n.Translate>Instance id</i18n.Translate>: <b>{id}</b>
+ <i18n.Translate>Instance id</i18n.Translate>: <b>{state.instance}</b>
</span>
</div>
</div>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
index be3793ac3..de1371974 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
@@ -24,8 +24,7 @@ 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 { useInstanceContext } from "../../../context/instance.js";
-import { AccessToken, MerchantBackend } from "../../../declaration.js";
+import { MerchantBackend } from "../../../declaration.js";
import {
useInstanceAPI,
useInstanceDetails,
@@ -65,7 +64,6 @@ function CommonUpdate(
onConfirm,
onLoadError,
onNotFound,
- onUpdateError,
onUnauthorized,
}: Props,
result: HttpResponse<
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
index b89e5e6bf..83604711e 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/create/CreatePage.tsx
@@ -28,12 +28,8 @@ import {
FormProvider,
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
-import { InputCurrency } from "../../../../components/form/InputCurrency.js";
-import { InputDuration } from "../../../../components/form/InputDuration.js";
-import { InputNumber } from "../../../../components/form/InputNumber.js";
-import { useBackendContext } from "../../../../context/backend.js";
-import { MerchantBackend } from "../../../../declaration.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
+import { MerchantBackend } from "../../../../declaration.js";
type Entity = MerchantBackend.Webhooks.WebhookAddDetails;
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
index 304ac90f3..be21629d5 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/webhooks/update/UpdatePage.tsx
@@ -28,7 +28,6 @@ import {
FormProvider,
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
-import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
type Entity = MerchantBackend.Webhooks.WebhookPatchDetails & WithId;
diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
index d94b7e506..1c0b915bd 100644
--- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
@@ -19,7 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ AccessToken,
+ HttpStatusCode,
+ TalerAuthentication,
+} from "@gnu-taler/taler-util";
import {
useMerchantApiContext,
useTranslationContext,
@@ -27,40 +31,68 @@ import {
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { NotificationCard } from "../../components/menu/index.js";
-import { AccessToken } from "../../declaration.js";
-import { DEFAULT_ADMIN_USERNAME, useSessionState } from "../../hooks/session.js";
+import {
+ DEFAULT_ADMIN_USERNAME,
+ useSessionContext,
+} from "../../context/session.js";
import { Notification } from "../../utils/types.js";
-interface Props {
-}
+interface Props {}
-function normalizeToken(r: string): AccessToken {
- return `secret-token:${r}` as AccessToken;
-}
+const tokenRequest = {
+ scope: "write",
+ duration: {
+ d_us: "forever" as const,
+ },
+ refreshable: true,
+};
export function LoginPage(_p: Props): VNode {
const [token, setToken] = useState("");
const [notif, setNotif] = useState<Notification | undefined>(undefined);
- const { state, logIn } = useSessionState();
+ const { state, logIn } = useSessionContext();
const { lib } = useMerchantApiContext();
const { i18n } = useTranslationContext();
+ async function doImpersonateImpl(instanceId: string) {
+ const result = await lib
+ .impersonate(instanceId)
+ .createAccessTokenMerchant(token, tokenRequest);
+ if (result.type === "ok") {
+ const { token } = result.body;
+ logIn({ token });
+ return;
+ } else {
+ switch (result.case) {
+ case HttpStatusCode.Unauthorized: {
+ setNotif({
+ message: "Your password is incorrect",
+ type: "ERROR",
+ });
+ return;
+ }
+ case HttpStatusCode.NotFound: {
+ setNotif({
+ message: "Your instance not found",
+ type: "ERROR",
+ });
+ return;
+ }
+ }
+ }
+ }
async function doLoginImpl() {
- const secretToken = normalizeToken(token);
- const result = await lib.authenticate.createAccessToken(secretToken, {
- scope: "write",
- duration: {
- d_us: "forever"
- },
- refreshable: true,
- });
+ const result = await lib.authenticate.createAccessTokenMerchant(
+ token,
+ tokenRequest,
+ );
if (result.type === "ok") {
- const { access_token } = result.body;
- logIn({ instance: state.instance, token: access_token });
+ const { token } = result.body;
+ logIn({ token });
return;
} else {
- switch(result.case) {
+ switch (result.case) {
case HttpStatusCode.Unauthorized: {
setNotif({
message: "Your password is incorrect",
@@ -79,8 +111,8 @@ export function LoginPage(_p: Props): VNode {
}
}
- if (state.isAdmin && state.instance !== DEFAULT_ADMIN_USERNAME) {
- //admin trying to access another instance
+ if (state.status === "loggedIn" && state.impersonate !== undefined) {
+ //the user is loggedin but trying to do an impersonation
return (
<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
@@ -115,7 +147,9 @@ export function LoginPage(_p: Props): VNode {
placeholder={"current access token"}
name="token"
onKeyPress={(e) =>
- e.keyCode === 13 ? doLoginImpl() : null
+ e.keyCode === 13
+ ? doImpersonateImpl(state.instance)
+ : null
}
value={token}
onInput={(e): void => setToken(e?.currentTarget.value)}
@@ -133,7 +167,7 @@ export function LoginPage(_p: Props): VNode {
borderTop: 0,
}}
>
- <AsyncButton onClick={doLoginImpl}>
+ <AsyncButton onClick={() => doImpersonateImpl(state.instance)}>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</footer>
diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
index 4efda43be..6290f48e6 100644
--- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
@@ -1,10 +1,30 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021-2024 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
import { VNode, h } from "preact";
-import { FormErrors, FormProvider } from "../../components/form/FormProvider.js";
+import {
+ FormErrors,
+ FormProvider,
+} from "../../components/form/FormProvider.js";
import { InputSelector } from "../../components/form/InputSelector.js";
import { InputToggle } from "../../components/form/InputToggle.js";
import { LangSelector } from "../../components/menu/LangSelector.js";
-import { Settings, usePreference } from "../../hooks/preference.js";
+import { Preferences, usePreference } from "../../hooks/preference.js";
+import { AbsoluteTime } from "@gnu-taler/taler-util";
function getBrowserLang(): string | undefined {
if (typeof window === "undefined") return undefined;
@@ -14,99 +34,107 @@ function getBrowserLang(): string | undefined {
}
export function Settings({ onClose }: { onClose?: () => void }): VNode {
- const { i18n } = useTranslationContext()
- const borwserLang = getBrowserLang()
- const { update } = useLang(undefined, {})
+ const { i18n } = useTranslationContext();
+ const borwserLang = getBrowserLang();
+ const { update } = useLang(undefined, {});
- const [value, updateValue] = usePreference()
- const errors: FormErrors<Settings> = {
- }
+ const [value, , updateValue] = usePreference();
+ const errors: FormErrors<Preferences> = {};
- function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
- const next = s(value)
- const v: Settings = {
+ function valueHandler(s: (d: Partial<Preferences>) => Partial<Preferences>): void {
+ const next = s(value);
+ const v: Preferences = {
advanceOrderMode: next.advanceOrderMode ?? false,
- dateFormat: next.dateFormat ?? "ymd"
- }
- updateValue(v)
+ hideKycUntil: next.hideKycUntil ?? AbsoluteTime.never(),
+ dateFormat: next.dateFormat ?? "ymd",
+ };
+ updateValue(v);
}
- return <div>
- <section class="section is-main-section">
- <div class="columns">
- <div class="column" />
- <div class="column is-four-fifths">
- <div>
-
- <FormProvider<Settings>
- name="settings"
- errors={errors}
- object={value}
- valueHandler={valueHandler}
- >
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Language</i18n.Translate>
- <span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}>
- <i class="mdi mdi-information" />
- </span>
- </label>
- </div>
- <div class="field field-body has-addons is-flex-grow-3">
- <LangSelector />
- &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>}
+ return (
+ <div>
+ <section class="section is-main-section">
+ <div class="columns">
+ <div class="column" />
+ <div class="column is-four-fifths">
+ <div>
+ <FormProvider<Preferences>
+ name="settings"
+ errors={errors}
+ object={value}
+ valueHandler={valueHandler}
+ >
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Language</i18n.Translate>
+ <span
+ class="icon has-tooltip-right"
+ data-tooltip={
+ "Force language setting instance of taking the browser"
+ }
+ >
+ <i class="mdi mdi-information" />
+ </span>
+ </label>
+ </div>
+ <div class="field field-body has-addons is-flex-grow-3">
+ <LangSelector />
+ &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));
+ e.preventDefault()
+ }}
+ >
+ <i18n.Translate>Set default</i18n.Translate>
+ </button>
+ )}
+ </div>
</div>
- </div>
- <InputToggle<Settings>
- label={i18n.str`Advance order creation`}
- tooltip={i18n.str`Shows more options in the order creation form`}
- name="advanceOrderMode"
- />
- <InputSelector<Settings>
- name="dateFormat"
- label={i18n.str`Date format`}
- expand={true}
- help={
- value.dateFormat === "dmy" ? "31/12/2001" : value.dateFormat === "mdy" ? "12/31/2001" : value.dateFormat === "ymd" ? "2001/12/31" : ""
- }
- toStr={(e) => {
- if (e === "ymd") return "year month day"
- if (e === "mdy") return "month day year"
- if (e === "dmy") return "day month year"
- return "choose one"
- }}
- values={[
- "ymd",
- "mdy",
- "dmy",
- ]}
- tooltip={i18n.str`how the date is going to be displayed`}
- />
- </FormProvider>
+ <InputToggle<Preferences>
+ label={i18n.str`Advance order creation`}
+ tooltip={i18n.str`Shows more options in the order creation form`}
+ name="advanceOrderMode"
+ />
+ <InputSelector<Preferences>
+ name="dateFormat"
+ label={i18n.str`Date format`}
+ expand={true}
+ help={
+ value.dateFormat === "dmy"
+ ? "31/12/2001"
+ : value.dateFormat === "mdy"
+ ? "12/31/2001"
+ : value.dateFormat === "ymd"
+ ? "2001/12/31"
+ : ""
+ }
+ toStr={(e) => {
+ if (e === "ymd") return "year month day";
+ if (e === "mdy") return "month day year";
+ if (e === "dmy") return "day month year";
+ return "choose one";
+ }}
+ values={["ymd", "mdy", "dmy"]}
+ tooltip={i18n.str`how the date is going to be displayed`}
+ />
+ </FormProvider>
+ </div>
</div>
+ <div class="column" />
</div>
- <div class="column" />
- </div>
- </section >
- {onClose &&
- <section class="section is-main-section">
- <button
- class="button"
- onClick={onClose}
- >
- <i18n.Translate>Close</i18n.Translate>
- </button>
</section>
- }
- </div >
-} \ No newline at end of file
+ {onClose && (
+ <section class="section is-main-section">
+ <button class="button" onClick={onClose}>
+ <i18n.Translate>Close</i18n.Translate>
+ </button>
+ </section>
+ )}
+ </div>
+ );
+}
diff --git a/packages/taler-util/src/http-client/authentication.ts b/packages/taler-util/src/http-client/authentication.ts
index e8ef6a274..00ef21a06 100644
--- a/packages/taler-util/src/http-client/authentication.ts
+++ b/packages/taler-util/src/http-client/authentication.ts
@@ -34,6 +34,7 @@ import {
AccessToken,
TalerAuthentication,
codecForTokenSuccessResponse,
+ codecForTokenSuccessResponseMerchant,
} from "./types.js";
import { makeBearerTokenAuthHeader } from "./utils.js";
@@ -85,6 +86,35 @@ export class TalerAuthenticationHttpClient {
}
}
+ /**
+ *
+ * @returns
+ */
+ async createAccessTokenMerchant(
+ password: string,
+ body: TalerAuthentication.TokenRequest,
+ ) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(password as AccessToken),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTokenSuccessResponseMerchant());
+ //FIXME: missing in docs
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await resp.text());
+ }
+ }
+
async deleteAccessToken(token: AccessToken) {
const url = new URL(`token`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
index 7407cce66..688e80c29 100644
--- a/packages/taler-util/src/http-client/merchant.ts
+++ b/packages/taler-util/src/http-client/merchant.ts
@@ -15,6 +15,7 @@
*/
import {
+ AccessToken,
HttpStatusCode,
LibtoolVersion,
PaginationParams,
@@ -64,6 +65,7 @@ import { opSuccessFromHttp, opUnknownFailure } from "../operation.js";
import {
CacheEvictor,
addMerchantPaginationParams,
+ makeBearerTokenAuthHeader,
nullEvictor,
} from "./utils.js";
@@ -126,12 +128,15 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-claim
*/
- async claimOrder(orderId: string, body: TalerMerchantApi.ClaimRequest) {
+ async claimOrder(token: AccessToken, orderId: string, body: TalerMerchantApi.ClaimRequest) {
const url = new URL(`orders/${orderId}/claim`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "POST",
body,
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ }
});
switch (resp.status) {
@@ -516,11 +521,14 @@ export class TalerMerchantInstanceHttpClient {
/**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts
*/
- async listAccounts() {
+ async listAccounts(token: AccessToken) {
const url = new URL(`private/accounts`, this.baseUrl);
const resp = await this.httpLib.fetch(url.href, {
method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ }
});
switch (resp.status) {
@@ -1496,7 +1504,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
}
getSubInstanceAPI(instanceId: string) {
- return new URL(`instances/${instanceId}`, this.baseUrl);
+ return new URL(`instances/${instanceId}/`, this.baseUrl);
}
//
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
index 05897614a..7f97f9ff1 100644
--- a/packages/taler-util/src/http-client/types.ts
+++ b/packages/taler-util/src/http-client/types.ts
@@ -221,6 +221,16 @@ export namespace TalerAuthentication {
// Opque access token.
access_token: AccessToken;
}
+ export interface TokenSuccessResponseMerchant {
+ // Expiration determined by the server.
+ // Can be based on the token_duration
+ // from the request, but ultimately the
+ // server decides the expiration.
+ expiration: Timestamp;
+
+ // Opque access token.
+ token: AccessToken;
+ }
}
// DD51 https://docs.taler.net/design-documents/051-fractional-digits.html
@@ -254,6 +264,13 @@ export const codecForTokenSuccessResponse =
.property("expiration", codecForTimestamp)
.build("TalerAuthentication.TokenSuccessResponse");
+export const codecForTokenSuccessResponseMerchant =
+ (): Codec<TalerAuthentication.TokenSuccessResponseMerchant> =>
+ buildCodecForObject<TalerAuthentication.TokenSuccessResponseMerchant>()
+ .property("token", codecForAccessToken())
+ .property("expiration", codecForTimestamp)
+ .build("TalerAuthentication.TokenSuccessResponseMerchant");
+
export const codecForCurrencySpecificiation =
(): Codec<CurrencySpecification> =>
buildCodecForObject<CurrencySpecification>()