gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: new login token


From: gnunet
Subject: [taler-wallet-core] branch master updated: new login token
Date: Mon, 11 Sep 2023 20:08:58 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 8c20f4b27 new login token
8c20f4b27 is described below

commit 8c20f4b27946679267bb44255721a9f14ae1077a
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Sep 11 15:07:55 2023 -0300

    new login token
---
 .../merchant-backoffice-ui/src/Application.tsx     |  32 +--
 .../src/ApplicationReadyRoutes.tsx                 |  42 ++-
 .../merchant-backoffice-ui/src/InstanceRoutes.tsx  |  35 +--
 .../src/components/exception/login.tsx             | 244 -----------------
 .../instance/DefaultInstanceFormFields.tsx         |   4 +-
 .../src/components/menu/SideBar.tsx                |   5 +-
 .../src/components/product/ProductForm.tsx         |   4 +-
 .../src/context/backend.test.ts                    |   6 +-
 .../merchant-backoffice-ui/src/context/backend.ts  |  66 +----
 .../merchant-backoffice-ui/src/context/instance.ts |   5 +-
 .../merchant-backoffice-ui/src/declaration.d.ts    |  39 +++
 .../merchant-backoffice-ui/src/hooks/backend.ts    | 139 ++++++++--
 packages/merchant-backoffice-ui/src/hooks/index.ts |  84 +++---
 .../src/hooks/instance.test.ts                     |   4 +-
 .../merchant-backoffice-ui/src/hooks/instance.ts   |  38 ++-
 .../merchant-backoffice-ui/src/hooks/testing.tsx   |   5 +-
 .../src/hooks/useSettings.ts                       |   9 -
 .../paths/instance/orders/details/DetailPage.tsx   |   8 +-
 .../paths/instance/reserves/details/RewardInfo.tsx |   6 +-
 .../paths/instance/templates/create/CreatePage.tsx |  12 +-
 .../src/paths/instance/templates/qr/QrPage.tsx     |  15 +-
 .../paths/instance/templates/update/UpdatePage.tsx |  16 +-
 .../src/paths/instance/token/DetailPage.tsx        |   6 +-
 .../src/paths/instance/token/index.tsx             |   8 +-
 .../src/paths/instance/update/index.tsx            |  18 +-
 .../validators/create/CreatedSuccessfully.tsx      |   6 +-
 .../src/paths/login/index.tsx                      | 299 ++++++++++++++++++++-
 .../src/paths/settings/index.tsx                   |  18 +-
 packages/web-util/src/utils/request.ts             |  75 +++++-
 29 files changed, 697 insertions(+), 551 deletions(-)

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/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

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]