gnunet-svn
[Top][All Lists]
Advanced

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

[taler-merchant-backoffice] branch master updated (e8bbe2b -> 3b7d2dd)


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated (e8bbe2b -> 3b7d2dd)
Date: Thu, 16 Dec 2021 20:20:44 +0100

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

sebasjm pushed a change to branch master
in repository merchant-backoffice.

    from e8bbe2b  bank: test registration
     new 5365512  more unit test and kyc interfaces
     new 40bcbdf  -formatted with prettier
     new 3b7d2dd  adding kyc frontend

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 packages/merchant-backend/src/hooks/product.ts     | 240 ++++----
 .../src/ApplicationReadyRoutes.tsx                 | 128 +++--
 .../merchant-backoffice/src/InstanceRoutes.tsx     |  73 ++-
 .../src/components/menu/SideBar.tsx                | 190 ++++--
 packages/merchant-backoffice/src/declaration.d.ts  |  40 ++
 packages/merchant-backoffice/src/hooks/admin.ts    |  63 --
 packages/merchant-backoffice/src/hooks/backend.ts  |   3 +
 packages/merchant-backoffice/src/hooks/instance.ts | 121 +++-
 .../src/paths/admin/create/index.tsx               |   2 +-
 .../src/paths/admin/list/index.tsx                 | 175 +++---
 .../src/paths/instance/kyc/list/ListPage.tsx       | 180 ++++++
 .../{reserves/details => kyc/list}/index.tsx       |  31 +-
 packages/merchant-backoffice/tests/axiosMock.ts    | 102 ++++
 .../merchant-backoffice/tests/hooks/async.test.ts  | 158 +++++
 .../tests/hooks/swr/instance.test.ts               | 636 +++++++++++++++++++++
 15 files changed, 1728 insertions(+), 414 deletions(-)
 delete mode 100644 packages/merchant-backoffice/src/hooks/admin.ts
 create mode 100644 
packages/merchant-backoffice/src/paths/instance/kyc/list/ListPage.tsx
 copy packages/merchant-backoffice/src/paths/instance/{reserves/details => 
kyc/list}/index.tsx (74%)
 create mode 100644 packages/merchant-backoffice/tests/hooks/async.test.ts
 create mode 100644 
packages/merchant-backoffice/tests/hooks/swr/instance.test.ts

diff --git a/packages/merchant-backend/src/hooks/product.ts 
b/packages/merchant-backend/src/hooks/product.ts
index edba27b..4fc8bcc 100644
--- a/packages/merchant-backend/src/hooks/product.ts
+++ b/packages/merchant-backend/src/hooks/product.ts
@@ -13,107 +13,93 @@
  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 { useEffect } from 'preact/hooks';
-import useSWR, { trigger, useSWRInfinite, cache, mutate } from 'swr';
-import { useBackendContext } from '../context/backend';
+import { useEffect } from "preact/hooks";
+import useSWR, { trigger, useSWRInfinite, cache, mutate } from "swr";
+import { useBackendContext } from "../context/backend";
 // import { useFetchContext } from '../context/fetch';
-import { useInstanceContext } from '../context/instance';
-import { MerchantBackend, WithId } from '../declaration';
-import { fetcher, HttpError, HttpResponse, HttpResponseOk, mutateAll, request 
} from './backend';
+import { useInstanceContext } from "../context/instance";
+import { MerchantBackend, WithId } from "../declaration";
+import {
+  fetcher,
+  HttpError,
+  HttpResponse,
+  HttpResponseOk,
+  mutateAll,
+  request,
+} from "./backend";
 
 export interface ProductAPI {
-  createProduct: (data: MerchantBackend.Products.ProductAddDetail) => 
Promise<void>;
-  updateProduct: (id: string, data: 
MerchantBackend.Products.ProductPatchDetail) => Promise<void>;
+  createProduct: (
+    data: MerchantBackend.Products.ProductAddDetail
+  ) => Promise<void>;
+  updateProduct: (
+    id: string,
+    data: MerchantBackend.Products.ProductPatchDetail
+  ) => Promise<void>;
   deleteProduct: (id: string) => Promise<void>;
-  lockProduct: (id: string, data: MerchantBackend.Products.LockRequest) => 
Promise<void>;
+  lockProduct: (
+    id: string,
+    data: MerchantBackend.Products.LockRequest
+  ) => Promise<void>;
 }
 
