aboutsummaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx2
-rw-r--r--packages/demobank-ui/Makefile1
-rw-r--r--packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts4
-rw-r--r--packages/idb-bridge/src/util/fakeDOMStringList.ts2
-rw-r--r--packages/merchant-backoffice-ui/src/Application.tsx32
-rw-r--r--packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx42
-rw-r--r--packages/merchant-backoffice-ui/src/InstanceRoutes.tsx35
-rw-r--r--packages/merchant-backoffice-ui/src/components/exception/login.tsx244
-rw-r--r--packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx4
-rw-r--r--packages/merchant-backoffice-ui/src/context/backend.test.ts6
-rw-r--r--packages/merchant-backoffice-ui/src/context/backend.ts66
-rw-r--r--packages/merchant-backoffice-ui/src/context/instance.ts5
-rw-r--r--packages/merchant-backoffice-ui/src/declaration.d.ts39
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/backend.ts139
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/index.ts84
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.test.ts4
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/instance.ts38
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/testing.tsx5
-rw-r--r--packages/merchant-backoffice-ui/src/hooks/useSettings.ts9
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx8
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/create/CreatePage.tsx12
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx15
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx16
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx8
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx18
-rw-r--r--packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx6
-rw-r--r--packages/merchant-backoffice-ui/src/paths/login/index.tsx299
-rw-r--r--packages/merchant-backoffice-ui/src/paths/settings/index.tsx18
-rw-r--r--packages/taler-harness/src/harness/harness.ts23
-rw-r--r--packages/taler-harness/src/harness/libeufin.ts74
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances.ts1
-rw-r--r--packages/taler-util/src/MerchantApiClient.ts3
-rw-r--r--packages/taler-util/src/taler-crypto.ts11
-rw-r--r--packages/taler-util/src/taler-types.ts116
-rw-r--r--packages/taler-util/src/wallet-types.ts48
-rw-r--r--packages/taler-wallet-cli/Makefile1
-rw-r--r--packages/taler-wallet-core/src/db.ts48
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/deposits.ts178
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts16
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts2
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts18
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts24
-rw-r--r--packages/taler-wallet-core/src/wallet.ts8
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx3
-rw-r--r--packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx12
-rw-r--r--packages/web-util/src/utils/request.ts75
51 files changed, 1022 insertions, 827 deletions
diff --git a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
index 3018f88dd..54bbc626d 100644
--- a/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AuthenticationEditorScreen.tsx
@@ -241,7 +241,7 @@ export function AuthenticationEditorScreen(): VNode {
</p>
{authAvailableSet.size > 0 && (
<p class="block">
- We couldn&apos;t find provider for some of the authentication
+ We couldn't find provider for some of the authentication
methods.
</p>
)}
diff --git a/packages/demobank-ui/Makefile b/packages/demobank-ui/Makefile
index fc570b270..8e41cc7c6 100644
--- a/packages/demobank-ui/Makefile
+++ b/packages/demobank-ui/Makefile
@@ -20,6 +20,7 @@ spa_dir=$(prefix)/share/taler/demobank-ui
.PHONY: deps
deps:
pnpm install --frozen-lockfile --filter @gnu-taler/demobank-ui...
+ pnpm run --filter @gnu-taler/demobank-ui... compile
pnpm run check
pnpm run build
diff --git a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts
index b13bd1fc3..e0e6c2bf8 100644
--- a/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts
+++ b/packages/idb-bridge/src/idb-wpt-ported/idbcursor-reused.test.ts
@@ -26,7 +26,7 @@ test("WPT idbcursor-reused.htm", async (t) => {
case 0:
cursor = e.target.result;
- t.deepEqual(cursor.value, "data", "prequisite cursor.value");
+ t.deepEqual(cursor.value, "data", "prerequisite cursor.value");
cursor.custom_cursor_value = 1;
e.target.custom_request_value = 2;
@@ -34,7 +34,7 @@ test("WPT idbcursor-reused.htm", async (t) => {
break;
case 1:
- t.deepEqual(cursor.value, "data2", "prequisite cursor.value");
+ t.deepEqual(cursor.value, "data2", "prerequisite cursor.value");
t.deepEqual(cursor.custom_cursor_value, 1, "custom cursor value");
t.deepEqual(
e.target.custom_request_value,
diff --git a/packages/idb-bridge/src/util/fakeDOMStringList.ts b/packages/idb-bridge/src/util/fakeDOMStringList.ts
index 92785f9e1..24f5c96f4 100644
--- a/packages/idb-bridge/src/util/fakeDOMStringList.ts
+++ b/packages/idb-bridge/src/util/fakeDOMStringList.ts
@@ -21,7 +21,7 @@ export interface FakeDOMStringList extends Array<string> {
item: (i: number) => string | null;
}
-// Would be nicer to sublcass Array, but I'd have to sacrifice Node 4 support to do that.
+// Would be nicer to subclass Array, but I'd have to sacrifice Node 4 support to do that.
export const fakeDOMStringList = (arr: string[]): FakeDOMStringList => {
const arr2 = arr.slice();
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx
index 5e82821ae..1a7617643 100644
--- a/packages/merchant-backoffice-ui/src/Application.tsx
+++ b/packages/merchant-backoffice-ui/src/Application.tsx
@@ -41,7 +41,8 @@ import {
import { ConfigContextProvider } from "./context/config.js";
import { useBackendConfig } from "./hooks/backend.js";
import { strings } from "./i18n/strings.js";
-import LoginPage from "./paths/login/index.js";
+import { ConnectionPage, LoginPage } from "./paths/login/index.js";
+import { LoginToken } from "./declaration.js";
export function Application(): VNode {
return (
@@ -59,25 +60,20 @@ export function Application(): VNode {
* @returns
*/
function ApplicationStatusRoutes(): VNode {
- const { url, updateLoginStatus, triedToLog } = useBackendContext();
+ const { url: backendURL, updateToken, changeBackend } = useBackendContext();
const result = useBackendConfig();
const { i18n } = useTranslationContext();
- const updateLoginInfoAndGoToRoot = (url: string, token?: string) => {
- updateLoginStatus(url, token);
- route("/");
- };
-
const { currency, version } = result.ok
? result.data
: { currency: "unknown", version: "unknown" };
const ctx = useMemo(() => ({ currency, version }), [currency, version]);
- if (!triedToLog) {
+ if (!backendURL) {
return (
<Fragment>
<NotConnectedAppMenu title="Welcome!" />
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
+ <ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@@ -91,7 +87,7 @@ function ApplicationStatusRoutes(): VNode {
return (
<Fragment>
<NotConnectedAppMenu title="Login" />
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
+ <ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@@ -109,7 +105,7 @@ function ApplicationStatusRoutes(): VNode {
description: `Check your url`,
}}
/>
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
+ <ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@@ -120,10 +116,10 @@ function ApplicationStatusRoutes(): VNode {
notification={{
message: i18n.str`Server response with an error code`,
type: "ERROR",
- description: i18n.str`Got message ${result.message} from ${result.info?.url}`,
+ description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
}}
/>
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
+ <ConnectionPage onConfirm={changeBackend} />
</Fragment>;
}
if (result.type === ErrorType.UNREADABLE) {
@@ -133,10 +129,10 @@ function ApplicationStatusRoutes(): VNode {
notification={{
message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
type: "ERROR",
- description: i18n.str`Got message ${result.message} from ${result.info?.url}`,
+ description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
}}
/>
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
+ <ConnectionPage onConfirm={changeBackend} />
</Fragment>;
}
return (
@@ -146,10 +142,10 @@ function ApplicationStatusRoutes(): VNode {
notification={{
message: i18n.str`Unexpected Error`,
type: "ERROR",
- description: i18n.str`Got message ${result.message} from ${result.info?.url}`,
+ description: i18n.str`Got message "${result.message}" from ${result.info?.url}`,
}}
/>
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
+ <ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@@ -168,7 +164,7 @@ function ApplicationStatusRoutes(): VNode {
description: i18n.str`Merchant backend server version ${result.data.version} is not compatible with the supported version ${SUPPORTED_VERSION}`,
}}
/>
- <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
+ <ConnectionPage onConfirm={changeBackend} />
</Fragment>
}
diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
index 46dea98e3..8bfbdb076 100644
--- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx
@@ -18,22 +18,23 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
+import { HttpStatusCode } from "@gnu-taler/taler-util";
import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser";
import { createHashHistory } from "history";
-import { Fragment, h, VNode } from "preact";
-import { Router, Route, route } from "preact-router";
-import { useEffect, useState } from "preact/hooks";
+import { Fragment, VNode, h } from "preact";
+import { Route, Router, route } from "preact-router";
+import { useState } from "preact/hooks";
+import { InstanceRoutes } from "./InstanceRoutes.js";
import {
- NotificationCard,
NotYetReadyAppMenu,
+ NotificationCard,
} from "./components/menu/index.js";
import { useBackendContext } from "./context/backend.js";
+import { LoginToken } from "./declaration.js";
import { useBackendInstancesTestForAdmin } from "./hooks/backend.js";
-import { InstanceRoutes } from "./InstanceRoutes.js";
-import LoginPage from "./paths/login/index.js";
-import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
-import { HttpStatusCode } from "@gnu-taler/taler-util";
+import { ConnectionPage, LoginPage } from "./paths/login/index.js";
import { Settings } from "./paths/settings/index.js";
+import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
/**
* Check if admin against /management/instances
@@ -41,15 +42,14 @@ import { Settings } from "./paths/settings/index.js";
*/
export function ApplicationReadyRoutes(): VNode {
const { i18n } = useTranslationContext();
+ const { url: backendURL, changeBackend } = useBackendContext()
const [unauthorized, setUnauthorized] = useState(false)
const {
- url: backendURL,
- updateLoginStatus: updateLoginStatus2,
+ updateToken,
} = useBackendContext();
- function updateLoginStatus(url: string, token: string | undefined) {
- console.log("updateing", url, token)
- updateLoginStatus2(url, token)
+ function updateLoginStatus(token: LoginToken | undefined) {
+ updateToken(token)
setUnauthorized(false)
}
@@ -59,15 +59,15 @@ export function ApplicationReadyRoutes(): VNode {
route("/");
};
const [showSettings, setShowSettings] = useState(false)
- // useEffect(() => {
- // setUnauthorized(FF)
- // }, [FF])
- const unauthorizedAdmin = !result.loading && !result.ok && result.type === ErrorType.CLIENT && result.status === HttpStatusCode.Unauthorized
+ const unauthorizedAdmin = !result.loading
+ && !result.ok
+ && result.type === ErrorType.CLIENT
+ && result.status === HttpStatusCode.Unauthorized;
if (showSettings) {
return <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
- <Settings />
+ <Settings onClose={() => setShowSettings(false)} />
</Fragment>
}
@@ -100,7 +100,7 @@ export function ApplicationReadyRoutes(): VNode {
type: "ERROR",
}}
/>
- <LoginPage onConfirm={updateLoginStatus} />
+ <ConnectionPage onConfirm={changeBackend} />
</Fragment>
);
}
@@ -108,14 +108,13 @@ export function ApplicationReadyRoutes(): VNode {
instanceNameByBackendURL = match[1];
}
- console.log(unauthorized, unauthorizedAdmin)
if (unauthorized || unauthorizedAdmin) {
return <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} />
<NotificationCard
notification={{
message: i18n.str`Access denied`,
- description: i18n.str`Check your token is valid`,
+ description: i18n.str`Check your token is valid 1`,
type: "ERROR",
}}
/>
@@ -132,7 +131,6 @@ export function ApplicationReadyRoutes(): VNode {
admin={admin}
onUnauthorized={() => setUnauthorized(true)}
onLoginPass={() => {
- console.log("ahora si")
setUnauthorized(false)
}}
instanceNameByBackendURL={instanceNameByBackendURL}
diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
index ee8db9a9f..c2a9d3b18 100644
--- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
+++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx
@@ -35,7 +35,7 @@ import { InstanceContextProvider } from "./context/instance.js";
import {
useBackendDefaultToken,
useBackendInstanceToken,
- useLocalStorage,
+ useSimpleLocalStorage,
} from "./hooks/index.js";
import { useInstanceKYCDetails } from "./hooks/instance.js";
import InstanceCreatePage from "./paths/admin/create/index.js";
@@ -71,10 +71,10 @@ import InstanceUpdatePage, {
AdminUpdate as InstanceAdminUpdatePage,
Props as InstanceUpdatePageProps,
} from "./paths/instance/update/index.js";
-import LoginPage from "./paths/login/index.js";
+import { LoginPage } from "./paths/login/index.js";
import NotFoundPage from "./paths/notfound/index.js";
import { Notification } from "./utils/types.js";
-import { MerchantBackend } from "./declaration.js";
+import { LoginToken, MerchantBackend } from "./declaration.js";
import { Settings } from "./paths/settings/index.js";
import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js";
@@ -143,7 +143,7 @@ export function InstanceRoutes({
id,
admin,
path,
- onUnauthorized,
+ // onUnauthorized,
onLoginPass,
setInstanceName,
}: Props): VNode {
@@ -155,7 +155,7 @@ export function InstanceRoutes({
const [globalNotification, setGlobalNotification] =
useState<GlobalNotifState>(undefined);
- const changeToken = (token?: string) => {
+ const changeToken = (token?: LoginToken) => {
if (admin) {
updateToken(token);
} else {
@@ -201,14 +201,17 @@ export function InstanceRoutes({
// const LoginPageAccessDeniend = onUnauthorized
const LoginPageAccessDenied = () => {
- onUnauthorized()
- return <NotificationCard
- notification={{
- message: i18n.str`Access denied`,
- description: i18n.str`Redirecting to login page.`,
- type: "ERROR",
- }}
- />
+ return <Fragment>
+ <NotificationCard
+ notification={{
+ message: i18n.str`Access denied`,
+ description: i18n.str`Redirecting to login page.`,
+ type: "ERROR",
+ }}
+ />
+ <LoginPage onConfirm={changeToken} />
+ </Fragment>
+
}
function IfAdminCreateDefaultOr<T>(Next: FunctionComponent<any>) {
@@ -687,9 +690,7 @@ function AdminInstanceUpdatePage({
...rest
}: { id: string } & InstanceUpdatePageProps): VNode {
const [token, changeToken] = useBackendInstanceToken(id);
- const { updateLoginStatus: changeBackend } = useBackendContext();
- const updateLoginStatus = (url: string, token?: string): void => {
- changeBackend(url);
+ const updateLoginStatus = (token?: LoginToken): void => {
changeToken(token);
};
const value = useMemo(
@@ -752,7 +753,7 @@ function KycBanner(): VNode {
const { i18n } = useTranslationContext();
const [settings] = useSettings();
const today = format(new Date(), dateFormatForSettings(settings));
- const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide");
+ const [lastHide, setLastHide] = useSimpleLocalStorage("kyc-last-hide");
const hasBeenHidden = today === lastHide;
const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
if (hasBeenHidden || !needsToBeShown) return <Fragment />;
diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx
deleted file mode 100644
index 4fa440fc7..000000000
--- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2023 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- *
- * @author Sebastian Javier Marchano (sebasjm)
- */
-
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { ComponentChildren, h, VNode } from "preact";
-import { useState } from "preact/hooks";
-import { useBackendContext } from "../../context/backend.js";
-import { useInstanceContext } from "../../context/instance.js";
-import { useCredentialsChecker } from "../../hooks/backend.js";
-import { Notification } from "../../utils/types.js";
-
-interface Props {
- withMessage?: Notification;
- onConfirm: (backend: string, token?: string) => void;
-}
-
-function getTokenValuePart(t: string): string {
- if (!t) return t;
- const match = /secret-token:(.*)/.exec(t);
- if (!match || !match[1]) return "";
- return match[1];
-}
-
-function normalizeToken(r: string): string {
- return `secret-token:${r}`;
-}
-
-function cleanUp(s: string): string {
- let result = s;
- if (result.indexOf("webui/") !== -1) {
- result = result.substring(0, result.indexOf("webui/"));
- }
- return result;
-}
-
-export function LoginModal({ onConfirm, withMessage }: Props): VNode {
- const { url: backendUrl, token: baseToken } = useBackendContext();
- const { admin, token: instanceToken, id } = useInstanceContext();
- const testLogin = useCredentialsChecker();
- const currentToken = getTokenValuePart(
- (!admin ? baseToken : instanceToken) ?? "",
- );
- const [token, setToken] = useState(currentToken);
-
- const [url, setURL] = useState(cleanUp(backendUrl));
- const { i18n } = useTranslationContext();
-
- if (admin && id !== "default") {
- //admin trying to access another instance
- return (<div class="columns is-centered" style={{ margin: "auto" }}>
- <div class="column is-two-thirds ">
- <div class="modal-card" style={{ width: "100%", margin: 0 }}>
- <header
- class="modal-card-head"
- style={{ border: "1px solid", borderBottom: 0 }}
- >
- <p class="modal-card-title">{i18n.str`Login required`}</p>
- </header>
- <section
- class="modal-card-body"
- style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
- >
- <p>
- <i18n.Translate>Need the access token for the instance.</i18n.Translate>
- </p>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Access Token</i18n.Translate>
- </label>
- </div>
- <div class="field-body">
- <div class="field">
- <p class="control is-expanded">
- <input
- class="input"
- type="password"
- placeholder={"current access token"}
- name="token"
- onKeyPress={(e) =>
- e.keyCode === 13
- ? onConfirm(url, normalizeToken(token))
- : null
- }
- value={token}
- onInput={(e): void => setToken(e?.currentTarget.value)}
- />
- </p>
- </div>
- </div>
- </div>
- </section>
- <footer
- class="modal-card-foot "
- style={{
- justifyContent: "flex-end",
- border: "1px solid",
- borderTop: 0,
- }}
- >
- <AsyncButton
- onClick={async () => {
- const secretToken = normalizeToken(token);
- const { valid, cause } = await testLogin(`${url}/instances/${id}`, secretToken);
- if (valid) {
- onConfirm(url, secretToken);
- } else {
- onConfirm(url);
- }
- }}
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </AsyncButton>
- </footer>
- </div>
- </div>
- </div>)
- }
-
- return (
- <div class="columns is-centered" style={{ margin: "auto" }}>
- <div class="column is-two-thirds ">
- <div class="modal-card" style={{ width: "100%", margin: 0 }}>
- <header
- class="modal-card-head"
- style={{ border: "1px solid", borderBottom: 0 }}
- >
- <p class="modal-card-title">{i18n.str`Login required`}</p>
- </header>
- <section
- class="modal-card-body"
- style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
- >
- <i18n.Translate>Please enter your access token.</i18n.Translate>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">URL</label>
- </div>
- <div class="field-body">
- <div class="field">
- <p class="control is-expanded">
- <input
- class="input"
- type="text"
- placeholder="set new url"
- name="id"
- value={url}
- onKeyPress={(e) =>
- e.keyCode === 13
- ? onConfirm(url, normalizeToken(token))
- : null
- }
- onInput={(e): void => setURL(e?.currentTarget.value)}
- />
- </p>
- </div>
- </div>
- </div>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Access Token</i18n.Translate>
- </label>
- </div>
- <div class="field-body">
- <div class="field">
- <p class="control is-expanded">
- <input
- class="input"
- type="password"
- placeholder={"current access token"}
- name="token"
- onKeyPress={(e) =>
- e.keyCode === 13
- ? onConfirm(url, normalizeToken(token))
- : null
- }
- value={token}
- onInput={(e): void => setToken(e?.currentTarget.value)}
- />
- </p>
- </div>
- </div>
- </div>
- </section>
- <footer
- class="modal-card-foot "
- style={{
- justifyContent: "flex-end",
- border: "1px solid",
- borderTop: 0,
- }}
- >
- <AsyncButton
- onClick={async () => {
- const secretToken = normalizeToken(token);
- const { valid, cause } = await testLogin(url, secretToken);
- if (valid) {
- onConfirm(url, secretToken);
- } else {
- onConfirm(url);
- }
- }}
- >
- <i18n.Translate>Confirm</i18n.Translate>
- </AsyncButton>
- </footer>
- </div>
- </div>
- </div>
- );
-}
-
-function AsyncButton({ onClick, children }: { onClick: () => Promise<void>, children: ComponentChildren }): VNode {
- const [running, setRunning] = useState(false)
- return <button class="button is-info" disabled={running} onClick={() => {
- setRunning(true)
- onClick().then(() => {
- setRunning(false)
- }).catch(() => {
- setRunning(false)
- })
- }}>
- {children}
- </button>
-}
diff --git a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
index b75dc83b3..6f5881fc0 100644
--- a/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
+++ b/packages/merchant-backoffice-ui/src/components/instance/DefaultInstanceFormFields.tsx
@@ -40,13 +40,13 @@ export function DefaultInstanceFormFields({
showId: boolean;
}): VNode {
const { i18n } = useTranslationContext();
- const backend = useBackendContext();
+ const { url: backendURL } = useBackendContext()
return (
<Fragment>
{showId && (
<InputWithAddon<Entity>
name="id"
- addonBefore={`${backend.url}/instances/`}
+ addonBefore={`${backendURL}/instances/`}
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 be2f8dde5..3d5f20c85 100644
--- a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -25,7 +25,6 @@ import { useBackendContext } from "../../context/backend.js";
import { useConfigContext } from "../../context/config.js";
import { useInstanceKYCDetails } from "../../hooks/instance.js";
import { LangSelector } from "./LangSelector.js";
-import { useCredentialsChecker } from "../../hooks/backend.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
@@ -50,7 +49,7 @@ export function Sidebar({
isPasswordOk
}: Props): VNode {
const config = useConfigContext();
- const backend = useBackendContext();
+ const { url: backendURL } = useBackendContext()
const { i18n } = useTranslationContext();
const kycStatus = useInstanceKYCDetails();
const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
@@ -230,7 +229,7 @@ export function Sidebar({
<i class="mdi mdi-web" />
</span>
<span class="menu-item-label">
- {new URL(backend.url).hostname}
+ {new URL(backendURL).hostname}
</span>
</div>
</li>
diff --git a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
index 726a94f5e..8bebbd298 100644
--- a/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
+++ b/packages/merchant-backoffice-ui/src/components/product/ProductForm.tsx
@@ -114,7 +114,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
onSubscribe(hasErrors ? undefined : submit);
}, [submit, hasErrors]);
- const backend = useBackendContext();
+ const { url: backendURL } = useBackendContext()
const { i18n } = useTranslationContext();
return (
@@ -128,7 +128,7 @@ export function ProductForm({ onSubscribe, initial, alreadyExist }: Props) {
{alreadyExist ? undefined : (
<InputWithAddon<Entity>
name="product_id"
- addonBefore={`${backend.url}/product/`}
+ addonBefore={`${backendURL}/product/`}
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
index cb0010c4b..b042d5a25 100644
--- a/packages/merchant-backoffice-ui/src/context/backend.test.ts
+++ b/packages/merchant-backoffice-ui/src/context/backend.test.ts
@@ -21,7 +21,7 @@
import * as tests from "@gnu-taler/web-util/testing";
import { ComponentChildren, h, VNode } from "preact";
-import { MerchantBackend } from "../declaration.js";
+import { AccessToken, MerchantBackend } from "../declaration.js";
import {
useAdminAPI,
useInstanceAPI,
@@ -64,7 +64,7 @@ describe("backend context api ", () => {
} as MerchantBackend.Instances.QueryInstancesResponse,
});
- management.setNewToken("another_token");
+ management.setNewToken("another_token" as AccessToken);
},
({ instance, management, admin }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
@@ -113,7 +113,7 @@ describe("backend context api ", () => {
name: "instance_name",
} as MerchantBackend.Instances.QueryInstancesResponse,
});
- instance.setNewToken("another_token");
+ instance.setNewToken("another_token" as AccessToken);
},
({ instance, management, admin }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts
index 43e9e4d27..056f9a192 100644
--- a/packages/merchant-backoffice-ui/src/context/backend.ts
+++ b/packages/merchant-backoffice-ui/src/context/backend.ts
@@ -20,90 +20,46 @@
*/
import { createContext, h, VNode } from "preact";
-import { useCallback, useContext, useState } from "preact/hooks";
+import { useContext } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
import { useBackendDefaultToken, useBackendURL } from "../hooks/index.js";
interface BackendContextType {
- url: string;
- token?: string;
- triedToLog: boolean;
- resetBackend: () => void;
- // clearAllTokens: () => void;
- // addTokenCleaner: (c: () => void) => void;
- updateLoginStatus: (url: string, token?: string) => void;
- updateToken: (token?: string) => void;
+ url: string,
+ token?: LoginToken;
+ updateToken: (token: LoginToken | undefined) => void;
+ changeBackend: (url: string) => void;
}
const BackendContext = createContext<BackendContextType>({
url: "",
token: undefined,
- triedToLog: false,
- resetBackend: () => null,
- // clearAllTokens: () => null,
- // addTokenCleaner: () => null,
- updateLoginStatus: () => null,
updateToken: () => null,
+ changeBackend: () => null,
});
function useBackendContextState(
defaultUrl?: string,
- initialToken?: string,
): BackendContextType {
- const [url, triedToLog, changeBackend, resetBackend] =
- useBackendURL(defaultUrl);
- const [token, _updateToken] = useBackendDefaultToken(initialToken);
- const updateToken = (t?: string) => {
- _updateToken(t);
- };
-
- // const tokenCleaner = useCallback(() => {
- // updateToken(undefined);
- // }, []);
- // const [cleaners, setCleaners] = useState([tokenCleaner]);
- // const addTokenCleaner = (c: () => void) => setCleaners((cs) => [...cs, c]);
- // const addTokenCleanerMemo = useCallback(
- // (c: () => void) => {
- // addTokenCleaner(c);
- // },
- // [tokenCleaner],
- // );
-
- // const clearAllTokens = () => {
- // cleaners.forEach((c) => c());
- // for (let i = 0; i < localStorage.length; i++) {
- // const k = localStorage.key(i);
- // if (k && /^backend-token/.test(k)) localStorage.removeItem(k);
- // }
- // resetBackend();
- // };
-
- const updateLoginStatus = (url: string, token?: string) => {
- changeBackend(url);
- updateToken(token);
- };
+ const [url, changeBackend] = useBackendURL(defaultUrl);
+ const [token, updateToken] = useBackendDefaultToken();
return {
url,
token,
- triedToLog,
- updateLoginStatus,
- resetBackend,
- // clearAllTokens,
updateToken,
- // addTokenCleaner: addTokenCleanerMemo,
+ changeBackend
};
}
export const BackendContextProvider = ({
children,
defaultUrl,
- initialToken,
}: {
children: any;
defaultUrl?: string;
- initialToken?: string;
}): VNode => {
- const value = useBackendContextState(defaultUrl, initialToken);
+ const value = useBackendContextState(defaultUrl);
return h(BackendContext.Provider, { value, children });
};
diff --git a/packages/merchant-backoffice-ui/src/context/instance.ts b/packages/merchant-backoffice-ui/src/context/instance.ts
index 9a25fe80c..3c6cc2b63 100644
--- a/packages/merchant-backoffice-ui/src/context/instance.ts
+++ b/packages/merchant-backoffice-ui/src/context/instance.ts
@@ -21,12 +21,13 @@
import { createContext } from "preact";
import { useContext } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
interface Type {
id: string;
- token?: string;
+ token?: LoginToken;
admin?: boolean;
- changeToken: (t?: string) => void;
+ changeToken: (t?: LoginToken) => void;
}
const Context = createContext<Type>({} as any);
diff --git a/packages/merchant-backoffice-ui/src/declaration.d.ts b/packages/merchant-backoffice-ui/src/declaration.d.ts
index 5ca9c1e09..c3e6ea3da 100644
--- a/packages/merchant-backoffice-ui/src/declaration.d.ts
+++ b/packages/merchant-backoffice-ui/src/declaration.d.ts
@@ -107,6 +107,16 @@ interface RegexAccountRestriction {
// human hints.
human_hint_i18n?: { [lang_tag: string]: string };
}
+interface LoginToken {
+ token: string,
+ expiration: Timestamp,
+}
+// 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 {
@@ -491,6 +501,35 @@ export namespace MerchantBackend {
};
}
// DELETE /private/instances/$INSTANCE
+ interface LoginTokenRequest {
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ duration?: RelativeTime;
+
+ // Can this token be refreshed?
+ // Defaults to false.
+ refreshable?: boolean;
+ }
+ interface LoginTokenSuccessResponse {
+ // The login token that can be used to access resources
+ // that are in scope for some time. Must be prefixed
+ // with "Bearer " when used in the "Authorization" HTTP header.
+ // Will already begin with the RFC 8959 prefix.
+ token: string;
+
+ // Scope of the token (which kinds of operations it will allow)
+ scope: "readonly" | "write";
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ expiration: Timestamp;
+
+ // Can this token be refreshed?
+ refreshable: boolean;
+ }
}
namespace KYC {
diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts
index ecd34df6d..fe4155788 100644
--- a/packages/merchant-backoffice-ui/src/hooks/backend.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts
@@ -19,19 +19,21 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { useSWRConfig } from "swr";
-import { MerchantBackend } from "../declaration.js";
-import { useBackendContext } from "../context/backend.js";
-import { useCallback, useEffect, useState } from "preact/hooks";
-import { useInstanceContext } from "../context/instance.js";
+import { AbsoluteTime, HttpStatusCode } from "@gnu-taler/taler-util";
import {
ErrorType,
+ HttpError,
HttpResponse,
HttpResponseOk,
RequestError,
RequestOptions,
+ useApiContext,
} from "@gnu-taler/web-util/browser";
-import { useApiContext } from "@gnu-taler/web-util/browser";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useSWRConfig } from "swr";
+import { useBackendContext } from "../context/backend.js";
+import { useInstanceContext } from "../context/instance.js";
+import { AccessToken, LoginToken, MerchantBackend, Timestamp } from "../declaration.js";
export function useMatchMutate(): (
@@ -85,6 +87,9 @@ export function useBackendInstancesTestForAdmin(): HttpResponse<
return result;
}
+const CHECK_CONFIG_INTERVAL_OK = 5 * 60 * 1000;
+const CHECK_CONFIG_INTERVAL_FAIL = 2 * 1000;
+
export function useBackendConfig(): HttpResponse<
MerchantBackend.VersionResponse,
RequestError<MerchantBackend.ErrorDetail>
@@ -92,18 +97,33 @@ export function useBackendConfig(): HttpResponse<
const { request } = useBackendBaseRequest();
type Type = MerchantBackend.VersionResponse;
-
- const [result, setResult] = useState<
- HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>
- >({ loading: true });
+ type State = { data: HttpResponse<Type, RequestError<MerchantBackend.ErrorDetail>>, timer: number }
+ const [result, setResult] = useState<State>({ data: { loading: true }, timer: 0 });
useEffect(() => {
- request<Type>(`/config`)
- .then((data) => setResult(data))
- .catch((error) => setResult(error));
+ if (result.timer) {
+ clearTimeout(result.timer)
+ }
+ function tryConfig(): void {
+ request<Type>(`/config`)
+ .then((data) => {
+ const timer: any = setTimeout(() => {
+ tryConfig()
+ }, CHECK_CONFIG_INTERVAL_OK)
+ setResult({ data, timer })
+ })
+ .catch((error) => {
+ const timer: any = setTimeout(() => {
+ tryConfig()
+ }, CHECK_CONFIG_INTERVAL_FAIL)
+ const data = error.cause
+ setResult({ data, timer })
+ });
+ }
+ tryConfig()
}, [request]);
- return result;
+ return result.data;
}
interface useBackendInstanceRequestType {
@@ -149,32 +169,86 @@ interface useBackendBaseRequestType {
}
type YesOrNo = "yes" | "no";
+type LoginResult = {
+ valid: true;
+ token: string;
+ expiration: Timestamp;
+} | {
+ valid: false;
+ cause: HttpError<{}>;
+}
export function useCredentialsChecker() {
const { request } = useApiContext();
//check against instance details endpoint
//while merchant backend doesn't have a login endpoint
- async function testLogin(
- instance: string,
- token: string,
- ): Promise<{
- valid: boolean;
- cause?: ErrorType;
- }> {
+ async function requestNewLoginToken(
+ baseUrl: string,
+ token: AccessToken,
+ ): Promise<LoginResult> {
+ const data: MerchantBackend.Instances.LoginTokenRequest = {
+ scope: "write",
+ duration: {
+ d_us: "forever"
+ },
+ refreshable: true,
+ }
try {
- const response = await request(instance, `/private/`, {
+ const response = await request<MerchantBackend.Instances.LoginTokenSuccessResponse>(baseUrl, `/private/token`, {
+ method: "POST",
token,
+ data
});
- return { valid: true };
+ return { valid: true, token: response.data.token, expiration: response.data.expiration };
} catch (error) {
if (error instanceof RequestError) {
- return { valid: false, cause: error.cause.type };
+ return { valid: false, cause: error.cause };
}
- return { valid: false, cause: ErrorType.UNEXPECTED };
+ return {
+ valid: false, cause: {
+ type: ErrorType.UNEXPECTED,
+ loading: false,
+ info: {
+ hasToken: true,
+ status: 0,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ exception: error,
+ message: (error instanceof Error ? error.message : "unpexepected error")
+ }
+ };
}
};
- return testLogin
+
+ async function refreshLoginToken(
+ baseUrl: string,
+ token: LoginToken
+ ): Promise<LoginResult> {
+
+ if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(token.expiration))) {
+ return {
+ valid: false, cause: {
+ type: ErrorType.CLIENT,
+ status: HttpStatusCode.Unauthorized,
+ message: "login token expired, login again.",
+ info: {
+ hasToken: true,
+ status: 401,
+ options: {},
+ url: `/private/token`,
+ payload: {}
+ },
+ payload: {}
+ },
+ }
+ }
+
+ return requestNewLoginToken(baseUrl, token.token as AccessToken)
+ }
+ return { requestNewLoginToken, refreshLoginToken }
}
/**
@@ -183,15 +257,20 @@ export function useCredentialsChecker() {
* @returns request handler to
*/
export function useBackendBaseRequest(): useBackendBaseRequestType {
- const { url: backend, token } = useBackendContext();
+ const { url: backend, token: loginToken } = useBackendContext();
const { request: requestHandler } = useApiContext();
+ const token = loginToken?.token;
const request = useCallback(
function requestImpl<T>(
endpoint: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
- return requestHandler<T>(backend, endpoint, { token, ...options });
+ return requestHandler<T>(backend, endpoint, { token, ...options }).then(res => {
+ return res
+ }).catch(err => {
+ throw err
+ });
},
[backend, token],
);
@@ -204,10 +283,12 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const { token: instanceToken, id, admin } = useInstanceContext();
const { request: requestHandler } = useApiContext();
- const { baseUrl, token } = !admin
+ const { baseUrl, token: loginToken } = !admin
? { baseUrl: rootBackendUrl, token: rootToken }
: { baseUrl: `${rootBackendUrl}/instances/${id}`, token: instanceToken };
+ const token = loginToken?.token;
+
const request = useCallback(
function requestImpl<T>(
endpoint: string,
diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts
index 79b22304a..ee696779f 100644
--- a/packages/merchant-backoffice-ui/src/hooks/index.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/index.ts
@@ -19,9 +19,11 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { StateUpdater, useCallback, useEffect, useState } from "preact/hooks";
+import { buildCodecForObject, codecForMap, codecForString, codecForTimestamp } from "@gnu-taler/taler-util";
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { StateUpdater, useEffect, useState } from "preact/hooks";
+import { LoginToken } from "../declaration.js";
import { ValueOrFunction } from "../utils/types.js";
-import { useMemoryStorage } from "@gnu-taler/web-util/browser";
import { useMatchMutate } from "./backend.js";
const calculateRootPath = () => {
@@ -32,53 +34,55 @@ const calculateRootPath = () => {
return rootPath;
};
+const loginTokenCodec = buildCodecForObject<LoginToken>()
+ .property("token", codecForString())
+ .property("expiration", codecForTimestamp)
+ .build("loginToken")
+const TOKENS_KEY = buildStorageKey("backend-token", codecForMap(loginTokenCodec));
+
+
export function useBackendURL(
url?: string,
-): [string, boolean, StateUpdater<string>, () => void] {
- const [value, setter] = useNotNullLocalStorage(
+): [string, StateUpdater<string>] {
+ const [value, setter] = useSimpleLocalStorage(
"backend-url",
url || calculateRootPath(),
);
- const [triedToLog, setTriedToLog] = useLocalStorage("tried-login");
const checkedSetter = (v: ValueOrFunction<string>) => {
- setTriedToLog("yes");
- return setter((p) => (v instanceof Function ? v(p) : v).replace(/\/$/, ""));
+ return setter((p) => (v instanceof Function ? v(p ?? "") : v).replace(/\/$/, ""));
};
- const resetBackend = () => {
- setTriedToLog(undefined);
- };
- return [value, !!triedToLog, checkedSetter, resetBackend];
+ return [value!, checkedSetter];
}
export function useBackendDefaultToken(
- initialValue?: string,
-): [string | undefined, ((d: string | undefined) => void)] {
- // uncomment for testing
- initialValue = "secret-token:secret" as string | undefined
- const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token`, initialValue)
+): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
+ const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
+
+ const tokenOfDefaultInstance = tokenMap["default"]
const clearCache = useMatchMutate()
useEffect(() => {
clearCache()
- }, [token])
+ }, [tokenOfDefaultInstance])
function updateToken(
- value: (string | undefined)
+ value: (LoginToken | undefined)
): void {
if (value === undefined) {
reset()
} else {
- setToken(value)
+ const res = { ...tokenMap, "default": value }
+ setToken(res)
}
}
- return [token, updateToken];
+ return [tokenMap["default"], updateToken];
}
export function useBackendInstanceToken(
id: string,
-): [string | undefined, ((d: string | undefined) => void)] {
- const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`)
+): [LoginToken | undefined, ((d: LoginToken | undefined) => void)] {
+ const { update: setToken, value: tokenMap, reset } = useLocalStorage(TOKENS_KEY, {})
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token
@@ -86,16 +90,17 @@ export function useBackendInstanceToken(
return [defaultToken, defaultSetToken];
}
function updateToken(
- value: (string | undefined)
+ value: (LoginToken | undefined)
): void {
if (value === undefined) {
reset()
} else {
- setToken(value)
+ const res = { ...tokenMap, [id]: value }
+ setToken(res)
}
}
- return [token, updateToken];
+ return [tokenMap[id], updateToken];
}
export function useLang(initial?: string): [string, StateUpdater<string>] {
@@ -104,10 +109,10 @@ export function useLang(initial?: string): [string, StateUpdater<string>] {
? navigator.language || (navigator as any).userLanguage
: undefined;
const defaultLang = (browserLang || initial || "en").substring(0, 2);
- return useNotNullLocalStorage("lang-preference", defaultLang);
+ return useSimpleLocalStorage("lang-preference", defaultLang) as [string, StateUpdater<string>];
}
-export function useLocalStorage(
+export function useSimpleLocalStorage(
key: string,
initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] {
@@ -137,28 +142,3 @@ export function useLocalStorage(
return [storedValue, setValue];
}
-
-export function useNotNullLocalStorage(
- key: string,
- initialValue: string,
-): [string, StateUpdater<string>] {
- const [storedValue, setStoredValue] = useState<string>((): string => {
- return typeof window !== "undefined"
- ? window.localStorage.getItem(key) || initialValue
- : initialValue;
- });
-
- const setValue = (value: string | ((val: string) => string)) => {
- const valueToStore = value instanceof Function ? value(storedValue) : value;
- setStoredValue(valueToStore);
- if (typeof window !== "undefined") {
- if (!valueToStore) {
- window.localStorage.removeItem(key);
- } else {
- window.localStorage.setItem(key, valueToStore);
- }
- }
- };
-
- return [storedValue, setValue];
-}
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts
index d15b3f6d7..a7b8d047c 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 { MerchantBackend } from "../declaration.js";
+import { AccessToken, MerchantBackend } from "../declaration.js";
import {
useAdminAPI,
useBackendInstances,
@@ -158,7 +158,7 @@ describe("instance api interaction with details", () => {
},
} as MerchantBackend.Instances.QueryInstancesResponse,
});
- api.setNewToken("secret");
+ api.setNewToken("secret" as AccessToken);
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
index 32ed30c6f..50f9487a3 100644
--- a/packages/merchant-backoffice-ui/src/hooks/instance.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -19,10 +19,11 @@ import {
RequestError,
} from "@gnu-taler/web-util/browser";
import { useBackendContext } from "../context/backend.js";
-import { MerchantBackend } from "../declaration.js";
+import { AccessToken, MerchantBackend } from "../declaration.js";
import {
useBackendBaseRequest,
useBackendInstanceRequest,
+ useCredentialsChecker,
useMatchMutate,
} from "./backend.js";
@@ -36,7 +37,7 @@ interface InstanceAPI {
) => Promise<void>;
deleteInstance: () => Promise<void>;
clearToken: () => Promise<void>;
- setNewToken: (token: string) => Promise<void>;
+ setNewToken: (token: AccessToken) => Promise<void>;
}
export function useAdminAPI(): AdminAPI {
@@ -86,8 +87,10 @@ export interface AdminAPI {
export function useManagementAPI(instanceId: string): InstanceAPI {
const mutateAll = useMatchMutate();
+ const { url: backendURL } = useBackendContext()
const { updateToken } = useBackendContext();
const { request } = useBackendBaseRequest();
+ const { requestNewLoginToken } = useCredentialsChecker()
const updateInstance = async (
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
@@ -117,13 +120,20 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
mutateAll(/\/management\/instances/);
};
- const setNewToken = async (newToken: string): Promise<void> => {
+ const setNewToken = async (newToken: AccessToken): Promise<void> => {
await request(`/management/instances/${instanceId}/auth`, {
method: "POST",
data: { method: "token", token: newToken },
});
- updateToken(newToken);
+ const resp = await requestNewLoginToken(backendURL, newToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+
mutateAll(/\/management\/instances/);
};
@@ -132,12 +142,13 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
export function useInstanceAPI(): InstanceAPI {
const { mutate } = useSWRConfig();
+ const { url: backendURL, updateToken } = useBackendContext()
+
const {
- url: baseUrl,
token: adminToken,
- updateLoginStatus,
} = useBackendContext();
const { request } = useBackendInstanceRequest();
+ const { requestNewLoginToken } = useCredentialsChecker()
const updateInstance = async (
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
@@ -147,7 +158,7 @@ export function useInstanceAPI(): InstanceAPI {
data: instance,
});
- if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
+ if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
mutate([`/private/`], null);
};
@@ -157,7 +168,7 @@ export function useInstanceAPI(): InstanceAPI {
// token: adminToken,
});
- if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
+ if (adminToken) mutate(["/private/instances", adminToken, backendURL], null);
mutate([`/private/`], null);
};
@@ -170,13 +181,20 @@ export function useInstanceAPI(): InstanceAPI {
mutate([`/private/`], null);
};
- const setNewToken = async (newToken: string): Promise<void> => {
+ const setNewToken = async (newToken: AccessToken): Promise<void> => {
await request(`/private/auth`, {
method: "POST",
data: { method: "token", token: newToken },
});
- updateLoginStatus(baseUrl, newToken);
+ const resp = await requestNewLoginToken(backendURL, newToken)
+ if (resp.valid) {
+ const { token, expiration } = resp
+ updateToken({ token, expiration });
+ } else {
+ updateToken(undefined)
+ }
+
mutate([`/private/`], null);
};
diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
index ebbc6f64a..847d512b0 100644
--- a/packages/merchant-backoffice-ui/src/hooks/testing.tsx
+++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx
@@ -90,10 +90,7 @@ export class ApiMockEnvironment extends MockEnvironment {
const SC: any = SWRConfig;
return (
- <BackendContextProvider
- defaultUrl="http://backend"
- initialToken={undefined}
- >
+ <BackendContextProvider defaultUrl="http://backend">
<InstanceContextProvider
value={{
token: undefined,
diff --git a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
index 7dee9f896..8c1ebd9f6 100644
--- a/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
+++ b/packages/merchant-backoffice-ui/src/hooks/useSettings.ts
@@ -24,15 +24,6 @@ import {
codecForString,
} from "@gnu-taler/taler-util";
-function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
- if (str === undefined) return undefined;
- try {
- return JSON.parse(str);
- } catch {
- return undefined;
- }
-}
-
export interface Settings {
advanceOrderMode: boolean;
dateFormat: "ymd" | "dmy" | "mdy";
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 e42adc2ff..1cfbec29b 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
@@ -22,7 +22,7 @@
import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format, formatDistance } from "date-fns";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { FormProvider } from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
@@ -35,10 +35,10 @@ import { TextField } from "../../../../components/form/TextField.js";
import { ProductList } from "../../../../components/product/ProductList.js";
import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend } from "../../../../declaration.js";
+import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
import { mergeRefunds } from "../../../../utils/amount.js";
import { RefundModal } from "../list/Table.js";
import { Event, Timeline } from "./Timeline.js";
-import { dateFormatForSettings, datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse;
type CT = MerchantBackend.ContractTerms;
@@ -416,9 +416,9 @@ function PaidPage({
})
const [value, valueHandler] = useState<Partial<Paid>>(order);
- const { url } = useBackendContext();
+ const { url: backendURL } = useBackendContext()
const refundurl = stringifyRefundUri({
- merchantBaseUrl: url,
+ merchantBaseUrl: backendURL,
orderId: order.contract_terms.order_id
})
const refundable =
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
index 57a051ed7..780068a91 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/RewardInfo.tsx
@@ -13,12 +13,12 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { stringifyRewardUri } from "@gnu-taler/taler-util";
import { format } from "date-fns";
import { Fragment, h, VNode } from "preact";
import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend } from "../../../../declaration.js";
import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js";
-import { stringifyRewardUri } from "@gnu-taler/taler-util";
type Entity = MerchantBackend.Rewards.RewardDetails;
@@ -29,9 +29,9 @@ interface Props {
}
export function RewardInfo({ id: merchantRewardId, amount, entity }: Props): VNode {
- const { url: merchantBaseUrl } = useBackendContext();
+ const { url: backendURL } = useBackendContext()
const [settings] = useSettings();
- const rewardURL = stringifyRewardUri({ merchantBaseUrl, merchantRewardId })
+ const rewardURL = stringifyRewardUri({ merchantBaseUrl: backendURL, merchantRewardId })
return (
<Fragment>
<div class="field is-horizontal">
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 8629d8dee..78ea07477 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
@@ -35,16 +35,12 @@ 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 { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
import { useBackendContext } from "../../../../context/backend.js";
-import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js";
-import {
- isBase32RFC3548Charset
-} from "../../../../utils/crypto.js";
-import { undefinedIfEmpty } from "../../../../utils/table.js";
-import { InputSearchOnList } from "../../../../components/form/InputSearchOnList.js";
import { useInstanceOtpDevices } from "../../../../hooks/otp.js";
+import { undefinedIfEmpty } from "../../../../utils/table.js";
type Entity = MerchantBackend.Template.TemplateAddDetails;
@@ -55,7 +51,7 @@ interface Props {
export function CreatePage({ onCreate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const backend = useBackendContext();
+ const { url: backendURL } = useBackendContext()
const devices = useInstanceOtpDevices()
const [state, setState] = useState<Partial<Entity>>({
@@ -128,7 +124,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
>
<InputWithAddon<Entity>
name="template_id"
- help={`${backend.url}/templates/${state.template_id ?? ""}`}
+ help={`${backendURL}/templates/${state.template_id ?? ""}`}
label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`}
/>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
index c65cf6a19..5140aae3a 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/qr/QrPage.tsx
@@ -19,8 +19,9 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { h, VNode } from "preact";
+import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { VNode, h } from "preact";
import { useState } from "preact/hooks";
import { QR } from "../../../../components/exception/QR.js";
import {
@@ -29,14 +30,10 @@ import {
} from "../../../../components/form/FormProvider.js";
import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
-import { ConfirmModal } from "../../../../components/modal/index.js";
import { useBackendContext } from "../../../../context/backend.js";
import { useConfigContext } from "../../../../context/config.js";
import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js";
-import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
-import { useOtpDeviceDetails } from "../../../../hooks/otp.js";
-import { Loading } from "../../../../components/exception/loading.js";
type Entity = MerchantBackend.Template.UsingTemplateDetails;
@@ -48,7 +45,7 @@ interface Props {
export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const { url: backendUrl } = useBackendContext();
+ const { url: backendURL } = useBackendContext()
const { id: instanceId } = useInstanceContext();
const config = useConfigContext();
@@ -75,7 +72,7 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
templateParams.summary = state.summary ?? ""
}
- const merchantBaseUrl = new URL(backendUrl).href;
+ const merchantBaseUrl = new URL(backendURL).href;
const payTemplateUri = stringifyPayTemplateUri({
merchantBaseUrl,
@@ -84,7 +81,7 @@ export function QrPage({ contract, id: templateId, onBack }: Props): VNode {
})
const issuer = encodeURIComponent(
- `${new URL(backendUrl).host}/${instanceId}`,
+ `${new URL(backendURL).host}/${instanceId}`,
);
return (
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 30d47385c..82b74e1fa 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
@@ -24,7 +24,7 @@ import {
MerchantTemplateContractDetails,
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { AsyncButton } from "../../../../components/exception/AsyncButton.js";
import {
@@ -35,17 +35,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 { InputSelector } from "../../../../components/form/InputSelector.js";
import { InputWithAddon } from "../../../../components/form/InputWithAddon.js";
import { useBackendContext } from "../../../../context/backend.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
-import {
- isBase32RFC3548Charset,
- randomBase32Key,
-} from "../../../../utils/crypto.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
-import { QR } from "../../../../components/exception/QR.js";
-import { useInstanceContext } from "../../../../context/instance.js";
type Entity = MerchantBackend.Template.TemplatePatchDetails & WithId;
@@ -55,12 +48,9 @@ interface Props {
template: Entity;
}
-const algorithms = [0, 1, 2];
-const algorithmsNames = ["off", "30s 8d TOTP-SHA1", "30s 8d eTOTP-SHA1"];
-
export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
- const backend = useBackendContext();
+ const { url: backendURL } = useBackendContext()
const [state, setState] = useState<Partial<Entity>>(template);
@@ -115,7 +105,7 @@ export function UpdatePage({ template, onUpdate, onBack }: Props): VNode {
<div class="level-left">
<div class="level-item">
<span class="is-size-4">
- {backend.url}/templates/{template.id}
+ {backendURL}/templates/{template.id}
</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 984880752..4b0db200a 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx
@@ -26,12 +26,13 @@ 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";
interface Props {
instanceId: string;
currentToken: string | undefined;
onClearToken: () => void;
- onNewToken: (s: string) => void;
+ onNewToken: (s: AccessToken) => void;
onBack?: () => void;
}
@@ -71,7 +72,8 @@ export function DetailPage({ instanceId, currentToken: oldToken, onBack, onNewTo
async function submitForm() {
if (hasErrors) return;
- onNewToken(form.new_token as any)
+ const nt = `secret-token:${form.new_token}` as AccessToken;
+ onNewToken(nt)
}
return (
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 d5910361b..0a49448f8 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx
@@ -17,7 +17,7 @@ import { HttpStatusCode } from "@gnu-taler/taler-util";
import { ErrorType, HttpError, useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { Loading } from "../../../components/exception/loading.js";
-import { MerchantBackend } from "../../../declaration.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
import { DetailPage } from "./DetailPage.js";
import { useInstanceContext } from "../../../context/instance.js";
@@ -49,13 +49,13 @@ export default function Token({
const { token: instanceToken, id, admin } = useInstanceContext();
const currentToken = !admin ? rootToken : instanceToken
- const hasPrefix = currentToken !== undefined && currentToken.startsWith(PREFIX)
+ const hasPrefix = currentToken !== undefined && currentToken.token.startsWith(PREFIX)
return (
<Fragment>
<NotificationCard notification={notif} />
<DetailPage
instanceId={id}
- currentToken={hasPrefix ? currentToken.substring(PREFIX.length) : currentToken}
+ currentToken={hasPrefix ? currentToken.token.substring(PREFIX.length) : currentToken?.token}
onClearToken={async (): Promise<void> => {
try {
await clearToken();
@@ -72,7 +72,7 @@ export default function Token({
}}
onNewToken={async (newToken): Promise<void> => {
try {
- await setNewToken(`secret-token:${newToken}`);
+ await setNewToken(newToken);
onChange();
} catch (error) {
if (error instanceof Error) {
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 4a8162611..6c5e7a514 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx
@@ -13,18 +13,19 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { HttpStatusCode } from "@gnu-taler/taler-util";
import {
ErrorType,
HttpError,
HttpResponse,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
-import { Fragment, h, VNode } from "preact";
+import { Fragment, VNode, h } from "preact";
import { useState } from "preact/hooks";
import { Loading } from "../../../components/exception/loading.js";
import { NotificationCard } from "../../../components/menu/index.js";
import { useInstanceContext } from "../../../context/instance.js";
-import { MerchantBackend } from "../../../declaration.js";
+import { AccessToken, MerchantBackend } from "../../../declaration.js";
import {
useInstanceAPI,
useInstanceDetails,
@@ -33,7 +34,6 @@ import {
} from "../../../hooks/instance.js";
import { Notification } from "../../../utils/types.js";
import { UpdatePage } from "./UpdatePage.js";
-import { HttpStatusCode } from "@gnu-taler/taler-util";
export interface Props {
onBack: () => void;
@@ -73,10 +73,9 @@ function CommonUpdate(
MerchantBackend.ErrorDetail
>,
updateInstance: any,
- clearToken: any,
- setNewToken: any,
+ clearToken: () => Promise<void>,
+ setNewToken: (t: AccessToken) => Promise<void>,
): VNode {
- const { changeToken } = useInstanceContext();
const [notif, setNotif] = useState<Notification | undefined>(undefined);
const { i18n } = useTranslationContext();
@@ -119,11 +118,8 @@ function CommonUpdate(
d: MerchantBackend.Instances.InstanceAuthConfigurationMessage,
): Promise<void> => {
const apiCall =
- d.method === "external" ? clearToken() : setNewToken(d.token!);
- return apiCall
- .then(() => changeToken(d.token))
- .then(onConfirm)
- .catch(onUpdateError);
+ d.method === "external" ? clearToken() : setNewToken(d.token! as AccessToken);
+ return apiCall.then(onConfirm).catch(onUpdateError);
}}
/>
</Fragment>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx
index 3ad3cb3a3..22ae55677 100644
--- a/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx
@@ -18,9 +18,9 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { QR } from "../../../../components/exception/QR.js";
import { CreatedSuccessfully as Template } from "../../../../components/notifications/CreatedSuccessfully.js";
-import { useBackendContext } from "../../../../context/backend.js";
import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js";
+import { useBackendContext } from "../../../../context/backend.js";
type Entity = MerchantBackend.OTP.OtpDeviceAddDetails;
@@ -38,9 +38,9 @@ export function CreatedSuccessfully({
onConfirm,
}: Props): VNode {
const { i18n } = useTranslationContext();
- const backend = useBackendContext();
+ const { url: backendURL } = useBackendContext()
const { id: instanceId } = useInstanceContext();
- const issuer = new URL(backend.url).hostname;
+ 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)}...`;
diff --git a/packages/merchant-backoffice-ui/src/paths/login/index.tsx b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
index caa63c714..9948307e4 100644
--- a/packages/merchant-backoffice-ui/src/paths/login/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/login/index.tsx
@@ -18,12 +18,301 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-import { h, VNode } from "preact";
-import { LoginModal } from "../../components/exception/login.js";
+
+import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import { ComponentChildren, h, VNode } from "preact";
+import { useCallback, useEffect, useState } from "preact/hooks";
+import { useBackendContext } from "../../context/backend.js";
+import { useInstanceContext } from "../../context/instance.js";
+import { AccessToken, LoginToken } from "../../declaration.js";
+import { useCredentialsChecker } from "../../hooks/backend.js";
+import { useBackendURL } from "../../hooks/index.js";
interface Props {
- onConfirm: (url: string, token?: string) => void;
+ onConfirm: (token: LoginToken | undefined) => void;
+}
+
+function getTokenValuePart(t: string): string {
+ if (!t) return t;
+ const match = /secret-token:(.*)/.exec(t);
+ if (!match || !match[1]) return "";
+ return match[1];
}
-export default function LoginPage({ onConfirm }: Props): VNode {
- return <LoginModal onConfirm={onConfirm} />;
+
+function normalizeToken(r: string): AccessToken {
+ return `secret-token:${r}` as AccessToken;
+}
+
+function cleanUp(s: string): string {
+ let result = s;
+ if (result.indexOf("webui/") !== -1) {
+ result = result.substring(0, result.indexOf("webui/"));
+ }
+ return result;
}
+
+export function LoginPage({ onConfirm }: Props): VNode {
+ const { url: backendURL, changeBackend } = useBackendContext();
+ const { admin, id } = useInstanceContext();
+ const { requestNewLoginToken } = useCredentialsChecker();
+ const [token, setToken] = useState("");
+
+ const { i18n } = useTranslationContext();
+
+
+ const doLogin = useCallback(async function doLoginImpl() {
+ const secretToken = normalizeToken(token);
+ const baseUrl = id === undefined ? backendURL : `${backendURL}/instances/${id}`
+ const result = await requestNewLoginToken(baseUrl, secretToken);
+ if (result.valid) {
+ const { token, expiration } = result
+ onConfirm({ token, expiration });
+ } else {
+ onConfirm(undefined);
+ }
+ }, [backendURL, id, token])
+
+ async function changeServer() {
+ changeBackend("")
+ }
+
+ console.log(admin, id)
+ if (admin && id !== "default") {
+ //admin trying to access another instance
+ return (<div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Login required`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <p>
+ <i18n.Translate>Need the access token for the instance.</i18n.Translate>
+ </p>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"current access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doLogin()
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "flex-end",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <AsyncButton
+ onClick={doLogin}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>)
+ }
+
+ return (
+ <div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Login required`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <i18n.Translate>Please enter your access token.</i18n.Translate>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">URL</label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="text"
+ placeholder="set new url"
+ name="id"
+ value={backendURL}
+ disabled
+ readOnly
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">
+ <i18n.Translate>Access Token</i18n.Translate>
+ </label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="password"
+ placeholder={"current access token"}
+ name="token"
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doLogin()
+ : null
+ }
+ value={token}
+ onInput={(e): void => setToken(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "space-between",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <AsyncButton
+
+ onClick={changeServer}
+ >
+ <i18n.Translate>Change server</i18n.Translate>
+ </AsyncButton>
+
+ <AsyncButton
+ type="is-info"
+ onClick={doLogin}
+ >
+ <i18n.Translate>Confirm</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+function AsyncButton({ onClick, disabled, type = "", children }: { type?: string, disabled?: boolean, onClick: () => Promise<void>, children: ComponentChildren }): VNode {
+ const [running, setRunning] = useState(false)
+ return <button class={"button " + type} disabled={disabled || running} onClick={() => {
+ setRunning(true)
+ onClick().then(() => {
+ setRunning(false)
+ }).catch(() => {
+ setRunning(false)
+ })
+ }}>
+ {children}
+ </button>
+}
+
+
+export function ConnectionPage({ onConfirm }: { onConfirm: (s: string) => void }): VNode {
+ const { url: backendURL } = useBackendContext()
+
+ const [url, setURL] = useState(cleanUp(backendURL));
+ const { i18n } = useTranslationContext();
+
+ async function doConnect() {
+ onConfirm(url)
+ }
+
+ return (
+ <div class="columns is-centered" style={{ margin: "auto" }}>
+ <div class="column is-two-thirds ">
+ <div class="modal-card" style={{ width: "100%", margin: 0 }}>
+ <header
+ class="modal-card-head"
+ style={{ border: "1px solid", borderBottom: 0 }}
+ >
+ <p class="modal-card-title">{i18n.str`Connect to backend`}</p>
+ </header>
+ <section
+ class="modal-card-body"
+ style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
+ >
+ <i18n.Translate>Location of the backend server</i18n.Translate>
+ <div class="field is-horizontal">
+ <div class="field-label is-normal">
+ <label class="label">URL</label>
+ </div>
+ <div class="field-body">
+ <div class="field">
+ <p class="control is-expanded">
+ <input
+ class="input"
+ type="text"
+ placeholder="set new url"
+ name="id"
+ value={url ?? ""}
+ onKeyPress={(e) =>
+ e.keyCode === 13
+ ? doConnect()
+ : null
+ }
+ onInput={(e): void => setURL(e?.currentTarget.value)}
+ />
+ </p>
+ </div>
+ </div>
+ </div>
+ </section>
+ <footer
+ class="modal-card-foot "
+ style={{
+ justifyContent: "flex-end",
+ border: "1px solid",
+ borderTop: 0,
+ }}
+ >
+ <AsyncButton
+ disabled={backendURL === url}
+ onClick={doConnect}
+ >
+ <i18n.Translate>Try again</i18n.Translate>
+ </AsyncButton>
+ </footer>
+ </div>
+ </div>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
index 0d514f2df..87bd2fa39 100644
--- a/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
+++ b/packages/merchant-backoffice-ui/src/paths/settings/index.tsx
@@ -13,7 +13,7 @@ function getBrowserLang(): string | undefined {
return undefined;
}
-export function Settings(): VNode {
+export function Settings({ onClose }: { onClose?: () => void }): VNode {
const { i18n } = useTranslationContext()
const borwserLang = getBrowserLang()
const { update } = useLang()
@@ -94,11 +94,19 @@ export function Settings(): VNode {
/>
</FormProvider>
</div>
-
-
</div>
<div class="column" />
</div>
- </section>
- </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
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 24e42099e..0c3c367af 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -565,7 +565,7 @@ class BankServiceBase {
protected globalTestState: GlobalTestState,
protected bankConfig: BankConfig,
protected configFile: string,
- ) {}
+ ) { }
}
export interface HarnessExchangeBankAccount {
@@ -580,8 +580,7 @@ export interface HarnessExchangeBankAccount {
*/
export class FakebankService
extends BankServiceBase
- implements BankServiceHandle
-{
+ implements BankServiceHandle {
proc: ProcessWrapper | undefined;
http = createPlatformHttpLib({ enableThrottling: false });
@@ -790,7 +789,7 @@ export class ExchangeService implements ExchangeServiceInterface {
async runWirewatchOnce() {
if (useLibeufinBank) {
- // Not even 2 secods showed to be enough!
+ // Not even 2 seconds showed to be enough!
await waitMs(4000);
}
await runCommand(
@@ -1013,7 +1012,7 @@ export class ExchangeService implements ExchangeServiceInterface {
private exchangeConfig: ExchangeConfig,
private configFilename: string,
private keyPair: EddsaKeyPair,
- ) {}
+ ) { }
get name() {
return this.exchangeConfig.name;
@@ -1369,7 +1368,7 @@ export class MerchantService implements MerchantServiceInterface {
private globalState: GlobalTestState,
private merchantConfig: MerchantConfig,
private configFilename: string,
- ) {}
+ ) { }
private currentTimetravelOffsetMs: number | undefined;
@@ -1707,7 +1706,7 @@ export class WalletService {
constructor(
private globalState: GlobalTestState,
private opts: WalletServiceOptions,
- ) {}
+ ) { }
get socketPath() {
const unixPath = path.join(
@@ -1816,7 +1815,7 @@ export class WalletClient {
return client.call(operation, payload);
}
- constructor(private args: WalletClientArgs) {}
+ constructor(private args: WalletClientArgs) { }
async connect(): Promise<void> {
const waiter = this.waiter;
@@ -1883,11 +1882,9 @@ export class WalletCli {
? `--crypto-worker=${cliOpts.cryptoWorkerType}`
: "";
const logName = `wallet-${self.name}`;
- const command = `taler-wallet-cli ${
- self.timetravelArg ?? ""
- } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${
- self.dbfile
- }' api '${op}' ${shellWrap(JSON.stringify(payload))}`;
+ const command = `taler-wallet-cli ${self.timetravelArg ?? ""
+ } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${self.dbfile
+ }' api '${op}' ${shellWrap(JSON.stringify(payload))}`;
const resp = await sh(self.globalTestState, logName, command);
logger.info("--- wallet core response ---");
logger.info(resp);
diff --git a/packages/taler-harness/src/harness/libeufin.ts b/packages/taler-harness/src/harness/libeufin.ts
index 9f3e7a5a0..caeea85ae 100644
--- a/packages/taler-harness/src/harness/libeufin.ts
+++ b/packages/taler-harness/src/harness/libeufin.ts
@@ -251,7 +251,7 @@ export interface NexusTask {
taskCronSpec: string;
// Only meaningful for "fetch" types.
taskParams: FetchParams;
- // Timestamp in secons when the next iteration will run.
+ // Timestamp in seconds when the next iteration will run.
nextScheduledExecutionSec: number;
// Timestamp in seconds when the previous iteration ran.
prevScheduledExecutionSec: number;
@@ -618,9 +618,9 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-createebicssubscriber",
"libeufin-cli sandbox ebicssubscriber create" +
- ` --host-id=${details.hostId}` +
- ` --partner-id=${details.partnerId}` +
- ` --user-id=${details.userId}`,
+ ` --host-id=${details.hostId}` +
+ ` --partner-id=${details.partnerId}` +
+ ` --user-id=${details.userId}`,
this.env(),
);
console.log(stdout);
@@ -634,13 +634,13 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-createebicsbankaccount",
"libeufin-cli sandbox ebicsbankaccount create" +
- ` --iban=${bankAccountDetails.iban}` +
- ` --bic=${bankAccountDetails.bic}` +
- ` --person-name='${bankAccountDetails.personName}'` +
- ` --account-name=${bankAccountDetails.accountName}` +
- ` --ebics-host-id=${sd.hostId}` +
- ` --ebics-partner-id=${sd.partnerId}` +
- ` --ebics-user-id=${sd.userId}`,
+ ` --iban=${bankAccountDetails.iban}` +
+ ` --bic=${bankAccountDetails.bic}` +
+ ` --person-name='${bankAccountDetails.personName}'` +
+ ` --account-name=${bankAccountDetails.accountName}` +
+ ` --ebics-host-id=${sd.hostId}` +
+ ` --ebics-partner-id=${sd.partnerId}` +
+ ` --ebics-user-id=${sd.userId}`,
this.env(),
);
console.log(stdout);
@@ -673,11 +673,11 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-createebicsconnection",
`libeufin-cli connections new-ebics-connection` +
- ` --ebics-url=${connectionDetails.ebicsUrl}` +
- ` --host-id=${connectionDetails.subscriberDetails.hostId}` +
- ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` +
- ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` +
- ` ${connectionDetails.connectionName}`,
+ ` --ebics-url=${connectionDetails.ebicsUrl}` +
+ ` --host-id=${connectionDetails.subscriberDetails.hostId}` +
+ ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` +
+ ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` +
+ ` ${connectionDetails.connectionName}`,
{
...process.env,
LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
@@ -693,9 +693,9 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-createbackupfile",
`libeufin-cli connections export-backup` +
- ` --passphrase=${details.passphrase}` +
- ` --output-file=${details.outputFile}` +
- ` ${details.connectionName}`,
+ ` --passphrase=${details.passphrase}` +
+ ` --output-file=${details.outputFile}` +
+ ` ${details.connectionName}`,
{
...process.env,
LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
@@ -711,7 +711,7 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-createkeyletter",
`libeufin-cli connections get-key-letter` +
- ` ${details.connectionName} ${details.outputFile}`,
+ ` ${details.connectionName} ${details.outputFile}`,
{
...process.env,
LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
@@ -774,9 +774,9 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-importbankaccount",
"libeufin-cli connections import-bank-account" +
- ` --offered-account-id=${importDetails.offeredBankAccountName}` +
- ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` +
- ` ${importDetails.connectionName}`,
+ ` --offered-account-id=${importDetails.offeredBankAccountName}` +
+ ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` +
+ ` ${importDetails.connectionName}`,
{
...process.env,
LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
@@ -822,12 +822,12 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-preparepayment",
`libeufin-cli accounts prepare-payment` +
- ` --creditor-iban=${details.creditorIban}` +
- ` --creditor-bic=${details.creditorBic}` +
- ` --creditor-name='${details.creditorName}'` +
- ` --payment-subject='${details.subject}'` +
- ` --payment-amount=${details.currency}:${details.amount}` +
- ` ${details.nexusBankAccountName}`,
+ ` --creditor-iban=${details.creditorIban}` +
+ ` --creditor-bic=${details.creditorBic}` +
+ ` --creditor-name='${details.creditorName}'` +
+ ` --payment-subject='${details.subject}'` +
+ ` --payment-amount=${details.currency}:${details.amount}` +
+ ` ${details.nexusBankAccountName}`,
{
...process.env,
LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
@@ -846,8 +846,8 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-submitpayments",
`libeufin-cli accounts submit-payments` +
- ` --payment-uuid=${paymentUuid}` +
- ` ${details.nexusBankAccountName}`,
+ ` --payment-uuid=${paymentUuid}` +
+ ` ${details.nexusBankAccountName}`,
{
...process.env,
LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
@@ -863,9 +863,9 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-new-anastasis-facade",
`libeufin-cli facades new-anastasis-facade` +
- ` --currency ${req.currency}` +
- ` --facade-name ${req.facadeName}` +
- ` ${req.connectionName} ${req.accountName}`,
+ ` --currency ${req.currency}` +
+ ` --facade-name ${req.facadeName}` +
+ ` ${req.connectionName} ${req.accountName}`,
{
...process.env,
LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
@@ -881,9 +881,9 @@ export class LibeufinCli {
this.globalTestState,
"libeufin-cli-new-taler-wire-gateway-facade",
`libeufin-cli facades new-taler-wire-gateway-facade` +
- ` --currency ${req.currency}` +
- ` --facade-name ${req.facadeName}` +
- ` ${req.connectionName} ${req.accountName}`,
+ ` --currency ${req.currency}` +
+ ` --facade-name ${req.facadeName}` +
+ ` ${req.connectionName} ${req.accountName}`,
{
...process.env,
LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl,
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
index fd7a8ca3a..27de8a0a0 100644
--- a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
@@ -193,7 +193,6 @@ export async function runMerchantInstancesTest(t: GlobalTestState) {
});
console.log(exc);
t.assertTrue(exc.errorDetail.httpStatusCode === 401);
- t.assertDeepEqual(exc.response?.status, 401);
}
}
diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts
index cbdcb9fdf..ccbbf79b3 100644
--- a/packages/taler-util/src/MerchantApiClient.ts
+++ b/packages/taler-util/src/MerchantApiClient.ts
@@ -14,6 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { codecForAny } from "./codec.js";
import {
createPlatformHttpLib,
expectSuccessResponseOrThrow,
@@ -221,7 +222,7 @@ export class MerchantApiClient {
const resp = await this.httpClient.fetch(url.href, {
headers: this.makeAuthHeader(),
});
- return resp.json();
+ return readSuccessResponseJsonOrThrow(resp, codecForAny());
}
async getInstanceFullDetails(instanceId: string): Promise<any> {
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index cc9c706ba..de5be71a1 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -392,7 +392,7 @@ function csKdfMod(
// Newer versions of node have TextEncoder and TextDecoder as a global,
// just like modern browsers.
// In older versions of node or environments that do not have these
-// globals, they must be polyfilled (by adding them to globa/globalThis)
+// globals, they must be polyfilled (by adding them to global/globalThis)
// before stringToBytes or bytesToString is called the first time.
let encoder: any;
@@ -693,7 +693,7 @@ export async function csBlind(
* Unblind operation to unblind the signature
* @param bseed seed to derive secrets
* @param rPub public R received from /csr
- * @param csPub denomination publick key
+ * @param csPub denomination public key
* @param b returned from exchange to select c
* @param csSig blinded signature
* @returns unblinded signature
@@ -721,7 +721,7 @@ export async function csUnblind(
* Verification algorithm for CS signatures
* @param hm message signed
* @param csSig unblinded signature
- * @param csPub denomination publick key
+ * @param csPub denomination public key
* @returns true if valid, false if invalid
*/
export async function csVerify(
@@ -844,8 +844,7 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
return hash(uint8ArrayBuf);
} else {
throw Error(
- `unsupported cipher (${
- (pub as DenominationPubKey).cipher
+ `unsupported cipher (${(pub as DenominationPubKey).cipher
}), unable to hash`,
);
}
@@ -1023,7 +1022,7 @@ export enum WalletAccountMergeFlags {
export class SignaturePurposeBuilder {
private chunks: Uint8Array[] = [];
- constructor(private purposeNum: number) {}
+ constructor(private purposeNum: number) { }
put(bytes: Uint8Array): SignaturePurposeBuilder {
this.chunks.push(Uint8Array.from(bytes));
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 17900129c..8a0608008 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -1972,42 +1972,58 @@ export interface ExchangeAgeWithdrawRevealResponse {
ev_sigs : BlindedDenominationSignature[];
}
-export interface DepositSuccess {
+interface DepositConfirmationSignature {
+ // The EdDSA signature of `TALER_DepositConfirmationPS` using a current
+ // `signing key of the exchange <sign-key-priv>` affirming the successful
+ // deposit and that the exchange will transfer the funds after the refund
+ // deadline, or as soon as possible if the refund deadline is zero.
+ exchange_sig: EddsaSignatureString;
+}
+
+export interface BatchDepositSuccess {
// Optional base URL of the exchange for looking up wire transfers
// associated with this transaction. If not given,
// the base URL is the same as the one used for this request.
- // Can be used if the base URL for /transactions/ differs from that
- // for /coins/, i.e. for load balancing. Clients SHOULD
- // respect the transaction_base_url if provided. Any HTTP server
+ // Can be used if the base URL for ``/transactions/`` differs from that
+ // for ``/coins/``, i.e. for load balancing. Clients SHOULD
+ // respect the ``transaction_base_url`` if provided. Any HTTP server
// belonging to an exchange MUST generate a 307 or 308 redirection
// to the correct base URL should a client uses the wrong base
// URL, or if the base URL has changed since the deposit.
transaction_base_url?: string;
- // timestamp when the deposit was received by the exchange.
+ // Timestamp when the deposit was received by the exchange.
exchange_timestamp: TalerProtocolTimestamp;
- // the EdDSA signature of TALER_DepositConfirmationPS using a current
- // signing key of the exchange affirming the successful
- // deposit and that the exchange will transfer the funds after the refund
- // deadline, or as soon as possible if the refund deadline is zero.
- exchange_sig: string;
-
- // public EdDSA key of the exchange that was used to
+ // `Public EdDSA key of the exchange <sign-key-pub>` that was used to
// generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
+ // Should match one of the exchange's signing keys from ``/keys``. It is given
// explicitly as the client might otherwise be confused by clock skew as to
// which signing key was used.
- exchange_pub: string;
+ exchange_pub: EddsaPublicKeyString;
+
+ // Array of deposit confirmation signatures from the exchange
+ // Entries must be in the same order the coins were given
+ // in the batch deposit request.
+ exchange_sigs: DepositConfirmationSignature[];
}
-export const codecForDepositSuccess = (): Codec<DepositSuccess> =>
- buildCodecForObject<DepositSuccess>()
+export const codecForDepositConfirmationSignature =
+ (): Codec<DepositConfirmationSignature> =>
+ buildCodecForObject<DepositConfirmationSignature>()
+ .property("exchange_sig", codecForString())
+ .build("DepositConfirmationSignature");
+
+export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
+ buildCodecForObject<BatchDepositSuccess>()
.property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
+ .property(
+ "exchange_sigs",
+ codecForList(codecForDepositConfirmationSignature()),
+ )
.property("exchange_timestamp", codecForTimestamp)
.property("transaction_base_url", codecOptional(codecForString()))
- .build("DepositSuccess");
+ .build("BatchDepositSuccess");
export interface TrackTransactionWired {
// Raw wire transfer identifier of the deposit.
@@ -2231,6 +2247,9 @@ export interface ExchangePurseDeposits {
deposits: PurseDeposit[];
}
+/**
+ * @deprecated batch deposit should be used.
+ */
export interface ExchangeDepositRequest {
// Amount to be deposited, can be a fraction of the
// coin's total value.
@@ -2293,6 +2312,67 @@ export interface ExchangeDepositRequest {
h_age_commitment?: string;
}
+export type WireSalt = string;
+
+export interface ExchangeBatchDepositRequest {
+ // The merchant's account details.
+ merchant_payto_uri: string;
+
+ // The salt is used to hide the ``payto_uri`` from customers
+ // when computing the ``h_wire`` of the merchant.
+ wire_salt: WireSalt;
+
+ // SHA-512 hash of the contract of the merchant with the customer. Further
+ // details are never disclosed to the exchange.
+ h_contract_terms: HashCodeString;
+
+ // The list of coins that are going to be deposited with this Request.
+ coins: BatchDepositRequestCoin[];
+
+ // Timestamp when the contract was finalized.
+ timestamp: TalerProtocolTimestamp;
+
+ // Indicative time by which the exchange undertakes to transfer the funds to
+ // the merchant, in case of successful payment. A wire transfer deadline of 'never'
+ // is not allowed.
+ wire_transfer_deadline: TalerProtocolTimestamp;
+
+ // EdDSA `public key of the merchant <merchant-pub>`, so that the client can identify the
+ // merchant for refund requests.
+ merchant_pub: EddsaPublicKeyString;
+
+ // Date until which the merchant can issue a refund to the customer via the
+ // exchange, to be omitted if refunds are not allowed.
+ //
+ // THIS FIELD WILL BE DEPRICATED, once the refund mechanism becomes a
+ // policy via extension.
+ refund_deadline?: TalerProtocolTimestamp;
+
+ // CAVEAT: THIS IS WORK IN PROGRESS
+ // (Optional) policy for the batch-deposit.
+ // This might be a refund, auction or escrow policy.
+ policy?: any;
+}
+
+export interface BatchDepositRequestCoin {
+ // EdDSA public key of the coin being deposited.
+ coin_pub: EddsaPublicKeyString;
+
+ // Hash of denomination RSA key with which the coin is signed.
+ denom_pub_hash: HashCodeString;
+
+ // Exchange's unblinded RSA signature of the coin.
+ ub_sig: UnblindedSignature;
+
+ // Amount to be deposited, can be a fraction of the
+ // coin's total value.
+ contribution: Amounts;
+
+ // Signature over `TALER_DepositRequestPS`, made by the customer with the
+ // `coin's private key <coin-priv>`.
+ coin_sig: EddsaSignatureString;
+}
+
export interface WalletKycUuid {
// UUID that the wallet should use when initiating
// the KYC check.
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index c6f19c73f..f7bd3d120 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -57,7 +57,9 @@ import {
DenomKeyType,
DenominationPubKey,
ExchangeAuditor,
+ InternationalizedString,
MerchantContractTerms,
+ MerchantInfo,
PeerContractTerms,
UnblindedSignature,
codecForMerchantContractTerms,
@@ -2667,3 +2669,49 @@ export const codecForTestingSetTimetravelRequest =
buildCodecForObject<TestingSetTimetravelRequest>()
.property("offsetMs", codecForNumber())
.build("TestingSetTimetravelRequest");
+
+export interface AllowedAuditorInfo {
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export interface AllowedExchangeInfo {
+ exchangeBaseUrl: string;
+ exchangePub: string;
+}
+
+/**
+ * Data extracted from the contract terms that is relevant for payment
+ * processing in the wallet.
+ */
+export interface WalletContractData {
+ /**
+ * Fulfillment URL, or the empty string if the order has no fulfillment URL.
+ *
+ * Stored as a non-nullable string as we use this field for IndexedDB indexing.
+ */
+ fulfillmentUrl: string;
+
+ contractTermsHash: string;
+ fulfillmentMessage?: string;
+ fulfillmentMessageI18n?: InternationalizedString;
+ merchantSig: string;
+ merchantPub: string;
+ merchant: MerchantInfo;
+ amount: AmountString;
+ orderId: string;
+ merchantBaseUrl: string;
+ summary: string;
+ summaryI18n: { [lang_tag: string]: string } | undefined;
+ autoRefund: TalerProtocolDuration | undefined;
+ maxWireFee: AmountString;
+ wireFeeAmortization: number;
+ payDeadline: TalerProtocolTimestamp;
+ refundDeadline: TalerProtocolTimestamp;
+ allowedExchanges: AllowedExchangeInfo[];
+ timestamp: TalerProtocolTimestamp;
+ wireMethod: string;
+ wireInfoHash: string;
+ maxDepositFee: AmountString;
+ minimumAge?: number;
+}
diff --git a/packages/taler-wallet-cli/Makefile b/packages/taler-wallet-cli/Makefile
index 388401eae..6d695e9c1 100644
--- a/packages/taler-wallet-cli/Makefile
+++ b/packages/taler-wallet-cli/Makefile
@@ -39,6 +39,7 @@ install-nodeps:
ln -sf $(install_target)/node_modules/taler-wallet-cli/bin/taler-wallet-cli.mjs $(prefix)/bin/taler-wallet-cli
deps:
pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-cli...
+ pnpm run --filter @gnu-taler/taler-wallet-cli... compile
install:
$(MAKE) deps
$(MAKE) install-nodeps
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 25757ef25..abfb02445 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1042,52 +1042,6 @@ export enum RefundReason {
AbortRefund = "abort-pay-refund",
}
-export interface AllowedAuditorInfo {
- auditorBaseUrl: string;
- auditorPub: string;
-}
-
-export interface AllowedExchangeInfo {
- exchangeBaseUrl: string;
- exchangePub: string;
-}
-
-/**
- * Data extracted from the contract terms that is relevant for payment
- * processing in the wallet.
- */
-export interface WalletContractData {
- /**
- * Fulfillment URL, or the empty string if the order has no fulfillment URL.
- *
- * Stored as a non-nullable string as we use this field for IndexedDB indexing.
- */
- fulfillmentUrl: string;
-
- contractTermsHash: string;
- fulfillmentMessage?: string;
- fulfillmentMessageI18n?: InternationalizedString;
- merchantSig: string;
- merchantPub: string;
- merchant: MerchantInfo;
- amount: AmountString;
- orderId: string;
- merchantBaseUrl: string;
- summary: string;
- summaryI18n: { [lang_tag: string]: string } | undefined;
- autoRefund: TalerProtocolDuration | undefined;
- maxWireFee: AmountString;
- wireFeeAmortization: number;
- payDeadline: TalerProtocolTimestamp;
- refundDeadline: TalerProtocolTimestamp;
- allowedExchanges: AllowedExchangeInfo[];
- timestamp: TalerProtocolTimestamp;
- wireMethod: string;
- wireInfoHash: string;
- maxDepositFee: AmountString;
- minimumAge?: number;
-}
-
export enum PurchaseStatus {
/**
* Not downloaded yet.
@@ -1710,6 +1664,8 @@ export interface DepositGroupRecord {
/**
* Verbatim contract terms.
+ *
+ * FIXME: Move this to the contract terms object store!
*/
contractTermsRaw: MerchantContractTerms;
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
index a20ded2af..8034f78ea 100644
--- a/packages/taler-wallet-core/src/operations/balance.ts
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -50,6 +50,8 @@
* Imports.
*/
import {
+ AllowedAuditorInfo,
+ AllowedExchangeInfo,
AmountJson,
Amounts,
BalancesResponse,
@@ -60,17 +62,15 @@ import {
ScopeType,
} from "@gnu-taler/taler-util";
import {
- AllowedAuditorInfo,
- AllowedExchangeInfo,
RefreshGroupRecord,
WalletStoresV1,
WithdrawalGroupStatus,
} from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js";
+import { assertUnreachable } from "../util/assertUnreachable.js";
import { checkLogicInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess } from "../util/query.js";
import { getExchangeDetails } from "./exchanges.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
/**
* Logger.
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index 8ea792d91..a3483a332 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -21,70 +21,69 @@ import {
AbsoluteTime,
AmountJson,
Amounts,
+ BatchDepositRequestCoin,
CancellationToken,
- canonicalJson,
- codecForDepositSuccess,
- codecForTackTransactionAccepted,
- codecForTackTransactionWired,
CoinRefreshRequest,
CreateDepositGroupRequest,
CreateDepositGroupResponse,
DepositGroupFees,
- durationFromSpec,
- encodeCrock,
- ExchangeDepositRequest,
+ Duration,
+ ExchangeBatchDepositRequest,
ExchangeRefundRequest,
- getRandomBytes,
- hashTruncate32,
- hashWire,
HttpStatusCode,
- j2s,
Logger,
MerchantContractTerms,
NotificationType,
- parsePaytoUri,
PayCoinSelection,
PrepareDepositRequest,
PrepareDepositResponse,
RefreshReason,
- stringToBytes,
+ TalerError,
TalerErrorCode,
- TalerProtocolTimestamp,
TalerPreciseTimestamp,
+ TalerProtocolTimestamp,
TrackTransaction,
+ TransactionAction,
TransactionMajorState,
TransactionMinorState,
TransactionState,
TransactionType,
URL,
WireFee,
- TransactionAction,
- Duration,
+ canonicalJson,
+ codecForBatchDepositSuccess,
+ codecForTackTransactionAccepted,
+ codecForTackTransactionWired,
+ durationFromSpec,
+ encodeCrock,
+ getRandomBytes,
+ hashTruncate32,
+ hashWire,
+ j2s,
+ parsePaytoUri,
+ stringToBytes,
} from "@gnu-taler/taler-util";
+import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import { DepositElementStatus, DepositGroupRecord } from "../db.js";
import {
- DenominationRecord,
- DepositGroupRecord,
- DepositElementStatus,
-} from "../db.js";
-import { TalerError } from "@gnu-taler/taler-util";
-import {
- createRefreshGroup,
DepositOperationStatus,
DepositTrackingInfo,
- getTotalRefreshCost,
KycPendingInfo,
- KycUserType,
PendingTaskType,
RefreshOperationStatus,
+ createRefreshGroup,
+ getTotalRefreshCost,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
-import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import { assertUnreachable } from "../util/assertUnreachable.js";
+import { selectPayCoinsNew } from "../util/coinSelection.js";
+import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import {
- constructTaskIdentifier,
TaskRunResult,
+ TombstoneTag,
+ constructTaskIdentifier,
runLongpollAsync,
spendCoins,
- TombstoneTag,
} from "./common.js";
import { getExchangeDetails } from "./exchanges.js";
import {
@@ -92,15 +91,12 @@ import {
generateDepositPermissions,
getTotalPaymentCost,
} from "./pay-merchant.js";
-import { selectPayCoinsNew } from "../util/coinSelection.js";
import {
constructTransactionIdentifier,
notifyTransition,
parseTransactionIdentifier,
stopLongpolling,
} from "./transactions.js";
-import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
-import { assertUnreachable } from "../util/assertUnreachable.js";
/**
* Logger.
@@ -169,6 +165,10 @@ export function computeDepositTransactionStatus(
}
}
+/**
+ * Compute the possible actions possible on a deposit transaction
+ * based on the current transaction state.
+ */
export function computeDepositTransactionActions(
dg: DepositGroupRecord,
): TransactionAction[] {
@@ -200,6 +200,11 @@ export function computeDepositTransactionActions(
}
}
+/**
+ * Put a deposit group in a suspended state.
+ * While the deposit group is suspended, no network requests
+ * will be made to advance the transaction status.
+ */
export async function suspendDepositGroup(
ws: InternalWalletState,
depositGroupId: string,
@@ -407,46 +412,6 @@ export async function deleteDepositGroup(
}
/**
- * Check KYC status with the exchange, throw an appropriate exception when KYC
- * is required.
- *
- * FIXME: Why does this throw an exception when KYC is required?
- * Should we not return some proper result record here?
- */
-async function checkDepositKycStatus(
- ws: InternalWalletState,
- exchangeUrl: string,
- kycInfo: KycPendingInfo,
- userType: KycUserType,
-): Promise<void> {
- const url = new URL(
- `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
- exchangeUrl,
- );
- logger.info(`kyc url ${url.href}`);
- const kycStatusReq = await ws.http.fetch(url.href, {
- method: "GET",
- });
- if (kycStatusReq.status === HttpStatusCode.Ok) {
- logger.warn("kyc requested, but already fulfilled");
- return;
- } else if (kycStatusReq.status === HttpStatusCode.Accepted) {
- const kycStatus = await kycStatusReq.json();
- logger.info(`kyc status: ${j2s(kycStatus)}`);
- // FIXME: This error code is totally wrong
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
- {
- kycUrl: kycStatus.kyc_url,
- },
- `KYC check required for deposit`,
- );
- } else {
- throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`);
- }
-}
-
-/**
* Check whether the refresh associated with the
* aborting deposit group is done.
*
@@ -940,38 +905,58 @@ async function processDepositGroupPendingDeposit(
contractData,
);
- for (let i = 0; i < depositPermissions.length; i++) {
- const perm = depositPermissions[i];
+ // Exchanges involved in the deposit
+ const exchanges: Set<string> = new Set();
- if (depositGroup.statusPerCoin[i] !== DepositElementStatus.DepositPending) {
- continue;
- }
+ for (const dp of depositPermissions) {
+ exchanges.add(dp.exchange_url);
+ }
- const requestBody: ExchangeDepositRequest = {
- contribution: Amounts.stringify(perm.contribution),
- merchant_payto_uri: depositGroup.wire.payto_uri,
- wire_salt: depositGroup.wire.salt,
+ // We need to do one batch per exchange.
+ for (const exchangeUrl of exchanges.values()) {
+ const coins: BatchDepositRequestCoin[] = [];
+ const batchIndexes: number[] = [];
+
+ const batchReq: ExchangeBatchDepositRequest = {
+ coins,
h_contract_terms: depositGroup.contractTermsHash,
- ub_sig: perm.ub_sig,
+ merchant_payto_uri: depositGroup.wire.payto_uri,
+ merchant_pub: depositGroup.contractTermsRaw.merchant_pub,
timestamp: depositGroup.contractTermsRaw.timestamp,
+ wire_salt: depositGroup.wire.salt,
wire_transfer_deadline:
depositGroup.contractTermsRaw.wire_transfer_deadline,
refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
- coin_sig: perm.coin_sig,
- denom_pub_hash: perm.h_denom,
- merchant_pub: depositGroup.merchantPub,
- h_age_commitment: perm.h_age_commitment,
};
+
+ for (let i = 0; i < depositPermissions.length; i++) {
+ const perm = depositPermissions[i];
+ if (perm.exchange_url != exchangeUrl) {
+ continue;
+ }
+ coins.push({
+ coin_pub: perm.coin_pub,
+ coin_sig: perm.coin_sig,
+ contribution: Amounts.stringify(perm.contribution),
+ denom_pub_hash: perm.h_denom,
+ ub_sig: perm.ub_sig,
+ });
+ batchIndexes.push(i);
+ }
+
// Check for cancellation before making network request.
cancellationToken?.throwIfCancelled();
- const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
+ const url = new URL(`batch-deposit`, exchangeUrl);
logger.info(`depositing to ${url}`);
const httpResp = await ws.http.fetch(url.href, {
method: "POST",
- body: requestBody,
+ body: batchReq,
cancellationToken: cancellationToken,
});
- await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
+ await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForBatchDepositSuccess(),
+ );
await ws.db
.mktx((x) => [x.depositGroups])
@@ -980,11 +965,13 @@ async function processDepositGroupPendingDeposit(
if (!dg) {
return;
}
- const coinStatus = dg.statusPerCoin[i];
- switch (coinStatus) {
- case DepositElementStatus.DepositPending:
- dg.statusPerCoin[i] = DepositElementStatus.Tracking;
- await tx.depositGroups.put(dg);
+ for (const batchIndex of batchIndexes) {
+ const coinStatus = dg.statusPerCoin[batchIndex];
+ switch (coinStatus) {
+ case DepositElementStatus.DepositPending:
+ dg.statusPerCoin[batchIndex] = DepositElementStatus.Tracking;
+ await tx.depositGroups.put(dg);
+ }
}
});
}
@@ -1538,10 +1525,7 @@ async function getTotalFeesForDepositAmount(
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl)
.filter((x) =>
- Amounts.isSameCurrency(
- x.value,
- pcs.coinContributions[i],
- ),
+ Amounts.isSameCurrency(x.value, pcs.coinContributions[i]),
);
const amountLeft = Amounts.sub(
denom.value,
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index 57367bb20..fe0cbeda0 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -63,7 +63,6 @@ import {
RefreshReason,
SharePaymentResult,
StartRefundQueryForUriResponse,
- stringifyPaytoUri,
stringifyPayUri,
stringifyTalerUri,
TalerError,
@@ -78,6 +77,7 @@ import {
TransactionState,
TransactionType,
URL,
+ WalletContractData,
} from "@gnu-taler/taler-util";
import {
getHttpResponseErrorDetails,
@@ -95,7 +95,6 @@ import {
PurchaseRecord,
PurchaseStatus,
RefundReason,
- WalletContractData,
WalletStoresV1,
} from "../db.js";
import {
@@ -115,15 +114,13 @@ import { checkDbInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess } from "../util/query.js";
import {
constructTaskIdentifier,
- TaskRunResult,
- TaskRunResultType,
RetryInfo,
- TaskIdentifiers,
-} from "./common.js";
-import {
runLongpollAsync,
runTaskWithErrorReporting,
spendCoins,
+ TaskIdentifiers,
+ TaskRunResult,
+ TaskRunResultType,
} from "./common.js";
import {
calculateRefreshOutput,
@@ -173,10 +170,7 @@ export async function getTotalPaymentCost(
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(coin.exchangeBaseUrl)
.filter((x) =>
- Amounts.isSameCurrency(
- x.value,
- pcs.coinContributions[i],
- ),
+ Amounts.isSameCurrency(x.value, pcs.coinContributions[i]),
);
const amountLeft = Amounts.sub(
denom.value,
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
index 7bdb9af5b..31655ad71 100644
--- a/packages/taler-wallet-core/src/operations/transactions.ts
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -41,6 +41,7 @@ import {
TransactionsResponse,
TransactionState,
TransactionType,
+ WalletContractData,
WithdrawalType,
} from "@gnu-taler/taler-util";
import {
@@ -60,7 +61,6 @@ import {
RefreshOperationStatus,
RefundGroupRecord,
RewardRecord,
- WalletContractData,
WithdrawalGroupRecord,
WithdrawalGroupStatus,
WithdrawalRecordType,
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
index ef2f85789..6fd0f1b86 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -28,21 +28,20 @@ import {
AbsoluteTime,
AgeCommitmentProof,
AgeRestriction,
+ AllowedAuditorInfo,
+ AllowedExchangeInfo,
AmountJson,
AmountLike,
- AmountResponse,
Amounts,
AmountString,
CoinPublicKeyString,
CoinStatus,
- ConvertAmountRequest,
DenominationInfo,
DenominationPubKey,
DenomSelectionState,
Duration,
ForcedCoinSel,
ForcedDenomSel,
- GetAmountRequest,
j2s,
Logger,
parsePaytoUri,
@@ -50,24 +49,13 @@ import {
PayMerchantInsufficientBalanceDetails,
PayPeerInsufficientBalanceDetails,
strcmp,
- TransactionAmountMode,
- TransactionType,
UnblindedSignature,
} from "@gnu-taler/taler-util";
+import { DenominationRecord } from "../db.js";
import {
- AllowedAuditorInfo,
- AllowedExchangeInfo,
- DenominationRecord,
-} from "../db.js";
-import {
- DbReadOnlyTransaction,
getExchangeDetails,
- GetReadOnlyAccess,
- GetReadWriteAccess,
isWithdrawableDenom,
- StoreNames,
WalletDbReadOnlyTransaction,
- WalletStoresV1,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import {
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index d89ad257a..67c05a42f 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -38,7 +38,6 @@ import {
ApplyDevExperimentRequest,
BackupRecovery,
BalancesResponse,
- FailTransactionRequest,
CheckPeerPullCreditRequest,
CheckPeerPullCreditResponse,
CheckPeerPushDebitRequest,
@@ -51,14 +50,19 @@ import {
ConvertAmountRequest,
CreateDepositGroupRequest,
CreateDepositGroupResponse,
+ CreateStoredBackupResponse,
+ DeleteStoredBackupRequest,
DeleteTransactionRequest,
ExchangeDetailedResponse,
ExchangesListResponse,
+ FailTransactionRequest,
ForceRefreshRequest,
ForgetKnownBankAccountsRequest,
GetAmountRequest,
GetBalanceDetailRequest,
GetContractTermsDetailsRequest,
+ GetCurrencyInfoRequest,
+ GetCurrencyInfoResponse,
GetExchangeTosRequest,
GetExchangeTosResult,
GetPlanForOperationRequest,
@@ -85,16 +89,21 @@ import {
PreparePeerPushCreditRequest,
PreparePeerPushCreditResponse,
PrepareRefundRequest,
- PrepareRewardRequest as PrepareRewardRequest,
+ PrepareRewardRequest,
PrepareTipResult as PrepareRewardResult,
+ RecoverStoredBackupRequest,
RecoveryLoadRequest,
RetryTransactionRequest,
SetCoinSuspendedRequest,
SetWalletDeviceIdRequest,
+ SharePaymentRequest,
+ SharePaymentResult,
StartRefundQueryForUriResponse,
StartRefundQueryRequest,
+ StoredBackupList,
TestPayArgs,
TestPayResult,
+ TestingSetTimetravelRequest,
Transaction,
TransactionByIdRequest,
TransactionsRequest,
@@ -106,22 +115,13 @@ import {
UserAttentionsResponse,
ValidateIbanRequest,
ValidateIbanResponse,
+ WalletContractData,
WalletCoreVersion,
WalletCurrencyInfo,
WithdrawFakebankRequest,
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
- SharePaymentRequest,
- SharePaymentResult,
- GetCurrencyInfoRequest,
- GetCurrencyInfoResponse,
- StoredBackupList,
- CreateStoredBackupResponse,
- RecoverStoredBackupRequest,
- DeleteStoredBackupRequest,
- TestingSetTimetravelRequest,
} from "@gnu-taler/taler-util";
-import { WalletContractData } from "./db.js";
import {
AddBackupProviderRequest,
AddBackupProviderResponse,
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 1a60b148c..2d0878afc 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -15,7 +15,7 @@
*/
/**
- * High-level wallet operations that should be indepentent from the underlying
+ * High-level wallet operations that should be independent from the underlying
* browser extension interface.
*/
@@ -923,9 +923,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
ageCommitmentProof: c.ageCommitmentProof,
spend_allocation: c.spendAllocation
? {
- amount: c.spendAllocation.amount,
- id: c.spendAllocation.id,
- }
+ amount: c.spendAllocation.amount,
+ id: c.spendAllocation.id,
+ }
: undefined,
});
}
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
index 74c92cbc6..1b1802b8c 100644
--- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.stories.tsx
@@ -18,8 +18,6 @@
*
* @author Sebastian Javier Marchano (sebasjm)
*/
-
-import { WalletContractData } from "@gnu-taler/taler-wallet-core";
import * as tests from "@gnu-taler/web-util/testing";
import {
ErrorView,
@@ -27,6 +25,7 @@ import {
LoadingView,
ShowView,
} from "./ShowFullContractTermPopup.js";
+import { WalletContractData } from "@gnu-taler/taler-util";
export default {
title: "ShowFullContractTermPopup",
diff --git a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
index db9b6ebcd..0b3cca0b2 100644
--- a/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
+++ b/packages/taler-wallet-webextension/src/components/ShowFullContractTermPopup.tsx
@@ -13,11 +13,13 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { AbsoluteTime, Duration, Location } from "@gnu-taler/taler-util";
import {
- WalletApiOperation,
+ AbsoluteTime,
+ Duration,
+ Location,
WalletContractData,
-} from "@gnu-taler/taler-wallet-core";
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
@@ -334,8 +336,8 @@ export function ShowView({ contractTerms, hideHandler }: States.Show): VNode {
!contractTerms.autoRefund
? Duration.getZero()
: Duration.fromTalerProtocolDuration(
- contractTerms.autoRefund,
- ),
+ contractTerms.autoRefund,
+ ),
)}
format="dd MMMM yyyy, HH:mm"
/>
diff --git a/packages/web-util/src/utils/request.ts b/packages/web-util/src/utils/request.ts
index 1464eca98..8ce21b0e1 100644
--- a/packages/web-util/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -25,6 +25,8 @@ export enum ErrorType {
UNEXPECTED,
}
+
+
/**
*
* @param baseUrl URL where the service is located
@@ -60,10 +62,27 @@ export async function defaultRequestHandler<T>(
const requestPreventCache = options.preventCache ?? false;
const requestPreventCors = options.preventCors ?? false;
- const _url = new URL(`${baseUrl}${endpoint}`);
+ const validURL = validateURL(baseUrl, endpoint);
+
+ if (!validURL) {
+ const error: HttpResponseUnexpectedError = {
+ info: {
+ url: `${baseUrl}${endpoint}`,
+ payload: {},
+ hasToken: !!options.token,
+ status: 0,
+ options,
+ },
+ type: ErrorType.UNEXPECTED,
+ exception: undefined,
+ loading: false,
+ message: `invalid URL: "${validURL}"`,
+ };
+ throw new RequestError(error)
+ }
Object.entries(requestParams).forEach(([key, value]) => {
- _url.searchParams.set(key, String(value));
+ validURL.searchParams.set(key, String(value));
});
let payload: BodyInit | undefined = undefined;
@@ -77,7 +96,20 @@ export async function defaultRequestHandler<T>(
} else if (typeof requestBody === "object") {
payload = JSON.stringify(requestBody);
} else {
- throw Error("unsupported request body type");
+ const error: HttpResponseUnexpectedError = {
+ info: {
+ url: validURL.href,
+ payload: {},
+ hasToken: !!options.token,
+ status: 0,
+ options,
+ },
+ type: ErrorType.UNEXPECTED,
+ exception: undefined,
+ loading: false,
+ message: `unsupported request body type: "${typeof requestBody}"`,
+ };
+ throw new RequestError(error)
}
}
@@ -88,7 +120,7 @@ export async function defaultRequestHandler<T>(
let response;
try {
- response = await fetch(_url.href, {
+ response = await fetch(validURL.href, {
headers: requestHeaders,
method: requestMethod,
credentials: "omit",
@@ -100,15 +132,29 @@ export async function defaultRequestHandler<T>(
} catch (ex) {
const info: RequestInfo = {
payload,
- url: _url.href,
+ url: validURL.href,
hasToken: !!options.token,
status: 0,
options,
};
- const error: HttpRequestTimeoutError = {
+
+ if (ex instanceof Error) {
+ if (ex.message === "HTTP_REQUEST_TIMEOUT") {
+ const error: HttpRequestTimeoutError = {
+ info,
+ type: ErrorType.TIMEOUT,
+ message: "request timeout",
+ };
+ throw new RequestError(error);
+ }
+ }
+
+ const error: HttpResponseUnexpectedError = {
info,
- type: ErrorType.TIMEOUT,
- message: "Request timeout",
+ type: ErrorType.UNEXPECTED,
+ exception: ex,
+ loading: false,
+ message: (ex instanceof Error ? ex.message : ""),
};
throw new RequestError(error);
}
@@ -124,7 +170,7 @@ export async function defaultRequestHandler<T>(
if (response.ok) {
const result = await buildRequestOk<T>(
response,
- _url.href,
+ validURL.href,
payload,
!!options.token,
options,
@@ -133,7 +179,7 @@ export async function defaultRequestHandler<T>(
} else {
const dataTxt = await response.text();
const error = buildRequestFailed(
- _url.href,
+ validURL.href,
dataTxt,
response.status,
payload,
@@ -377,3 +423,12 @@ export function buildRequestFailed<ErrorDetail>(
return error;
}
}
+
+function validateURL(baseUrl: string, endpoint: string): URL | undefined {
+ try {
+ return new URL(`${baseUrl}${endpoint}`)
+ } catch (ex) {
+ return undefined
+ }
+
+} \ No newline at end of file