-
 export function useProductAPI(): ProductAPI {
   const { url: baseUrl, token: adminToken } = useBackendContext();
   const { token: instanceToken, id, admin } = useInstanceContext();
 
-  const { url, token } = !admin ? {
-    url: baseUrl, token: adminToken
-  } : {
-    url: `${baseUrl}/instances/${id}`, token: instanceToken
-  };
-
-
-  const createProduct = async (data: 
MerchantBackend.Products.ProductAddDetail): Promise<void> => {
+  const { url, token } = !admin
+    ? {
+        url: baseUrl,
+        token: adminToken,
+      }
+    : {
+        url: `${baseUrl}/instances/${id}`,
+        token: instanceToken,
+      };
+
+  const createProduct = async (
+    data: MerchantBackend.Products.ProductAddDetail
+  ): Promise<void> => {
     await request(`${url}/private/products`, {
-      method: 'post',
+      method: "post",
       token,
-      data
+      data,
     });
 
     await mutateAll(/@"\/private\/products"@/, null);
   };
 
-  const updateProduct = async (productId: string, data: 
MerchantBackend.Products.ProductPatchDetail): Promise<void> => {
+  const updateProduct = async (
+    productId: string,
+    data: MerchantBackend.Products.ProductPatchDetail
+  ): Promise<void> => {
     const r = await request(`${url}/private/products/${productId}`, {
-      method: 'patch',
+      method: "patch",
       token,
-      data
+      data,
     });
 
-    /**
-     * There is some inconsistency in how the cache is evicted.
-     * I'm keeping this for later inspection
-     */
-
-    // -- Clear all cache 
-    // -- This seems to work always but is bad
-
-    // const keys = [...cache.keys()]
-    // console.log(keys)
-    // cache.clear()
-    // await Promise.all(keys.map(k => trigger(k)))
-
-    // -- From the keys to the cache trigger
-    // -- An intermediate step 
-
-    // const keys = [
-    //   [`/private/products`, token, url],
-    //   [`/private/products/${productId}`, token, url],
-    // ]
-    // cache.clear()
-    // const f: string[][] = keys.map(k => cache.serializeKey(k))
-    // console.log(f)
-    // const m = flat(f)
-    // console.log(m)
-    // await Promise.all(m.map(k => trigger(k, true)))
-
-    // await Promise.all(keys.map(k => mutate(k)))
-
-    // -- This is how is supposed to be use  
-
-    // await mutate([`/private/products`, token, url])
-    // await mutate([`/private/products/${productId}`, token, url])
-
-    // await mutateAll(/@"\/private\/products"@/);
     await mutateAll(/@"\/private\/products\/.*"@/);
-    // return true
-    // return r
-
-    // -- FIXME: why this un-break the tests?
-    return Promise.resolve()
+    return Promise.resolve();
   };
 
   const deleteProduct = async (productId: string): Promise<void> => {
     await request(`${url}/private/products/${productId}`, {
-      method: 'delete',
+      method: "delete",
       token,
     });
 
     await mutateAll(/@"\/private\/products"@/);
   };
 
-  const lockProduct = async (productId: string, data: 
MerchantBackend.Products.LockRequest): Promise<void> => {
+  const lockProduct = async (
+    productId: string,
+    data: MerchantBackend.Products.LockRequest
+  ): Promise<void> => {
     await request(`${url}/private/products/${productId}/lock`, {
-      method: 'post',
+      method: "post",
       token,
-      data
+      data,
     });
 
     await mutateAll(/@"\/private\/products"@/);
@@ -122,19 +108,31 @@ export function useProductAPI(): ProductAPI {
   return { createProduct, updateProduct, deleteProduct, lockProduct };
 }
 
-
-export function useInstanceProducts(): 
HttpResponse<(MerchantBackend.Products.ProductDetail & WithId)[]> {
+export function useInstanceProducts(): HttpResponse<
+  (MerchantBackend.Products.ProductDetail & WithId)[]
+> {
   const { url: baseUrl, token: baseToken } = useBackendContext();
   const { token: instanceToken, id, admin } = useInstanceContext();
   // const { useSWR, useSWRInfinite } = useFetchContext();
 
-  const { url, token } = !admin ? {
-    url: baseUrl, token: baseToken
-  } : {
-    url: `${baseUrl}/instances/${id}`, token: instanceToken
-  };
-
-  const { data: list, error: listError, isValidating: listLoading } = 
useSWR<HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>, 
HttpError>([`/private/products`, token, url], fetcher, {
+  const { url, token } = !admin
+    ? {
+        url: baseUrl,
+        token: baseToken,
+      }
+    : {
+        url: `${baseUrl}/instances/${id}`,
+        token: instanceToken,
+      };
+
+  const {
+    data: list,
+    error: listError,
+    isValidating: listLoading,
+  } = useSWR<
+    HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>,
+    HttpError
+  >([`/private/products`, token, url], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
@@ -142,58 +140,84 @@ export function useInstanceProducts(): 
HttpResponse<(MerchantBackend.Products.Pr
     refreshWhenOffline: false,
   });
 
-  const { data: products, error: productError, setSize, size } = 
useSWRInfinite<HttpResponseOk<MerchantBackend.Products.ProductDetail>, 
HttpError>((pageIndex: number) => {
-    if (!list?.data || !list.data.products.length || listError || listLoading) 
return null
-    return [`/private/products/${list.data.products[pageIndex].product_id}`, 
token, url]
-  }, fetcher, {
-    revalidateAll: true,
-  })
+  const {
+    data: products,
+    error: productError,
+    setSize,
+    size,
+  } = useSWRInfinite<
+    HttpResponseOk<MerchantBackend.Products.ProductDetail>,
+    HttpError
+  >(
+    (pageIndex: number) => {
+      if (!list?.data || !list.data.products.length || listError || 
listLoading)
+        return null;
+      return [
+        `/private/products/${list.data.products[pageIndex].product_id}`,
+        token,
+        url,
+      ];
+    },
+    fetcher,
+    {
+      revalidateAll: true,
+    }
+  );
 
   useEffect(() => {
     if (list?.data && list.data.products.length > 0) {
-      setSize(list.data.products.length)
+      setSize(list.data.products.length);
     }
-  }, [list?.data.products.length, listLoading])
-
+  }, [list?.data.products.length, listLoading]);
 
-  if (listLoading) return { loading: true, data: [] }
-  if (listError) return listError
-  if (productError) return productError
+  if (listLoading) return { loading: true, data: [] };
+  if (listError) return listError;
+  if (productError) return productError;
   if (list?.data && list.data.products.length === 0) {
-    return { ok: true, data: [] }
+    return { ok: true, data: [] };
   }
   if (products) {
     const dataWithId = products.map((d) => {
       //take the id from the queried url
-      return ({ ...d.data, id: d.info?.url.replace(/.*\/private\/products\//, 
'') || '' })
-    })
-    return { ok: true, data: dataWithId }
+      return {
+        ...d.data,
+        id: d.info?.url.replace(/.*\/private\/products\//, "") || "",
+      };
+    });
+    return { ok: true, data: dataWithId };
   }
-  return { loading: true }
+  return { loading: true };
 }
 
-export function useProductDetails(productId: string): 
HttpResponse<MerchantBackend.Products.ProductDetail> {
+export function useProductDetails(
+  productId: string
+): HttpResponse<MerchantBackend.Products.ProductDetail> {
   const { url: baseUrl, token: baseToken } = useBackendContext();
   const { token: instanceToken, id, admin } = useInstanceContext();
 
-  const { url, token } = !admin ? {
-    url: baseUrl, token: baseToken
-  } : {
-    url: `${baseUrl}/instances/${id}`, token: instanceToken
-  };
-
-  const { data, error, isValidating } = 
useSWR<HttpResponseOk<MerchantBackend.Products.ProductDetail>, HttpError>(
-    [`/private/products/${productId}`, token, url], fetcher, {
+  const { url, token } = !admin
+    ? {
+        url: baseUrl,
+        token: baseToken,
+      }
+    : {
+        url: `${baseUrl}/instances/${id}`,
+        token: instanceToken,
+      };
+
+  const { data, error, isValidating } = useSWR<
+    HttpResponseOk<MerchantBackend.Products.ProductDetail>,
+    HttpError
+  >([`/private/products/${productId}`, token, url], fetcher, {
     refreshInterval: 0,
     refreshWhenHidden: false,
     revalidateOnFocus: false,
     revalidateOnReconnect: false,
     refreshWhenOffline: false,
-  }
-  )
+  });
 
-  if (isValidating) return { loading: true, data: data?.data }
-  if (data) return data
-  if (error) return error
-  return { loading: true }
+  if (isValidating) return { loading: true, data: data?.data };
+  if (data) return data;
+  if (error) return error;
+  return { loading: true };
 }
diff --git a/packages/merchant-backoffice/src/ApplicationReadyRoutes.tsx 
b/packages/merchant-backoffice/src/ApplicationReadyRoutes.tsx
index 994226a..ebc3d1d 100644
--- a/packages/merchant-backoffice/src/ApplicationReadyRoutes.tsx
+++ b/packages/merchant-backoffice/src/ApplicationReadyRoutes.tsx
@@ -15,86 +15,106 @@
  */
 
 /**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-import { Fragment, h, VNode } from 'preact';
-import Router, { Route, route } from 'preact-router';
-import { useBackendContext } from './context/backend';
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { Fragment, h, VNode } from "preact";
+import Router, { Route, route } from "preact-router";
+import { useBackendContext } from "./context/backend";
 import { useBackendInstancesTestForAdmin } from "./hooks/backend";
 import { InstanceRoutes } from "./InstanceRoutes";
-import LoginPage from './paths/login';
-import { INSTANCE_ID_LOOKUP } from './utils/constants';
-import { NotYetReadyAppMenu, Menu, NotificationCard } from './components/menu';
-import { useTranslator } from './i18n';
-import { createHashHistory } from 'history';
-import { useState } from 'preact/hooks';
+import LoginPage from "./paths/login";
+import { INSTANCE_ID_LOOKUP } from "./utils/constants";
+import { NotYetReadyAppMenu, Menu, NotificationCard } from "./components/menu";
+import { useTranslator } from "./i18n";
+import { createHashHistory } from "history";
+import { useState } from "preact/hooks";
 
 export function ApplicationReadyRoutes(): VNode {
   const i18n = useTranslator();
-  const { url: backendURL, updateLoginStatus, clearAllTokens } = 
useBackendContext();
+  const {
+    url: backendURL,
+    updateLoginStatus,
+    clearAllTokens,
+  } = useBackendContext();
 
-  const result = useBackendInstancesTestForAdmin()
+  const result = useBackendInstancesTestForAdmin();
 
   const clearTokenAndGoToRoot = () => {
     clearAllTokens();
-    route('/')
-  }
+    route("/");
+  };
 
   if (result.clientError && result.isUnauthorized) {
-    return <Fragment>
-      <NotYetReadyAppMenu title="Login" onLogout={clearTokenAndGoToRoot} />
-      <NotificationCard notification={{
-        message: i18n`Access denied`,
-        description: i18n`Check your token is valid`,
-        type: 'ERROR'
-      }}
-      />
-      <LoginPage onConfirm={updateLoginStatus} />
-    </Fragment>
+    return (
+      <Fragment>
+        <NotYetReadyAppMenu title="Login" onLogout={clearTokenAndGoToRoot} />
+        <NotificationCard
+          notification={{
+            message: i18n`Access denied`,
+            description: i18n`Check your token is valid`,
+            type: "ERROR",
+          }}
+        />
+        <LoginPage onConfirm={updateLoginStatus} />
+      </Fragment>
+    );
   }
 
-
-  if (result.loading) return <NotYetReadyAppMenu title="Loading..." />
+  if (result.loading) return <NotYetReadyAppMenu title="Loading..." />;
 
   let admin = true;
   let instanceNameByBackendURL;
 
   if (!result.ok) {
-    const path = new URL(backendURL).pathname
-    const match = INSTANCE_ID_LOOKUP.exec(path)
+    const path = new URL(backendURL).pathname;
+    const match = INSTANCE_ID_LOOKUP.exec(path);
     if (!match || !match[1]) {
       // this should be rare because
       // query to /config is ok but the URL
       // does not match our pattern
-      return <Fragment>
-        <NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} />
-        <NotificationCard notification={{
-          message: i18n`Couldn't access the server.`,
-          description: i18n`Could not infer instance id from url 
${backendURL}`,
-          type: 'ERROR',
-        }}
-        />
-        <LoginPage onConfirm={updateLoginStatus} />
-      </Fragment>
+      return (
+        <Fragment>
+          <NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} />
+          <NotificationCard
+            notification={{
+              message: i18n`Couldn't access the server.`,
+              description: i18n`Could not infer instance id from url 
${backendURL}`,
+              type: "ERROR",
+            }}
+          />
+          <LoginPage onConfirm={updateLoginStatus} />
+        </Fragment>
+      );
     }
 
-    admin = false
-    instanceNameByBackendURL = match[1]
+    admin = false;
+    instanceNameByBackendURL = match[1];
   }
 
-  const history = createHashHistory()
-  return <Router history={history}>
-    <Route default component={DefaultMainRoute} 
clearTokenAndGoToRoot={clearTokenAndGoToRoot} admin={admin} 
instanceNameByBackendURL={instanceNameByBackendURL} />
-  </Router>
+  const history = createHashHistory();
+  return (
+    <Router history={history}>
+      <Route
+        default
+        component={DefaultMainRoute}
+        admin={admin}
+        instanceNameByBackendURL={instanceNameByBackendURL}
+      />
+    </Router>
+  );
 }
 
-function DefaultMainRoute({ clearTokenAndGoToRoot, instance, admin, 
instanceNameByBackendURL }: any) {
-  const [instanceName, setInstanceName] = useState(instanceNameByBackendURL || 
instance || 'default')
-
-  return <Fragment>
-    <Menu instance={instanceName} admin={admin} 
onLogout={clearTokenAndGoToRoot} setInstanceName={setInstanceName} />
-    <InstanceRoutes admin={admin} id={instanceName} 
setInstanceName={setInstanceName} />
-  </Fragment>
-
+function DefaultMainRoute({ instance, admin, instanceNameByBackendURL }: any) {
+  const [instanceName, setInstanceName] = useState(
+    instanceNameByBackendURL || instance || "default"
+  );
+
+  return (
+    <InstanceRoutes
+      admin={admin}
+      id={instanceName}
+      setInstanceName={setInstanceName}
+    />
+  );
 }
diff --git a/packages/merchant-backoffice/src/InstanceRoutes.tsx 
b/packages/merchant-backoffice/src/InstanceRoutes.tsx
index 20a7601..06f1db1 100644
--- a/packages/merchant-backoffice/src/InstanceRoutes.tsx
+++ b/packages/merchant-backoffice/src/InstanceRoutes.tsx
@@ -23,12 +23,16 @@ import { Fragment, FunctionComponent, h, VNode } from 
"preact";
 import { Route, route, Router } from "preact-router";
 import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
 import { Loading } from "./components/exception/loading";
-import { NotificationCard } from "./components/menu";
+import { Menu, NotificationCard } from "./components/menu";
 import { useBackendContext } from "./context/backend";
 import { InstanceContextProvider } from "./context/instance";
-import { useBackendDefaultToken, useBackendInstanceToken } from "./hooks";
+import {
+  useBackendDefaultToken,
+  useBackendInstanceToken,
+  useLocalStorage,
+} from "./hooks";
 import { HttpError } from "./hooks/backend";
-import { useTranslator } from "./i18n";
+import { Translate, useTranslator } from "./i18n";
 import InstanceCreatePage from "./paths/admin/create";
 import InstanceListPage from "./paths/admin/list";
 import OrderCreatePage from "./paths/instance/orders/create";
@@ -42,6 +46,7 @@ import TransferCreatePage from 
"./paths/instance/transfers/create";
 import ReservesCreatePage from "./paths/instance/reserves/create";
 import ReservesDetailsPage from "./paths/instance/reserves/details";
 import ReservesListPage from "./paths/instance/reserves/list";
+import ListKYCPage from "./paths/instance/kyc/list";
 import InstanceUpdatePage, {
   Props as InstanceUpdatePageProps,
   AdminUpdate as InstanceAdminUpdatePage,
@@ -49,6 +54,8 @@ import InstanceUpdatePage, {
 import LoginPage from "./paths/login";
 import NotFoundPage from "./paths/notfound";
 import { Notification } from "./utils/types";
+import { useInstanceKYCDetails } from "./hooks/instance";
+import { format } from "date-fns";
 
 export enum InstancePaths {
   // details = '/',
@@ -67,6 +74,8 @@ export enum InstancePaths {
   reserves_details = "/reserves/:rid/details",
   reserves_new = "/reserves/new",
 
+  kyc = "/kyc",
+
   transfers_list = "/transfers",
   transfers_new = "/transfer/new",
 }
@@ -89,15 +98,19 @@ export interface Props {
 export function InstanceRoutes({ id, admin, setInstanceName }: Props): VNode {
   const [_, updateDefaultToken] = useBackendDefaultToken();
   const [token, updateToken] = useBackendInstanceToken(id);
-  const { updateLoginStatus: changeBackend, addTokenCleaner } =
-    useBackendContext();
+  const {
+    updateLoginStatus: changeBackend,
+    addTokenCleaner,
+    clearAllTokens,
+  } = useBackendContext();
   const cleaner = useCallback(() => {
     updateToken(undefined);
   }, [id]);
   const i18n = useTranslator();
-  const [globalNotification, setGlobalNotification] = useState<
-    (Notification & { to: string }) | undefined
-  >(undefined);
+
+  type GlobalNotifState = (Notification & { to: string }) | undefined;
+  const [globalNotification, setGlobalNotification] =
+    useState<GlobalNotifState>(undefined);
 
   useEffect(() => {
     addTokenCleaner(cleaner);
@@ -178,8 +191,20 @@ export function InstanceRoutes({ id, admin, 
setInstanceName }: Props): VNode {
     };
   }
 
+  const clearTokenAndGoToRoot = () => {
+    clearAllTokens();
+    route("/");
+  };
+
   return (
     <InstanceContextProvider value={value}>
+      <Menu
+        instance={id}
+        admin={admin}
+        onLogout={clearTokenAndGoToRoot}
+        setInstanceName={setInstanceName}
+      />
+      <KycBanner />
       <NotificationCard notification={globalNotification} />
 
       <Router
@@ -395,6 +420,8 @@ export function InstanceRoutes({ id, admin, setInstanceName 
}: Props): VNode {
             route(InstancePaths.reserves_list);
           }}
         />
+
+        <Route path={InstancePaths.kyc} component={ListKYCPage} />
         {/**
          * Example pages
          */}
@@ -469,3 +496,33 @@ function AdminInstanceUpdatePage({
     </InstanceContextProvider>
   );
 }
+
+function KycBanner(): VNode {
+  const kycStatus = useInstanceKYCDetails();
+  const today = format(new Date(), "yyyy-MM-dd");
+  const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide");
+  const hasBeenHidden = today === lastHide;
+  const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect";
+  if (hasBeenHidden || !needsToBeShown) return <Fragment />;
+  return (
+    <NotificationCard
+      notification={{
+        type: "WARN",
+        message: "KYC verification needed",
+        description: (
+          <div>
+            <p>
+              Some transfer are on hold until a KYC process is completed. Go to
+              the KYC section in the left panel for more information
+            </p>
+            <div class="buttons is-right">
+              <button class="button" onClick={() => setLastHide(today)}>
+                <Translate>Hide for today</Translate>
+              </button>
+            </div>
+          </div>
+        ),
+      }}
+    />
+  );
+}
diff --git a/packages/merchant-backoffice/src/components/menu/SideBar.tsx 
b/packages/merchant-backoffice/src/components/menu/SideBar.tsx
index d579736..231ac7d 100644
--- a/packages/merchant-backoffice/src/components/menu/SideBar.tsx
+++ b/packages/merchant-backoffice/src/components/menu/SideBar.tsx
@@ -15,18 +15,18 @@
  */
 
 /**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
-
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
 
-import { Fragment, h, VNode } from 'preact';
-import { useCallback } from 'preact/hooks';
-import { useBackendContext } from '../../context/backend';
-import { useConfigContext } from '../../context/config';
-import { useInstanceContext } from '../../context/instance';
-import { Translate } from '../../i18n';
-import { LangSelector } from './LangSelector';
+import { Fragment, h, VNode } from "preact";
+import { useCallback } from "preact/hooks";
+import { useBackendContext } from "../../context/backend";
+import { useConfigContext } from "../../context/config";
+import { useInstanceContext } from "../../context/instance";
+import { useInstanceKYCDetails } from "../../hooks/instance";
+import { Translate } from "../../i18n";
+import { LangSelector } from "./LangSelector";
 
 interface Props {
   onLogout: () => void;
@@ -36,26 +36,48 @@ interface Props {
   mimic?: boolean;
 }
 
-export function Sidebar({ mobile, instance, onLogout, admin, mimic }: Props): 
VNode {
+export function Sidebar({
+  mobile,
+  instance,
+  onLogout,
+  admin,
+  mimic,
+}: Props): VNode {
   const config = useConfigContext();
   const backend = useBackendContext();
 
+  const kycStatus = useInstanceKYCDetails();
+  const needKYC = kycStatus.ok && kycStatus.data.type === "redirect";
   // const withInstanceIdIfNeeded = useCallback(function (path: string) {
   //   if (mimic) {
   //     return path + '?instance=' + instance
   //   }
   //   return path
   // },[instance])
-  
+
   return (
     <aside class="aside is-placed-left is-expanded">
-      { mobile && <div class="footer" onClick={(e) => { return 
e.stopImmediatePropagation() }}>
-        <LangSelector />
-      </div>}
+      {mobile && (
+        <div
+          class="footer"
+          onClick={(e) => {
+            return e.stopImmediatePropagation();
+          }}
+        >
+          <LangSelector />
+        </div>
+      )}
       <div class="aside-tools">
         <div class="aside-tools-label">
-          <div><b>Taler</b> Backoffice</div>
-          <div class="is-size-7 has-text-right" style={{ lineHeight: 0, 
marginTop: -10 }}>{process.env.__VERSION__} ({config.version})</div>
+          <div>
+            <b>Taler</b> Backoffice
+          </div>
+          <div
+            class="is-size-7 has-text-right"
+            style={{ lineHeight: 0, marginTop: -10 }}
+          >
+            {process.env.__VERSION__} ({config.version})
+          </div>
         </div>
       </div>
       <div class="menu is-menu-main">
@@ -64,79 +86,134 @@ export function Sidebar({ mobile, instance, onLogout, 
admin, mimic }: Props): VN
         </p>
         <ul class="menu-list">
           <li>
-            <a href={("/update")} class="has-icon">
-              <span class="icon"><i class="mdi mdi-square-edit-outline" 
/></span>
-              <span 
class="menu-item-label"><Translate>Settings</Translate></span>
+            <a href={"/update"} class="has-icon">
+              <span class="icon">
+                <i class="mdi mdi-square-edit-outline" />
+              </span>
+              <span class="menu-item-label">
+                <Translate>Settings</Translate>
+              </span>
             </a>
           </li>
           <li>
-            <a href={("/orders")} class="has-icon">
-              <span class="icon"><i class="mdi mdi-cash-register" /></span>
-              <span 
class="menu-item-label"><Translate>Orders</Translate></span>
+            <a href={"/orders"} class="has-icon">
+              <span class="icon">
+                <i class="mdi mdi-cash-register" />
+              </span>
+              <span class="menu-item-label">
+                <Translate>Orders</Translate>
+              </span>
             </a>
           </li>
           <li>
-            <a href={("/products")} class="has-icon">
-              <span class="icon"><i class="mdi mdi-shopping" /></span>
-              <span 
class="menu-item-label"><Translate>Products</Translate></span>
+            <a href={"/products"} class="has-icon">
+              <span class="icon">
+                <i class="mdi mdi-shopping" />
+              </span>
+              <span class="menu-item-label">
+                <Translate>Products</Translate>
+              </span>
             </a>
           </li>
           <li>
-            <a href={("/transfers")} class="has-icon">
-              <span class="icon"><i class="mdi mdi-bank" /></span>
-              <span 
class="menu-item-label"><Translate>Transfers</Translate></span>
+            <a href={"/transfers"} class="has-icon">
+              <span class="icon">
+                <i class="mdi mdi-bank" />
+              </span>
+              <span class="menu-item-label">
+                <Translate>Transfers</Translate>
+              </span>
             </a>
           </li>
           <li>
-            <a href={("/reserves")} class="has-icon">
-              <span class="icon"><i class="mdi mdi-cash" /></span>
+            <a href={"/reserves"} class="has-icon">
+              <span class="icon">
+                <i class="mdi mdi-cash" />
+              </span>
               <span class="menu-item-label">Reserves</span>
             </a>
           </li>
+          {needKYC && (
+            <li>
+              <a href={"/kyc"} class="has-icon">
+                <span class="icon">
+                  <i class="mdi mdi-account-check" />
+                </span>
+                <span class="menu-item-label">KYC Status</span>
+              </a>
+            </li>
+          )}
         </ul>
-        <p class="menu-label"><Translate>Connection</Translate></p>
+        <p class="menu-label">
+          <Translate>Connection</Translate>
+        </p>
         <ul class="menu-list">
           <li>
             <div>
-              <span style={{ width: '3rem' }} class="icon"><i class="mdi 
mdi-currency-eur" /></span>
+              <span style={{ width: "3rem" }} class="icon">
+                <i class="mdi mdi-currency-eur" />
+              </span>
               <span class="menu-item-label">{config.currency}</span>
             </div>
           </li>
           <li>
-            <div >
-              <span style={{ width: '3rem' }} class="icon"><i class="mdi 
mdi-web" /></span>
+            <div>
+              <span style={{ width: "3rem" }} class="icon">
+                <i class="mdi mdi-web" />
+              </span>
               <span class="menu-item-label">
                 {new URL(backend.url).hostname}
               </span>
             </div>
           </li>
           <li>
-            <div >
-              <span style={{ width: '3rem' }} class="icon">ID</span>
+            <div>
+              <span style={{ width: "3rem" }} class="icon">
+                ID
+              </span>
               <span class="menu-item-label">
                 {!instance ? "default" : instance}
               </span>
             </div>
           </li>
-          {admin && !mimic && <Fragment>
-            <p class="menu-label"><Translate>Instances</Translate></p>
-            <li>
-              <a href={("/instance/new")} class="has-icon">
-                <span class="icon"><i class="mdi mdi-plus" /></span>
-                <span class="menu-item-label"><Translate>New</Translate></span>
-              </a>
-            </li>
-            <li>
-              <a href={("/instances")} class="has-icon">
-                <span class="icon"><i class="mdi mdi-format-list-bulleted" 
/></span>
-                <span 
class="menu-item-label"><Translate>List</Translate></span>
-              </a>
-            </li>
-          </Fragment>}
+          {admin && !mimic && (
+            <Fragment>
+              <p class="menu-label">
+                <Translate>Instances</Translate>
+              </p>
+              <li>
+                <a href={"/instance/new"} class="has-icon">
+                  <span class="icon">
+                    <i class="mdi mdi-plus" />
+                  </span>
+                  <span class="menu-item-label">
+                    <Translate>New</Translate>
+                  </span>
+                </a>
+              </li>
+              <li>
+                <a href={"/instances"} class="has-icon">
+                  <span class="icon">
+                    <i class="mdi mdi-format-list-bulleted" />
+                  </span>
+                  <span class="menu-item-label">
+                    <Translate>List</Translate>
+                  </span>
+                </a>
+              </li>
+            </Fragment>
+          )}
           <li>
-            <a class="has-icon is-state-info is-hoverable" onClick={(): void 
=> onLogout()}>
-              <span class="icon"><i class="mdi mdi-logout default" /></span>
-              <span class="menu-item-label"><Translate>Log 
out</Translate></span>
+            <a
+              class="has-icon is-state-info is-hoverable"
+              onClick={(): void => onLogout()}
+            >
+              <span class="icon">
+                <i class="mdi mdi-logout default" />
+              </span>
+              <span class="menu-item-label">
+                <Translate>Log out</Translate>
+              </span>
             </a>
           </li>
         </ul>
@@ -144,4 +221,3 @@ export function Sidebar({ mobile, instance, onLogout, 
admin, mimic }: Props): VN
     </aside>
   );
 }
-
diff --git a/packages/merchant-backoffice/src/declaration.d.ts 
b/packages/merchant-backoffice/src/declaration.d.ts
index e5486de..35b80c6 100644
--- a/packages/merchant-backoffice/src/declaration.d.ts
+++ b/packages/merchant-backoffice/src/declaration.d.ts
@@ -388,6 +388,45 @@ export namespace MerchantBackend {
 
         }
 
+        //GET /private/instances/$INSTANCE/kyc
+        interface AccountKycRedirects {
+            // Array of pending KYCs.
+            pending_kycs: MerchantAccountKycRedirect[];
+
+            // Array of exchanges with no reply.
+            timeout_kycs: ExchangeKycTimeout[];
+
+        }
+        interface MerchantAccountKycRedirect {
+
+            // URL that the user should open in a browser to
+            // proceed with the KYC process (as returned
+            // by the exchange's /kyc-check/ endpoint).
+            kyc_url: string;
+
+            // Base URL of the exchange this is about.
+            exchange_url: string;
+
+            // Our bank wire account this is about.
+            payto_uri: string;
+
+        }
+        interface ExchangeKycTimeout {
+
+            // Base URL of the exchange this is about.
+            exchange_url: string;
+
+            // Numeric error code indicating errors the exchange
+            // returned, or TALER_EC_INVALID for none.
+            exchange_code: number;
+
+            // HTTP status code returned by the exchange when we asked for
+            // information about the KYC status.
+            // 0 if there was no response at all.
+            exchange_http_status: number;
+
+        }
+
         //GET /private/instances/$INSTANCE
         interface QueryInstancesResponse {
             // The URI where the wallet will send coins.  A merchant may have
@@ -433,6 +472,7 @@ export namespace MerchantBackend {
             // Does not contain the token when token auth is configured.
             auth: {
                 method: "external" | "token";
+                token?: string;
             };
         }
 
diff --git a/packages/merchant-backoffice/src/hooks/admin.ts 
b/packages/merchant-backoffice/src/hooks/admin.ts
deleted file mode 100644
index 1ac9e69..0000000
--- a/packages/merchant-backoffice/src/hooks/admin.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
- */
-import { MerchantBackend } from "../declaration";
-import { useBackendContext } from "../context/backend";
-import { request, useMatchMutate } from "./backend";
-
-export function useAdminAPI(): AdminAPI {
-  const { url, token } = useBackendContext();
-  const mutateAll = useMatchMutate();
-
-  const createInstance = async (
-    instance: MerchantBackend.Instances.InstanceConfigurationMessage
-  ): Promise<void> => {
-    await request(`${url}/management/instances`, {
-      method: "post",
-      token,
-      data: instance,
-    });
-
-    mutateAll(/@"\/private\/instances"@/);
-  };
-
-  const deleteInstance = async (id: string): Promise<void> => {
-    await request(`${url}/management/instances/${id}`, {
-      method: "delete",
-      token,
-    });
-
-    mutateAll(/@"\/private\/instances"@/);
-  };
-
-  const purgeInstance = async (id: string): Promise<void> => {
-    await request(`${url}/management/instances/${id}?purge=YES`, {
-      method: "delete",
-      token,
-    });
-
-    mutateAll(/@"\/private\/instances"@/);
-  };
-
-  return { createInstance, deleteInstance, purgeInstance };
-}
-
-export interface AdminAPI {
-  createInstance: (
-    data: MerchantBackend.Instances.InstanceConfigurationMessage
-  ) => Promise<void>;
-  deleteInstance: (id: string) => Promise<void>;
-  purgeInstance: (id: string) => Promise<void>;
-}
diff --git a/packages/merchant-backoffice/src/hooks/backend.ts 
b/packages/merchant-backoffice/src/hooks/backend.ts
index 1b27cfe..789cfc8 100644
--- a/packages/merchant-backoffice/src/hooks/backend.ts
+++ b/packages/merchant-backoffice/src/hooks/backend.ts
@@ -66,6 +66,7 @@ export interface RequestInfo {
   hasToken: boolean;
   params: unknown;
   data: unknown;
+  status: number;
 }
 
 interface HttpResponseLoading<T> {
@@ -163,6 +164,7 @@ function buildRequestOk<T>(
       data: res.config.data,
       url,
       hasToken,
+      status: res.status,
     },
   };
 }
@@ -187,6 +189,7 @@ function buildRequestFailed(
     params: ex.request?.params,
     url,
     hasToken,
+    status: status || 0,
   };
 
   if (status && status >= 400 && status < 500) {
diff --git a/packages/merchant-backoffice/src/hooks/instance.ts 
b/packages/merchant-backoffice/src/hooks/instance.ts
index 2f4923e..9153e19 100644
--- a/packages/merchant-backoffice/src/hooks/instance.ts
+++ b/packages/merchant-backoffice/src/hooks/instance.ts
@@ -13,18 +13,18 @@
  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 { MerchantBackend } from "../declaration";
+import useSWR, { useSWRConfig } from "swr";
 import { useBackendContext } from "../context/backend";
+import { useInstanceContext } from "../context/instance";
+import { MerchantBackend } from "../declaration";
 import {
   fetcher,
   HttpError,
   HttpResponse,
   HttpResponseOk,
   request,
-  SwrError,
+  useMatchMutate,
 } from "./backend";
-import useSWR, { useSWRConfig } from "swr";
-import { useInstanceContext } from "../context/instance";
 
 interface InstanceAPI {
   updateInstance: (
@@ -35,8 +35,56 @@ interface InstanceAPI {
   setNewToken: (token: string) => Promise<void>;
 }
 
+export function useAdminAPI(): AdminAPI {
+  const { url, token } = useBackendContext();
+  const mutateAll = useMatchMutate();
+
+  const createInstance = async (
+    instance: MerchantBackend.Instances.InstanceConfigurationMessage
+  ): Promise<void> => {
+    await request(`${url}/management/instances`, {
+      method: "post",
+      token,
+      data: instance,
+    });
+
+    mutateAll(/\/management\/instances/);
+  };
+
+  const deleteInstance = async (id: string): Promise<void> => {
+    await request(`${url}/management/instances/${id}`, {
+      method: "delete",
+      token,
+    });
+
+    mutateAll(/\/management\/instances/);
+  };
+
+  const purgeInstance = async (id: string): Promise<void> => {
+    await request(`${url}/management/instances/${id}`, {
+      method: "delete",
+      token,
+      params: {
+        purge: "YES",
+      },
+    });
+
+    mutateAll(/\/management\/instances/);
+  };
+
+  return { createInstance, deleteInstance, purgeInstance };
+}
+
+export interface AdminAPI {
+  createInstance: (
+    data: MerchantBackend.Instances.InstanceConfigurationMessage
+  ) => Promise<void>;
+  deleteInstance: (id: string) => Promise<void>;
+  purgeInstance: (id: string) => Promise<void>;
+}
+
 export function useManagementAPI(instanceId: string): InstanceAPI {
-  const { mutate } = useSWRConfig();
+  const mutateAll = useMatchMutate();
   const { url, token } = useBackendContext();
 
   const updateInstance = async (
@@ -48,7 +96,7 @@ export function useManagementAPI(instanceId: string): 
InstanceAPI {
       data: instance,
     });
 
-    mutate([`/private/`, token, url], null);
+    mutateAll(/\/management\/instances/);
   };
 
   const deleteInstance = async (): Promise<void> => {
@@ -57,7 +105,7 @@ export function useManagementAPI(instanceId: string): 
InstanceAPI {
       token,
     });
 
-    mutate([`/private/`, token, url], null);
+    mutateAll(/\/management\/instances/);
   };
 
   const clearToken = async (): Promise<void> => {
@@ -67,7 +115,7 @@ export function useManagementAPI(instanceId: string): 
InstanceAPI {
       data: { method: "external" },
     });
 
-    mutate([`/private/`, token, url], null);
+    mutateAll(/\/management\/instances/);
   };
 
   const setNewToken = async (newToken: string): Promise<void> => {
@@ -77,7 +125,7 @@ export function useManagementAPI(instanceId: string): 
InstanceAPI {
       data: { method: "token", token: newToken },
     });
 
-    mutate([`/private/`, token, url], null);
+    mutateAll(/\/management\/instances/);
   };
 
   return { updateInstance, deleteInstance, setNewToken, clearToken };
@@ -89,14 +137,8 @@ export function useInstanceAPI(): InstanceAPI {
   const { token: instanceToken, id, admin } = useInstanceContext();
 
   const { url, token } = !admin
-    ? {
-        url: baseUrl,
-        token: adminToken,
-      }
-    : {
-        url: `${baseUrl}/instances/${id}`,
-        token: instanceToken,
-      };
+    ? { url: baseUrl, token: adminToken }
+    : { url: `${baseUrl}/instances/${id}`, token: instanceToken };
 
   const updateInstance = async (
     instance: MerchantBackend.Instances.InstanceReconfigurationMessage
@@ -149,14 +191,8 @@ export function useInstanceDetails(): 
HttpResponse<MerchantBackend.Instances.Que
   const { token: instanceToken, id, admin } = useInstanceContext();
 
   const { url, token } = !admin
-    ? {
-        url: baseUrl,
-        token: baseToken,
-      }
-    : {
-        url: `${baseUrl}/instances/${id}`,
-        token: instanceToken,
-      };
+    ? { url: baseUrl, token: baseToken }
+    : { url: `${baseUrl}/instances/${id}`, token: instanceToken };
 
   const { data, error, isValidating } = useSWR<
     HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
@@ -178,6 +214,41 @@ export function useInstanceDetails(): 
HttpResponse<MerchantBackend.Instances.Que
   return { loading: true };
 }
 
+type KYCStatus =
+  | { type: "ok" }
+  | { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects 
};
+
+export function useInstanceKYCDetails(): HttpResponse<KYCStatus> {
+  const { url: baseUrl, token: baseToken } = useBackendContext();
+  const { token: instanceToken, id, admin } = useInstanceContext();
+
+  const { url, token } = !admin
+    ? { url: baseUrl, token: baseToken }
+    : { url: `${baseUrl}/instances/${id}`, token: instanceToken };
+
+  const { data, error } = useSWR<
+    HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>,
+    HttpError
+  >([`/private/kyc`, token, url], fetcher, {
+    refreshInterval: 5000,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+    errorRetryCount: 0,
+    errorRetryInterval: 1,
+    shouldRetryOnError: false,
+  });
+
+  if (data) {
+    if (data.info?.status === 202)
+      return { ok: true, data: { type: "redirect", status: data.data } };
+    return { ok: true, data: { type: "ok" } };
+  }
+  if (error) return error;
+  return { loading: true };
+}
+
 export function useManagedInstanceDetails(
   instanceId: string
 ): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
diff --git a/packages/merchant-backoffice/src/paths/admin/create/index.tsx 
b/packages/merchant-backoffice/src/paths/admin/create/index.tsx
index 3f31b3d..aaed6d6 100644
--- a/packages/merchant-backoffice/src/paths/admin/create/index.tsx
+++ b/packages/merchant-backoffice/src/paths/admin/create/index.tsx
@@ -21,7 +21,7 @@ import { Fragment, h, VNode } from "preact";
 import { useState } from "preact/hooks";
 import { NotificationCard } from "../../../components/menu";
 import { MerchantBackend } from "../../../declaration";
-import { useAdminAPI } from "../../../hooks/admin";
+import { useAdminAPI } from "../../../hooks/instance";
 import { useTranslator } from "../../../i18n";
 import { Notification } from "../../../utils/types";
 import { CreatePage } from "./CreatePage";
diff --git a/packages/merchant-backoffice/src/paths/admin/list/index.tsx 
b/packages/merchant-backoffice/src/paths/admin/list/index.tsx
index f762a07..c5609fd 100644
--- a/packages/merchant-backoffice/src/paths/admin/list/index.tsx
+++ b/packages/merchant-backoffice/src/paths/admin/list/index.tsx
@@ -15,22 +15,21 @@
  */
 
 /**
-*
-* @author Sebastian Javier Marchano (sebasjm)
-*/
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
 
-import { Fragment, h, VNode } from 'preact';
-import { useState } from 'preact/hooks';
-import { Loading } from '../../../components/exception/loading';
-import { NotificationCard } from '../../../components/menu';
-import { DeleteModal, PurgeModal } from '../../../components/modal';
-import { MerchantBackend } from '../../../declaration';
-import { useAdminAPI } from "../../../hooks/admin";
-import { HttpError } from '../../../hooks/backend';
-import { useBackendInstances } from '../../../hooks/instance';
-import { useTranslator } from '../../../i18n';
-import { Notification } from '../../../utils/types';
-import { View } from './View';
+import { Fragment, h, VNode } from "preact";
+import { useState } from "preact/hooks";
+import { Loading } from "../../../components/exception/loading";
+import { NotificationCard } from "../../../components/menu";
+import { DeleteModal, PurgeModal } from "../../../components/modal";
+import { MerchantBackend } from "../../../declaration";
+import { HttpError } from "../../../hooks/backend";
+import { useAdminAPI, useBackendInstances } from "../../../hooks/instance";
+import { useTranslator } from "../../../i18n";
+import { Notification } from "../../../utils/types";
+import { View } from "./View";
 
 interface Props {
   onCreate: () => void;
@@ -39,73 +38,89 @@ interface Props {
   onUnauthorized: () => VNode;
   onNotFound: () => VNode;
   onLoadError: (error: HttpError) => VNode;
-  setInstanceName: (s:string) => void;
+  setInstanceName: (s: string) => void;
 }
 
-export default function Instances({ onUnauthorized, onLoadError, onNotFound, 
onCreate, onUpdate, setInstanceName }: Props): VNode {
-  const result = useBackendInstances()
-  const [deleting, setDeleting] = useState<MerchantBackend.Instances.Instance 
| null>(null)
-  const [purging, setPurging] = useState<MerchantBackend.Instances.Instance | 
null>(null)
-  const { deleteInstance, purgeInstance } = useAdminAPI()
-  const [notif, setNotif] = useState<Notification | undefined>(undefined)
-  const i18n = useTranslator()
+export default function Instances({
+  onUnauthorized,
+  onLoadError,
+  onNotFound,
+  onCreate,
+  onUpdate,
+  setInstanceName,
+}: Props): VNode {
+  const result = useBackendInstances();
+  const [deleting, setDeleting] =
+    useState<MerchantBackend.Instances.Instance | null>(null);
+  const [purging, setPurging] =
+    useState<MerchantBackend.Instances.Instance | null>(null);
+  const { deleteInstance, purgeInstance } = useAdminAPI();
+  const [notif, setNotif] = useState<Notification | undefined>(undefined);
+  const i18n = useTranslator();
 
-  if (result.clientError && result.isUnauthorized) return onUnauthorized()
-  if (result.clientError && result.isNotfound) return onNotFound()
-  if (result.loading) return <Loading />
-  if (!result.ok) return onLoadError(result)
+  if (result.clientError && result.isUnauthorized) return onUnauthorized();
+  if (result.clientError && result.isNotfound) return onNotFound();
+  if (result.loading) return <Loading />;
+  if (!result.ok) return onLoadError(result);
 
-  return <Fragment>
-    <NotificationCard notification={notif} />
-    <View instances={result.data.instances}
-      onDelete={setDeleting}
-      onCreate={onCreate}
-      onPurge={setPurging}
-      onUpdate={onUpdate}
-      setInstanceName={setInstanceName}
-      selected={!!deleting}
-    />
-    {deleting && <DeleteModal
-      element={deleting}
-      onCancel={() => setDeleting(null)}
-      onConfirm={async (): Promise<void> => {
-        try {
-          await deleteInstance(deleting.id)
-          // pushNotification({ message: 'delete_success', type: 'SUCCESS' })
-          setNotif({
-            message: i18n`Instance "${deleting.name}" (ID: ${deleting.id}) has 
been deleted`,
-            type: 'SUCCESS'
-          })
-        } catch (error) {
-          setNotif({
-            message: i18n`Failed to delete instance`,
-            type: "ERROR",
-            description: error instanceof Error ? error.message : undefined
-          })
-          // pushNotification({ message: 'delete_error', type: 'ERROR' })
-        }
-        setDeleting(null)
-      }}
-    />}
-    {purging && <PurgeModal
-      element={purging}
-      onCancel={() => setPurging(null)}
-      onConfirm={async (): Promise<void> => {
-        try {
-          await purgeInstance(purging.id)
-          setNotif({
-            message: i18n`Instance "${purging.name}" (ID: ${purging.id}) has 
been disabled`,
-            type: 'SUCCESS'
-          })
-        } catch (error) {
-          setNotif({
-            message: i18n`Failed to purge instance`,
-            type: "ERROR",
-            description: error instanceof Error ? error.message : undefined
-          })
-        }
-        setPurging(null)
-      }}
-    />}
-  </Fragment>;
+  return (
+    <Fragment>
+      <NotificationCard notification={notif} />
+      <View
+        instances={result.data.instances}
+        onDelete={setDeleting}
+        onCreate={onCreate}
+        onPurge={setPurging}
+        onUpdate={onUpdate}
+        setInstanceName={setInstanceName}
+        selected={!!deleting}
+      />
+      {deleting && (
+        <DeleteModal
+          element={deleting}
+          onCancel={() => setDeleting(null)}
+          onConfirm={async (): Promise<void> => {
+            try {
+              await deleteInstance(deleting.id);
+              // pushNotification({ message: 'delete_success', type: 'SUCCESS' 
})
+              setNotif({
+                message: i18n`Instance "${deleting.name}" (ID: ${deleting.id}) 
has been deleted`,
+                type: "SUCCESS",
+              });
+            } catch (error) {
+              setNotif({
+                message: i18n`Failed to delete instance`,
+                type: "ERROR",
+                description: error instanceof Error ? error.message : 
undefined,
+              });
+              // pushNotification({ message: 'delete_error', type: 'ERROR' })
+            }
+            setDeleting(null);
+          }}
+        />
+      )}
+      {purging && (
+        <PurgeModal
+          element={purging}
+          onCancel={() => setPurging(null)}
+          onConfirm={async (): Promise<void> => {
+            try {
+              await purgeInstance(purging.id);
+              setNotif({
+                message: i18n`Instance "${purging.name}" (ID: ${purging.id}) 
has been disabled`,
+                type: "SUCCESS",
+              });
+            } catch (error) {
+              setNotif({
+                message: i18n`Failed to purge instance`,
+                type: "ERROR",
+                description: error instanceof Error ? error.message : 
undefined,
+              });
+            }
+            setPurging(null);
+          }}
+        />
+      )}
+    </Fragment>
+  );
 }
diff --git 
a/packages/merchant-backoffice/src/paths/instance/kyc/list/ListPage.tsx 
b/packages/merchant-backoffice/src/paths/instance/kyc/list/ListPage.tsx
new file mode 100644
index 0000000..13f9b03
--- /dev/null
+++ b/packages/merchant-backoffice/src/paths/instance/kyc/list/ListPage.tsx
@@ -0,0 +1,180 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+import { h, VNode } from "preact";
+import { MerchantBackend } from "../../../../declaration";
+import { Translate, useTranslator } from "../../../../i18n";
+
+export interface Props {
+  status: MerchantBackend.Instances.AccountKycRedirects;
+}
+
+export function ListPage({ status }: Props): VNode {
+  const i18n = useTranslator();
+
+  return (
+    <section class="section is-main-section">
+      <p>asdasdasd</p>
+
+      <div class="card has-table">
+        <header class="card-header">
+          <p class="card-header-title">
+            <span class="icon">
+              <i class="mdi mdi-clock" />
+            </span>
+            <Translate>Pending KYC verification</Translate>
+          </p>
+
+          <div class="card-header-icon" aria-label="more options" />
+        </header>
+        <div class="card-content">
+          <div class="b-table has-pagination">
+            <div class="table-wrapper has-mobile-cards">
+              {status.pending_kycs.length > 0 ? (
+                <PendingTable entries={status.pending_kycs} />
+              ) : (
+                <EmptyTable />
+              )}
+            </div>
+          </div>
+        </div>
+      </div>
+
+      {status.timeout_kycs.length > 0 ? (
+        <div class="card has-table">
+          <header class="card-header">
+            <p class="card-header-title">
+              <span class="icon">
+                <i class="mdi mdi-clock" />
+              </span>
+              <Translate>Timed out</Translate>
+            </p>
+
+            <div class="card-header-icon" aria-label="more options" />
+          </header>
+          <div class="card-content">
+            <div class="b-table has-pagination">
+              <div class="table-wrapper has-mobile-cards">
+                {status.timeout_kycs.length > 0 ? (
+                  <TimedOutTable entries={status.timeout_kycs} />
+                ) : (
+                  <EmptyTable />
+                )}
+              </div>
+            </div>
+          </div>
+        </div>
+      ) : undefined}
+    </section>
+  );
+}
+interface PendingTableProps {
+  entries: MerchantBackend.Instances.MerchantAccountKycRedirect[];
+}
+
+interface TimedOutTableProps {
+  entries: MerchantBackend.Instances.ExchangeKycTimeout[];
+}
+
+function PendingTable({ entries }: PendingTableProps): VNode {
+  return (
+    <div class="table-container">
+      <table class="table is-striped is-hoverable is-fullwidth">
+        <thead>
+          <tr>
+            <th>
+              <Translate>Exchange</Translate>
+            </th>
+            <th>
+              <Translate>Target account</Translate>
+            </th>
+            <th>
+              <Translate>KYC URL</Translate>
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          {entries.map((e, i) => {
+            return (
+              <tr key={i}>
+                <td>{e.exchange_url}</td>
+                <td>{e.payto_uri}</td>
+                <td>
+                  <a href={e.kyc_url} target="_black" rel="noreferrer">
+                    {e.kyc_url}
+                  </a>
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+    </div>
+  );
+}
+
+function TimedOutTable({ entries }: TimedOutTableProps): VNode {
+  return (
+    <div class="table-container">
+      <table class="table is-striped is-hoverable is-fullwidth">
+        <thead>
+          <tr>
+            <th>
+              <Translate>Exchange</Translate>
+            </th>
+            <th>
+              <Translate>Code</Translate>
+            </th>
+            <th>
+              <Translate>Http Status</Translate>
+            </th>
+          </tr>
+        </thead>
+        <tbody>
+          {entries.map((e, i) => {
+            return (
+              <tr key={i}>
+                <td>{e.exchange_url}</td>
+                <td>{e.exchange_code}</td>
+                <td>{e.exchange_http_status}</td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+    </div>
+  );
+}
+
+function EmptyTable(): VNode {
+  return (
+    <div class="content has-text-grey has-text-centered">
+      <p>
+        <span class="icon is-large">
+          <i class="mdi mdi-emoticon-happy mdi-48px" />
+        </span>
+      </p>
+      <p>
+        <Translate>No pending kyc verification!</Translate>
+      </p>
+    </div>
+  );
+}
diff --git 
a/packages/merchant-backoffice/src/paths/instance/reserves/details/index.tsx 
b/packages/merchant-backoffice/src/paths/instance/kyc/list/index.tsx
similarity index 74%
copy from 
packages/merchant-backoffice/src/paths/instance/reserves/details/index.tsx
copy to packages/merchant-backoffice/src/paths/instance/kyc/list/index.tsx
index c2483f0..5dff019 100644
--- a/packages/merchant-backoffice/src/paths/instance/reserves/details/index.tsx
+++ b/packages/merchant-backoffice/src/paths/instance/kyc/list/index.tsx
@@ -19,38 +19,33 @@
  * @author Sebastian Javier Marchano (sebasjm)
  */
 
-import { Fragment, h, VNode } from "preact";
+import { h, VNode } from "preact";
 import { Loading } from "../../../../components/exception/loading";
 import { HttpError } from "../../../../hooks/backend";
-import { useReserveDetails } from "../../../../hooks/reserves";
-import { DetailPage } from "./DetailPage";
+import { useInstanceKYCDetails } from "../../../../hooks/instance";
+import { ListPage } from "./ListPage";
 
 interface Props {
-  rid: string;
-
   onUnauthorized: () => VNode;
   onLoadError: (error: HttpError) => VNode;
   onNotFound: () => VNode;
-  onDelete: () => void;
-  onBack: () => void;
 }
-export default function DetailReserve({
-  rid,
+
+export default function ListKYC({
   onUnauthorized,
   onLoadError,
   onNotFound,
-  onBack,
-  onDelete,
 }: Props): VNode {
-  const result = useReserveDetails(rid);
-
+  const result = useInstanceKYCDetails();
   if (result.clientError && result.isUnauthorized) return onUnauthorized();
   if (result.clientError && result.isNotfound) return onNotFound();
   if (result.loading) return <Loading />;
   if (!result.ok) return onLoadError(result);
-  return (
-    <Fragment>
-      <DetailPage selected={result.data} onBack={onBack} id={rid} />
-    </Fragment>
-  );
+
+  const status = result.data.type === "ok" ? undefined : result.data.status;
+
+  if (!status) {
+    return <div>no kyc required</div>;
+  }
+  return <ListPage status={status} />;
 }
diff --git a/packages/merchant-backoffice/tests/axiosMock.ts 
b/packages/merchant-backoffice/tests/axiosMock.ts
index 7b33e2a..412d2a0 100644
--- a/packages/merchant-backoffice/tests/axiosMock.ts
+++ b/packages/merchant-backoffice/tests/axiosMock.ts
@@ -330,3 +330,105 @@ export const API_DELETE_RESERVE = (
   delete: `http://backend/instances/default/private/reserves/${id}`,
 });
 
+
+////////////////////
+// INSTANCE ADMIN
+////////////////////
+
+export const API_CREATE_INSTANCE: Query<
+  MerchantBackend.Instances.InstanceConfigurationMessage,
+  unknown
+> = {
+  post: "http://backend/management/instances";,
+};
+
+export const API_GET_INSTANCE_BY_ID = (
+  id: string
+): Query<
+  unknown,
+  MerchantBackend.Instances.QueryInstancesResponse
+> => ({
+  get: `http://backend/management/instances/${id}`,
+});
+
+export const API_GET_INSTANCE_KYC_BY_ID = (
+  id: string
+): Query<
+  unknown,
+  MerchantBackend.Instances.AccountKycRedirects
+> => ({
+  get: `http://backend/management/instances/${id}/kyc`,
+});
+
+export const API_LIST_INSTANCES: Query<
+  unknown,
+  MerchantBackend.Instances.InstancesResponse
+> = {
+  get: "http://backend/management/instances";,
+};
+
+export const API_UPDATE_INSTANCE_BY_ID = (
+  id: string
+): Query<
+  MerchantBackend.Instances.InstanceReconfigurationMessage,
+  unknown
+> => ({
+  patch: `http://backend/management/instances/${id}`,
+});
+
+export const API_UPDATE_INSTANCE_AUTH_BY_ID = (
+  id: string
+): Query<
+  MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+  unknown
+> => ({
+  post: `http://backend/management/instances/${id}/auth`,
+});
+
+export const API_DELETE_INSTANCE = (
+  id: string
+): Query<unknown, unknown> => ({
+  delete: `http://backend/management/instances/${id}`,
+});
+
+////////////////////
+// INSTANCE 
+////////////////////
+
+export const API_GET_CURRENT_INSTANCE: Query<
+  unknown,
+  MerchantBackend.Instances.QueryInstancesResponse
+> = ({
+  get: `http://backend/instances/default/private/`,
+});
+
+export const API_GET_CURRENT_INSTANCE_KYC: Query<
+  unknown,
+  MerchantBackend.Instances.AccountKycRedirects
+> =
+  ({
+    get: `http://backend/instances/default/private/kyc`,
+  });
+
+export const API_UPDATE_CURRENT_INSTANCE: Query<
+  MerchantBackend.Instances.InstanceReconfigurationMessage,
+  unknown
+> = {
+  patch: `http://backend/instances/default/private/`,
+};
+
+export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query<
+  MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+  unknown
+> = {
+  post: `http://backend/instances/default/private/auth`,
+};
+
+export const API_DELETE_CURRENT_INSTANCE: Query<
+  unknown,
+  unknown
+> = ({
+  delete: `http://backend/instances/default/private`,
+});
+
+
diff --git a/packages/merchant-backoffice/tests/hooks/async.test.ts 
b/packages/merchant-backoffice/tests/hooks/async.test.ts
new file mode 100644
index 0000000..a6d0cdd
--- /dev/null
+++ b/packages/merchant-backoffice/tests/hooks/async.test.ts
@@ -0,0 +1,158 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { renderHook } from "@testing-library/preact-hooks"
+import { useAsync } from "../../src/hooks/async"
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+test("async function is called", async () => {
+  jest.useFakeTimers()
+
+  const timeout = 500
+
+  const asyncFunction = jest.fn(() => new Promise((res) => {
+    setTimeout(() => {
+      res({ the_answer: 'yes' })
+    }, timeout);
+  }))
+
+  const { result, waitForNextUpdate } = renderHook(() => {
+    return useAsync(asyncFunction)
+  })
+
+  expect(result.current?.isLoading).toBeFalsy()
+
+  result.current?.request()
+  expect(asyncFunction).toBeCalled()
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeTruthy()
+
+  jest.advanceTimersByTime(timeout + 1)
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeFalsy()
+  expect(result.current?.data).toMatchObject({ the_answer: 'yes' })
+  expect(result.current?.error).toBeUndefined()
+  expect(result.current?.isSlow).toBeFalsy()
+})
+
+test("async function return error if rejected", async () => {
+  jest.useFakeTimers()
+
+  const timeout = 500
+
+  const asyncFunction = jest.fn(() => new Promise((_, rej) => {
+    setTimeout(() => {
+      rej({ the_error: 'yes' })
+    }, timeout);
+  }))
+
+  const { result, waitForNextUpdate } = renderHook(() => {
+    return useAsync(asyncFunction)
+  })
+
+  expect(result.current?.isLoading).toBeFalsy()
+
+  result.current?.request()
+  expect(asyncFunction).toBeCalled()
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeTruthy()
+
+  jest.advanceTimersByTime(timeout + 1)
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeFalsy()
+  expect(result.current?.error).toMatchObject({ the_error: 'yes' })
+  expect(result.current?.data).toBeUndefined()
+  expect(result.current?.isSlow).toBeFalsy()
+})
+
+test("async function is slow", async () => {
+  jest.useFakeTimers()
+
+  const timeout = 2200
+
+  const asyncFunction = jest.fn(() => new Promise((res) => {
+    setTimeout(() => {
+      res({ the_answer: 'yes' })
+    }, timeout);
+  }))
+
+  const { result, waitForNextUpdate } = renderHook(() => {
+    return useAsync(asyncFunction)
+  })
+
+  expect(result.current?.isLoading).toBeFalsy()
+
+  result.current?.request()
+  expect(asyncFunction).toBeCalled()
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeTruthy()
+
+  jest.advanceTimersByTime(timeout / 2)
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeTruthy()
+  expect(result.current?.isSlow).toBeTruthy()
+  expect(result.current?.data).toBeUndefined()
+  expect(result.current?.error).toBeUndefined()
+
+  jest.advanceTimersByTime(timeout / 2)
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeFalsy()
+  expect(result.current?.data).toMatchObject({ the_answer: 'yes' })
+  expect(result.current?.error).toBeUndefined()
+  expect(result.current?.isSlow).toBeFalsy()
+
+})
+
+test("async function is cancellable", async () => {
+  jest.useFakeTimers()
+
+  const timeout = 2200
+
+  const asyncFunction = jest.fn(() => new Promise((res) => {
+    setTimeout(() => {
+      res({ the_answer: 'yes' })
+    }, timeout);
+  }))
+
+  const { result, waitForNextUpdate } = renderHook(() => {
+    return useAsync(asyncFunction)
+  })
+
+  expect(result.current?.isLoading).toBeFalsy()
+
+  result.current?.request()
+  expect(asyncFunction).toBeCalled()
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeTruthy()
+
+  jest.advanceTimersByTime(timeout / 2)
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeTruthy()
+  expect(result.current?.isSlow).toBeTruthy()
+  expect(result.current?.data).toBeUndefined()
+  expect(result.current?.error).toBeUndefined()
+
+  result.current?.cancel()
+  await waitForNextUpdate({ timeout: 1 })
+  expect(result.current?.isLoading).toBeFalsy()
+  expect(result.current?.data).toBeUndefined()
+  expect(result.current?.error).toBeUndefined()
+  expect(result.current?.isSlow).toBeFalsy()
+
+})
diff --git a/packages/merchant-backoffice/tests/hooks/swr/instance.test.ts 
b/packages/merchant-backoffice/tests/hooks/swr/instance.test.ts
new file mode 100644
index 0000000..55d9fa6
--- /dev/null
+++ b/packages/merchant-backoffice/tests/hooks/swr/instance.test.ts
@@ -0,0 +1,636 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { renderHook } from "@testing-library/preact-hooks";
+import { act } from "preact/test-utils";
+import { MerchantBackend } from "../../../src/declaration";
+import { useAdminAPI, useBackendInstances, useInstanceAPI, useInstanceDetails, 
useManagementAPI } from "../../../src/hooks/instance";
+import {
+  API_CREATE_INSTANCE,
+  API_DELETE_INSTANCE,
+  API_GET_CURRENT_INSTANCE,
+  API_LIST_INSTANCES,
+  API_UPDATE_CURRENT_INSTANCE,
+  API_UPDATE_CURRENT_INSTANCE_AUTH,
+  API_UPDATE_INSTANCE_AUTH_BY_ID,
+  API_UPDATE_INSTANCE_BY_ID,
+  assertJustExpectedRequestWereMade,
+  AxiosMockEnvironment
+} from "../../axiosMock";
+import { TestingContext } from "./index";
+
+describe("instance api interaction with details ", () => {
+
+  it("should evict cache when updating an instance", async () => {
+
+    const env = new AxiosMockEnvironment();
+
+    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+      response: {
+        name: 'instance_name'
+      } as MerchantBackend.Instances.QueryInstancesResponse,
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useInstanceAPI();
+        const query = useInstanceDetails();
+
+        return { query, api };
+      },
+      { wrapper: TestingContext }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      name: 'instance_name'
+    });
+
+    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, {
+      request: {
+        name: 'other_name'
+      } as MerchantBackend.Instances.InstanceReconfigurationMessage,
+    });
+
+    act(async () => {
+      await result.current?.api.updateInstance({
+        name: 'other_name'
+      } as MerchantBackend.Instances.InstanceReconfigurationMessage);
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+      response: {
+        name: 'other_name'
+      } as MerchantBackend.Instances.QueryInstancesResponse,
+    });
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      name: 'other_name'
+    });
+  });
+
+  it("should evict cache when setting the instance's token", async () => {
+    const env = new AxiosMockEnvironment();
+
+    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+      response: {
+        name: 'instance_name',
+        auth: {
+          method: 'token',
+          token: 'not-secret',
+        }
+      } as MerchantBackend.Instances.QueryInstancesResponse,
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useInstanceAPI();
+        const query = useInstanceDetails();
+
+        return { query, api };
+      },
+      { wrapper: TestingContext }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      name: 'instance_name',
+      auth: {
+        method: 'token',
+        token: 'not-secret',
+      }
+    });
+
+    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
+      request: {
+        method: 'token',
+        token: 'secret'
+      } as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+    });
+
+    act(async () => {
+      await result.current?.api.setNewToken('secret');
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+      response: {
+        name: 'instance_name',
+        auth: {
+          method: 'token',
+          token: 'secret',
+        }
+      } as MerchantBackend.Instances.QueryInstancesResponse,
+    });
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      name: 'instance_name',
+      auth: {
+        method: 'token',
+        token: 'secret',
+      }
+    });
+  });
+
+  it("should evict cache when clearing the instance's token", async () => {
+    const env = new AxiosMockEnvironment();
+
+    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+      response: {
+        name: 'instance_name',
+        auth: {
+          method: 'token',
+          token: 'not-secret',
+        }
+      } as MerchantBackend.Instances.QueryInstancesResponse,
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useInstanceAPI();
+        const query = useInstanceDetails();
+
+        return { query, api };
+      },
+      { wrapper: TestingContext }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      name: 'instance_name',
+      auth: {
+        method: 'token',
+        token: 'not-secret',
+      }
+    });
+
+    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
+      request: {
+        method: 'external',
+      } as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
+    });
+
+    act(async () => {
+      await result.current?.api.clearToken();
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
+      response: {
+        name: 'instance_name',
+        auth: {
+          method: 'external',
+        }
+      } as MerchantBackend.Instances.QueryInstancesResponse,
+    });
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      name: 'instance_name',
+      auth: {
+        method: 'external',
+      }
+    });
+  });
+});
+
+describe("instance admin api interaction with listing ", () => {
+
+  it("should evict cache when creating a new instance", async () => {
+    const env = new AxiosMockEnvironment();
+
+    env.addRequestExpectation(API_LIST_INSTANCES, {
+      response: {
+        instances: [{
+          name: 'instance_name'
+        } as MerchantBackend.Instances.Instance]
+      },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useAdminAPI();
+        const query = useBackendInstances();
+
+        return { query, api };
+      },
+      { wrapper: TestingContext }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      instances: [{
+        name: 'instance_name'
+      }]
+    });
+
+    env.addRequestExpectation(API_CREATE_INSTANCE, {
+      request: {
+        name: 'other_name'
+      } as MerchantBackend.Instances.InstanceConfigurationMessage,
+    });
+
+    act(async () => {
+      await result.current?.api.createInstance({
+        name: 'other_name'
+      } as MerchantBackend.Instances.InstanceConfigurationMessage);
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_LIST_INSTANCES, {
+      response: {
+        instances: [{
+          name: 'instance_name'
+        } as MerchantBackend.Instances.Instance,
+        {
+          name: 'other_name'
+        } as MerchantBackend.Instances.Instance]
+      },
+    });
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      instances: [{
+        name: 'instance_name'
+      }, {
+        name: 'other_name'
+      }]
+    });
+  });
+
+  it("should evict cache when deleting an instance", async () => {
+    const env = new AxiosMockEnvironment();
+
+    env.addRequestExpectation(API_LIST_INSTANCES, {
+      response: {
+        instances: [{
+          id: 'default',
+          name: 'instance_name'
+        } as MerchantBackend.Instances.Instance,
+        {
+          id: 'the_id',
+          name: 'second_instance'
+        } as MerchantBackend.Instances.Instance]
+      },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useAdminAPI();
+        const query = useBackendInstances();
+
+        return { query, api };
+      },
+      { wrapper: TestingContext }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      instances: [{
+        id: 'default',
+        name: 'instance_name'
+      }, {
+        id: 'the_id',
+        name: 'second_instance'
+      }]
+    });
+
+    env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {});
+
+    act(async () => {
+      await result.current?.api.deleteInstance('the_id');
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_LIST_INSTANCES, {
+      response: {
+        instances: [{
+          id: 'default',
+          name: 'instance_name'
+        } as MerchantBackend.Instances.Instance]
+      },
+    });
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      instances: [{
+        id: 'default',
+        name: 'instance_name'
+      }]
+    });
+  });
+  it("should evict cache when deleting (purge) an instance", async () => {
+    const env = new AxiosMockEnvironment();
+
+    env.addRequestExpectation(API_LIST_INSTANCES, {
+      response: {
+        instances: [{
+          id: 'default',
+          name: 'instance_name'
+        } as MerchantBackend.Instances.Instance,
+        {
+          id: 'the_id',
+          name: 'second_instance'
+        } as MerchantBackend.Instances.Instance]
+      },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useAdminAPI();
+        const query = useBackendInstances();
+
+        return { query, api };
+      },
+      { wrapper: TestingContext }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      instances: [{
+        id: 'default',
+        name: 'instance_name'
+      }, {
+        id: 'the_id',
+        name: 'second_instance'
+      }]
+    });
+
+    env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {
+      qparam: {
+        purge: 'YES'
+      }
+    });
+
+    act(async () => {
+      await result.current?.api.purgeInstance('the_id');
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_LIST_INSTANCES, {
+      response: {
+        instances: [{
+          id: 'default',
+          name: 'instance_name'
+        } as MerchantBackend.Instances.Instance]
+      },
+    });
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      instances: [{
+        id: 'default',
+        name: 'instance_name'
+      }]
+    });
+  });
+});
+
+describe("instance management api interaction with listing ", () => {
+
+  it("should evict cache when updating an instance", async () => {
+    const env = new AxiosMockEnvironment();
+
+    env.addRequestExpectation(API_LIST_INSTANCES, {
+      response: {
+        instances: [{
+          id: 'managed',
+          name: 'instance_name'
+        } as MerchantBackend.Instances.Instance]
+      },
+    });
+
+    const { result, waitForNextUpdate } = renderHook(
+      () => {
+        const api = useManagementAPI('managed');
+        const query = useBackendInstances();
+
+        return { query, api };
+      },
+      { wrapper: TestingContext }
+    );
+
+    if (!result.current) {
+      expect(result.current).toBeDefined();
+      return;
+    }
+    expect(result.current.query.loading).toBeTruthy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    expect(result.current?.query.ok).toBeTruthy();
+    if (!result.current?.query.ok) return;
+
+    expect(result.current.query.data).toEqual({
+      instances: [{
+        id: 'managed',
+        name: 'instance_name'
+      }]
+    });
+
+    env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID('managed'), {
+      request: {
+        name: 'other_name'
+      } as MerchantBackend.Instances.InstanceReconfigurationMessage,
+    });
+
+    act(async () => {
+      await result.current?.api.updateInstance({
+        name: 'other_name'
+      } as MerchantBackend.Instances.InstanceConfigurationMessage);
+    });
+
+    assertJustExpectedRequestWereMade(env);
+
+    env.addRequestExpectation(API_LIST_INSTANCES, {
+      response: {
+        instances: [
+          {
+            id: 'managed',
+            name: 'other_name'
+          } as MerchantBackend.Instances.Instance]
+      },
+    });
+
+    expect(result.current.query.loading).toBeFalsy();
+
+    await waitForNextUpdate({ timeout: 1 });
+
+    assertJustExpectedRequestWereMade(env);
+
+    expect(result.current.query.loading).toBeFalsy();
+    expect(result.current.query.ok).toBeTruthy();
+
+    expect(result.current.query.data).toEqual({
+      instances: [{
+        id: 'managed',
+        name: 'other_name'
+      }]
+    });
+  });
+
+});
+

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